Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
1 change: 1 addition & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,7 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().InsertChatMessages(gomock.Any(), arg).Return(msgs, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(msgs)
}))

s.Run("InsertChatQueuedMessage", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := testutil.Fake(s.T(), faker, database.InsertChatQueuedMessageParams{ChatID: chat.ID})
Expand Down
1 change: 1 addition & 0 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func Chat(t testing.TB, db database.Store, seed database.Chat) database.Chat {
}

chat, err := db.InsertChat(genCtx, database.InsertChatParams{
ID: uuid.NullUUID{UUID: seed.ID, Valid: seed.ID != uuid.Nil},
OrganizationID: takeFirst(seed.OrganizationID, uuid.New()),
OwnerID: takeFirst(seed.OwnerID, uuid.New()),
WorkspaceID: seed.WorkspaceID,
Expand Down
22 changes: 13 additions & 9 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/database/queries/chats.sql
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ ORDER BY
-- name: InsertChat :one
WITH inserted_chat AS (
INSERT INTO chats (
id,
organization_id,
owner_id,
workspace_id,
Expand All @@ -795,6 +796,7 @@ INSERT INTO chats (
dynamic_tools,
client_type
) VALUES (
COALESCE(sqlc.narg('id')::uuid, gen_random_uuid()),
@organization_id::uuid,
@owner_id::uuid,
sqlc.narg('workspace_id')::uuid,
Expand Down
5 changes: 3 additions & 2 deletions coderd/x/chatd/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ I don't recommend reading the rest of section thoroughly if this is your first t
- `DeleteQueuedMessage(qid)` removes one queued message without changing the active history.
- `PromoteQueuedMessage(qid)` makes a queued message the next message to process. It reorders the queue, interrupts active work, cancels pending dynamic-tool action, or promotes into history immediately as required by the input state.
- `Interrupt(reason)` requests cancellation of an active generation or closes pending dynamic-tool action. It preserves queued backlog.
- `CompleteRequiresAction(results)` inserts submitted tool-result messages, clears `requires_action_deadline_at`, and lands in `running`. It preserves queued messages.
- `CompleteRequiresAction(results)` inserts submitted tool-result messages followed by any caller-provided suffix messages, clears `requires_action_deadline_at`, and lands in `running`. It preserves queued messages.
- `RequestCompaction` records a manual compaction request on an idle chat by setting `compaction_requested_at` and landing in `running` without inserting any message. The chat worker picks the chat up like any other running chat and consumes the request. See [Manual compaction](#manual-compaction).

### Transitions used by the chat worker
Expand All @@ -128,7 +128,7 @@ I don't recommend reading the rest of section thoroughly if this is your first t
- `RecordGenerationAttempt` verifies the chat is still `running`, increments `generation_attempt`, and returns the updated chat snapshot.
- `RecordRetryState(payload)` verifies the chat is still `running`, stores the retry payload sent to clients as `retry_state`, and returns the updated chat snapshot.
- `FinishTurn` completes the current generation turn atomically. If the queue is empty, it lands in `waiting`. If the queue is non-empty, it removes the queue head, inserts it into history as a user turn, and lands in `running`.
- `FinishError(err)` ends a running chat in `error` and persists `last_error = err`, overwriting any prior stored error.
- `FinishError(err)` parks the chat in `error` and persists `last_error = err`, replacing any previously stored error. It is allowed when an unarchived chat is waiting or running.
- `CancelRequiresAction(reason)` closes pending dynamic tool calls with synthetic cancellation tool results, satisfies the pending-action projection, clears `requires_action_deadline_at`, and lands in `running`.
- `ReconcileInvalidState` reconciles a chat in an invalid state by setting it to a valid state. Defined in the [Invalid states](#invalid-states) section.

Expand All @@ -149,6 +149,7 @@ stateDiagram-v2
W --> R0: SendMessage
W --> R0: EditMessage
W --> R0: RequestCompaction
W --> E0: FinishError
W --> XW: SetArchived(true)

E0 --> R0: SendMessage
Expand Down
29 changes: 29 additions & 0 deletions coderd/x/chatd/chatstate/toolresults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package chatstate

import "encoding/json"

// ValidateToolResults returns the first validation error for results
// submitted against pending dynamic tool calls.
func ValidateToolResults(results []ToolResultInput, pending map[string]string) *ToolResultValidationError {
submitted := make(map[string]struct{}, len(results))
for _, result := range results {
if _, dup := submitted[result.ToolCallID]; dup {
return &ToolResultValidationError{Cause: ErrToolResultDuplicate, ToolCallID: result.ToolCallID}
}
if !json.Valid(result.Output) {
return &ToolResultValidationError{Cause: ErrToolResultInvalidJSON, ToolCallID: result.ToolCallID}
}
submitted[result.ToolCallID] = struct{}{}
}
for toolCallID := range pending {
if _, ok := submitted[toolCallID]; !ok {
return &ToolResultValidationError{Cause: ErrToolResultMissing, ToolCallID: toolCallID}
}
}
for toolCallID := range submitted {
if _, ok := pending[toolCallID]; !ok {
return &ToolResultValidationError{Cause: ErrToolResultUnexpected, ToolCallID: toolCallID}
}
}
return nil
}
85 changes: 85 additions & 0 deletions coderd/x/chatd/chatstate/toolresults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package chatstate_test

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/x/chatd/chatstate"
)

func TestValidateToolResults(t *testing.T) {
t.Parallel()

// Validation order is part of the contract because callers surface only
// the first violation.
pending := map[string]string{"call_a": "execute", "call_b": "read_file"}
resultA := chatstate.ToolResultInput{ToolCallID: "call_a", Output: json.RawMessage(`{"ok":true}`)}
resultB := chatstate.ToolResultInput{ToolCallID: "call_b", Output: json.RawMessage(`"done"`)}
badJSON := chatstate.ToolResultInput{ToolCallID: "call_b", Output: json.RawMessage(`{`)}
resultC := chatstate.ToolResultInput{ToolCallID: "call_c", Output: json.RawMessage(`{}`)}

cases := []struct {
name string
results []chatstate.ToolResultInput
wantCause error
wantToolCallID string
}{
{
name: "Complete",
results: []chatstate.ToolResultInput{resultA, resultB},
},
{
name: "Duplicate",
results: []chatstate.ToolResultInput{resultA, resultB, resultA},
wantCause: chatstate.ErrToolResultDuplicate,
wantToolCallID: "call_a",
},
{
name: "InvalidJSON",
results: []chatstate.ToolResultInput{resultA, badJSON},
wantCause: chatstate.ErrToolResultInvalidJSON,
wantToolCallID: "call_b",
},
{
name: "Missing",
results: []chatstate.ToolResultInput{resultA},
wantCause: chatstate.ErrToolResultMissing,
wantToolCallID: "call_b",
},
{
name: "Unexpected",
results: []chatstate.ToolResultInput{resultA, resultB, resultC},
wantCause: chatstate.ErrToolResultUnexpected,
wantToolCallID: "call_c",
},
{
name: "PerResultRulesOutrankSweeps",
results: []chatstate.ToolResultInput{resultC, badJSON},
wantCause: chatstate.ErrToolResultInvalidJSON,
wantToolCallID: "call_b",
},
{
name: "MissingOutranksUnexpected",
results: []chatstate.ToolResultInput{resultA, resultC},
wantCause: chatstate.ErrToolResultMissing,
wantToolCallID: "call_b",
},
}

for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

invalid := chatstate.ValidateToolResults(test.results, pending)
if test.wantCause == nil {
require.Nil(t, invalid)
return
}
require.NotNil(t, invalid)
require.ErrorIs(t, invalid, test.wantCause)
require.Equal(t, test.wantToolCallID, invalid.ToolCallID)
})
}
}
1 change: 1 addition & 0 deletions coderd/x/chatd/chatstate/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ var transitionMatrix = map[ExecutionState]map[Transition][]ExecutionState{
TransitionSendMessage: {StateR0},
TransitionEditMessage: {StateR0},
TransitionRequestCompaction: {StateR0},
TransitionFinishError: {StateE0},
},
StateE0: {
TransitionSetArchived: {StateXE0},
Expand Down
Loading
Loading