mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add CreatedAt to tool-call and tool-result ChatMessageParts (#24101)
Adds an optional `CreatedAt` timestamp to `tool-call` and `tool-result` `ChatMessagePart` variants so the frontend can compute tool execution duration (`result.created_at - call.created_at`). Timestamps are recorded at the correct moments in the chatloop: - **Tool-call**: when the model stream emits the tool call - **Tool-result**: when tool execution completes (or is interrupted) These are passed through `PersistedStep.PartCreatedAt` so the persistence layer can apply accurate timestamps to stored parts. SSE-published parts also carry `CreatedAt` for real-time display. Old persisted messages without `created_at` deserialize to `nil` — fully backward compatible. <details><summary>Implementation notes (Coder Agents generated)</summary> ### Why not stamp in `PartFromContent`? `PartFromContent` is called both for SSE publishing (correct timing) and during persistence (wrong timing — both tool-call and tool-result would get the same "persistence time" timestamp, yielding ~0 duration). Instead, timestamps are captured in the chatloop at the right moments and carried through `PersistedStep.PartCreatedAt` as a `map[string]time.Time` keyed by `"call:<id>"` / `"result:<id>"`. ### Interrupted tool calls `persistInterruptedStep` also stamps `CreatedAt` on synthetic error results for cancelled/interrupted tool calls, so partial duration is available. ### Files changed | File | Change | |------|--------| | `codersdk/chats.go` | Add `CreatedAt *time.Time` field | | `codersdk/chats_test.go` | JSON round-trip test | | `coderd/database/dbtime/dbtime.go` | Add `TimePtr` helper | | `coderd/x/chatd/chatloop/chatloop.go` | Track timestamps, pass through `PersistedStep` | | `coderd/x/chatd/chatd.go` | Apply timestamps during persistence | | `coderd/x/chatd/chatprompt/chatprompt_test.go` | Verify `PartFromContent` does NOT stamp | | `site/src/api/typesGenerated.ts` | Auto-generated | </details> --------- Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com>
This commit is contained in:
@@ -214,6 +214,10 @@ type ChatMessagePart struct {
|
||||
// ProviderExecuted indicates the tool call was executed by
|
||||
// the provider (e.g. Anthropic computer use).
|
||||
ProviderExecuted bool `json:"provider_executed,omitempty" variants:"tool-call?,tool-result?"`
|
||||
// CreatedAt records when this part was produced. Present on
|
||||
// tool-call and tool-result parts so the frontend can compute
|
||||
// tool execution duration.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty" format:"date-time" variants:"tool-call?,tool-result?"`
|
||||
// ContextFilePath is the absolute path of a file loaded into
|
||||
// the LLM context (e.g. an AGENTS.md instruction file).
|
||||
ContextFilePath string `json:"context_file_path" variants:"context-file"`
|
||||
|
||||
@@ -329,6 +329,42 @@ func TestChatMessagePartVariantTags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestChatMessagePart_CreatedAt_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("RoundTrips", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := time.Date(2025, 6, 15, 12, 30, 0, 0, time.UTC)
|
||||
part := codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: "tc-1",
|
||||
ToolName: "execute",
|
||||
CreatedAt: &ts,
|
||||
}
|
||||
data, err := json.Marshal(part)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(data), `"created_at"`)
|
||||
|
||||
var decoded codersdk.ChatMessagePart
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decoded.CreatedAt)
|
||||
require.True(t, ts.Equal(*decoded.CreatedAt))
|
||||
})
|
||||
|
||||
t.Run("OmittedWhenNil", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
part := codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeToolCall,
|
||||
ToolCallID: "tc-1",
|
||||
ToolName: "execute",
|
||||
}
|
||||
data, err := json.Marshal(part)
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, string(data), `"created_at"`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModelCostConfig_LegacyNumericJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user