kanna-code 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -19
- package/dist/client/assets/index-C-sGbl7X.js +409 -0
- package/dist/client/assets/index-gld9RxCU.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +18 -2
- package/src/server/agent.test.ts +415 -0
- package/src/server/agent.ts +483 -194
- package/src/server/codex-app-server-protocol.ts +440 -0
- package/src/server/codex-app-server.test.ts +1303 -0
- package/src/server/codex-app-server.ts +1277 -0
- package/src/server/event-store.ts +81 -34
- package/src/server/events.ts +25 -17
- package/src/server/harness-types.ts +19 -0
- package/src/server/provider-catalog.test.ts +34 -0
- package/src/server/provider-catalog.ts +77 -0
- package/src/server/read-models.test.ts +63 -0
- package/src/server/read-models.ts +5 -1
- package/src/shared/protocol.ts +12 -2
- package/src/shared/tools.test.ts +88 -0
- package/src/shared/tools.ts +233 -0
- package/src/shared/types.ts +404 -5
- package/dist/client/assets/index-BRiM6Nxc.css +0 -1
- package/dist/client/assets/index-DelZ0MyD.js +0 -418
package/src/server/agent.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { query, type CanUseTool, type PermissionResult, type Query } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import type {
|
|
3
|
+
AgentProvider,
|
|
4
|
+
NormalizedToolCall,
|
|
5
|
+
PendingToolSnapshot,
|
|
6
|
+
KannaStatus,
|
|
7
|
+
TranscriptEntry,
|
|
8
|
+
} from "../shared/types"
|
|
9
|
+
import { normalizeToolCall } from "../shared/tools"
|
|
2
10
|
import type { ClientCommand } from "../shared/protocol"
|
|
3
|
-
|
|
4
|
-
import type { KannaStatus, PendingToolSnapshot } from "../shared/types"
|
|
5
11
|
import { EventStore } from "./event-store"
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
import { CodexAppServerManager } from "./codex-app-server"
|
|
13
|
+
import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
|
|
14
|
+
import {
|
|
15
|
+
codexServiceTierFromModelOptions,
|
|
16
|
+
getServerProviderCatalog,
|
|
17
|
+
normalizeClaudeModelOptions,
|
|
18
|
+
normalizeCodexModelOptions,
|
|
19
|
+
normalizeServerModel,
|
|
20
|
+
} from "./provider-catalog"
|
|
21
|
+
|
|
22
|
+
const CLAUDE_TOOLSET = [
|
|
11
23
|
"Skill",
|
|
12
24
|
"WebFetch",
|
|
13
25
|
"WebSearch",
|
|
@@ -28,16 +40,21 @@ const TOOLSET = [
|
|
|
28
40
|
|
|
29
41
|
interface PendingToolRequest {
|
|
30
42
|
toolUseId: string
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
resolve: (result: PermissionResult) => void
|
|
43
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
44
|
+
resolve: (result: unknown) => void
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
interface ActiveTurn {
|
|
37
48
|
chatId: string
|
|
38
|
-
|
|
49
|
+
provider: AgentProvider
|
|
50
|
+
turn: HarnessTurn
|
|
51
|
+
model: string
|
|
52
|
+
effort?: string
|
|
53
|
+
serviceTier?: "fast"
|
|
54
|
+
planMode: boolean
|
|
39
55
|
status: KannaStatus
|
|
40
56
|
pendingTool: PendingToolRequest | null
|
|
57
|
+
postToolFollowUp: { content: string; planMode: boolean } | null
|
|
41
58
|
hasFinalResult: boolean
|
|
42
59
|
cancelRequested: boolean
|
|
43
60
|
cancelRecorded: boolean
|
|
@@ -46,70 +63,270 @@ interface ActiveTurn {
|
|
|
46
63
|
interface AgentCoordinatorArgs {
|
|
47
64
|
store: EventStore
|
|
48
65
|
onStateChange: () => void
|
|
66
|
+
codexManager?: CodexAppServerManager
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
function
|
|
52
|
-
|
|
69
|
+
function deriveChatTitle(content: string) {
|
|
70
|
+
const singleLine = content.replace(/\s+/g, " ").trim()
|
|
71
|
+
return singleLine.slice(0, 60) || "New Chat"
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
content: JSON.stringify(body),
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
})
|
|
74
|
+
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(
|
|
75
|
+
entry: T,
|
|
76
|
+
createdAt = Date.now()
|
|
77
|
+
): TranscriptEntry {
|
|
78
|
+
return {
|
|
79
|
+
_id: crypto.randomUUID(),
|
|
80
|
+
createdAt,
|
|
81
|
+
...entry,
|
|
82
|
+
} as TranscriptEntry
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
function
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
85
|
+
function stringFromUnknown(value: unknown) {
|
|
86
|
+
if (typeof value === "string") return value
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(value, null, 2)
|
|
89
|
+
} catch {
|
|
90
|
+
return String(value)
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
export function normalizeClaudeStreamMessage(message: any): TranscriptEntry[] {
|
|
95
|
+
const debugRaw = JSON.stringify(message)
|
|
96
|
+
const messageId = typeof message.uuid === "string" ? message.uuid : undefined
|
|
97
|
+
|
|
98
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
99
|
+
return [
|
|
100
|
+
timestamped({
|
|
101
|
+
kind: "system_init",
|
|
102
|
+
messageId,
|
|
103
|
+
provider: "claude",
|
|
104
|
+
model: typeof message.model === "string" ? message.model : "unknown",
|
|
105
|
+
tools: Array.isArray(message.tools) ? message.tools : [],
|
|
106
|
+
agents: Array.isArray(message.agents) ? message.agents : [],
|
|
107
|
+
slashCommands: Array.isArray(message.slash_commands)
|
|
108
|
+
? message.slash_commands.filter((entry: string) => !entry.startsWith("._"))
|
|
109
|
+
: [],
|
|
110
|
+
mcpServers: Array.isArray(message.mcp_servers) ? message.mcp_servers : [],
|
|
111
|
+
debugRaw,
|
|
112
|
+
}),
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (message.type === "assistant" && Array.isArray(message.message?.content)) {
|
|
117
|
+
const entries: TranscriptEntry[] = []
|
|
118
|
+
for (const content of message.message.content) {
|
|
119
|
+
if (content.type === "text" && typeof content.text === "string") {
|
|
120
|
+
entries.push(timestamped({
|
|
121
|
+
kind: "assistant_text",
|
|
122
|
+
messageId,
|
|
123
|
+
text: content.text,
|
|
124
|
+
debugRaw,
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
if (content.type === "tool_use" && typeof content.name === "string" && typeof content.id === "string") {
|
|
128
|
+
entries.push(timestamped({
|
|
129
|
+
kind: "tool_call",
|
|
130
|
+
messageId,
|
|
131
|
+
tool: normalizeToolCall({
|
|
132
|
+
toolName: content.name,
|
|
133
|
+
toolId: content.id,
|
|
134
|
+
input: (content.input ?? {}) as Record<string, unknown>,
|
|
135
|
+
}),
|
|
136
|
+
debugRaw,
|
|
137
|
+
}))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return entries
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (message.type === "user" && Array.isArray(message.message?.content)) {
|
|
144
|
+
const entries: TranscriptEntry[] = []
|
|
145
|
+
for (const content of message.message.content) {
|
|
146
|
+
if (content.type === "tool_result" && typeof content.tool_use_id === "string") {
|
|
147
|
+
entries.push(timestamped({
|
|
148
|
+
kind: "tool_result",
|
|
149
|
+
messageId,
|
|
150
|
+
toolId: content.tool_use_id,
|
|
151
|
+
content: content.content,
|
|
152
|
+
isError: Boolean(content.is_error),
|
|
153
|
+
debugRaw,
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
if (message.message.role === "user" && typeof message.message.content === "string") {
|
|
157
|
+
entries.push(timestamped({
|
|
158
|
+
kind: "compact_summary",
|
|
159
|
+
messageId,
|
|
160
|
+
summary: message.message.content,
|
|
161
|
+
debugRaw,
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return entries
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (message.type === "result") {
|
|
169
|
+
if (message.subtype === "cancelled") {
|
|
170
|
+
return [timestamped({ kind: "interrupted", messageId, debugRaw })]
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
timestamped({
|
|
174
|
+
kind: "result",
|
|
175
|
+
messageId,
|
|
176
|
+
subtype: message.is_error ? "error" : "success",
|
|
177
|
+
isError: Boolean(message.is_error),
|
|
178
|
+
durationMs: typeof message.duration_ms === "number" ? message.duration_ms : 0,
|
|
179
|
+
result: typeof message.result === "string" ? message.result : stringFromUnknown(message.result),
|
|
180
|
+
costUsd: typeof message.total_cost_usd === "number" ? message.total_cost_usd : undefined,
|
|
181
|
+
debugRaw,
|
|
182
|
+
}),
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (message.type === "system" && message.subtype === "status" && typeof message.status === "string") {
|
|
187
|
+
return [timestamped({ kind: "status", messageId, status: message.status, debugRaw })]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
191
|
+
return [timestamped({ kind: "compact_boundary", messageId, debugRaw })]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (message.type === "system" && message.subtype === "context_cleared") {
|
|
195
|
+
return [timestamped({ kind: "context_cleared", messageId, debugRaw })]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
message.type === "user" &&
|
|
200
|
+
message.message?.role === "user" &&
|
|
201
|
+
typeof message.message.content === "string" &&
|
|
202
|
+
message.message.content.startsWith("This session is being continued")
|
|
203
|
+
) {
|
|
204
|
+
return [timestamped({ kind: "compact_summary", messageId, summary: message.message.content, debugRaw })]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return []
|
|
89
208
|
}
|
|
90
209
|
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
210
|
+
async function* createClaudeHarnessStream(q: Query): AsyncGenerator<HarnessEvent> {
|
|
211
|
+
for await (const sdkMessage of q as AsyncIterable<any>) {
|
|
212
|
+
const sessionToken = typeof sdkMessage.session_id === "string" ? sdkMessage.session_id : null
|
|
213
|
+
if (sessionToken) {
|
|
214
|
+
yield { type: "session_token", sessionToken }
|
|
215
|
+
}
|
|
216
|
+
for (const entry of normalizeClaudeStreamMessage(sdkMessage)) {
|
|
217
|
+
yield { type: "transcript", entry }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
94
220
|
}
|
|
95
221
|
|
|
96
|
-
function
|
|
222
|
+
async function startClaudeTurn(args: {
|
|
223
|
+
content: string
|
|
224
|
+
localPath: string
|
|
225
|
+
model: string
|
|
226
|
+
effort?: string
|
|
227
|
+
planMode: boolean
|
|
228
|
+
sessionToken: string | null
|
|
229
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
|
|
230
|
+
}): Promise<HarnessTurn> {
|
|
231
|
+
const canUseTool: CanUseTool = async (toolName, input, options) => {
|
|
232
|
+
if (toolName !== "AskUserQuestion" && toolName !== "ExitPlanMode") {
|
|
233
|
+
return {
|
|
234
|
+
behavior: "allow",
|
|
235
|
+
updatedInput: input,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tool = normalizeToolCall({
|
|
240
|
+
toolName,
|
|
241
|
+
toolId: options.toolUseID,
|
|
242
|
+
input: (input ?? {}) as Record<string, unknown>,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (tool.toolKind !== "ask_user_question" && tool.toolKind !== "exit_plan_mode") {
|
|
246
|
+
return {
|
|
247
|
+
behavior: "deny",
|
|
248
|
+
message: "Unsupported tool request",
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await args.onToolRequest({ tool })
|
|
253
|
+
|
|
254
|
+
if (tool.toolKind === "ask_user_question") {
|
|
255
|
+
const record = result && typeof result === "object" ? result as Record<string, unknown> : {}
|
|
256
|
+
return {
|
|
257
|
+
behavior: "allow",
|
|
258
|
+
updatedInput: {
|
|
259
|
+
...(tool.rawInput ?? {}),
|
|
260
|
+
questions: record.questions ?? tool.input.questions,
|
|
261
|
+
answers: record.answers ?? result,
|
|
262
|
+
},
|
|
263
|
+
} satisfies PermissionResult
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const record = result && typeof result === "object" ? result as Record<string, unknown> : {}
|
|
267
|
+
const confirmed = Boolean(record.confirmed)
|
|
268
|
+
if (confirmed) {
|
|
269
|
+
return {
|
|
270
|
+
behavior: "allow",
|
|
271
|
+
updatedInput: {
|
|
272
|
+
...(tool.rawInput ?? {}),
|
|
273
|
+
...record,
|
|
274
|
+
},
|
|
275
|
+
} satisfies PermissionResult
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
behavior: "deny",
|
|
280
|
+
message: typeof record.message === "string"
|
|
281
|
+
? `User wants to suggest edits to the plan: ${record.message}`
|
|
282
|
+
: "User wants to suggest edits to the plan before approving.",
|
|
283
|
+
} satisfies PermissionResult
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const q = query({
|
|
287
|
+
prompt: args.content,
|
|
288
|
+
options: {
|
|
289
|
+
cwd: args.localPath,
|
|
290
|
+
model: args.model,
|
|
291
|
+
effort: args.effort as "low" | "medium" | "high" | "max" | undefined,
|
|
292
|
+
resume: args.sessionToken ?? undefined,
|
|
293
|
+
permissionMode: args.planMode ? "plan" : "acceptEdits",
|
|
294
|
+
canUseTool,
|
|
295
|
+
tools: [...CLAUDE_TOOLSET],
|
|
296
|
+
settingSources: ["user", "project", "local"],
|
|
297
|
+
env: { ...process.env },
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
|
|
97
301
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
302
|
+
provider: "claude",
|
|
303
|
+
stream: createClaudeHarnessStream(q),
|
|
304
|
+
getAccountInfo: async () => {
|
|
305
|
+
try {
|
|
306
|
+
return await q.accountInfo()
|
|
307
|
+
} catch {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
interrupt: async () => {
|
|
312
|
+
await q.interrupt()
|
|
313
|
+
},
|
|
314
|
+
close: () => {
|
|
315
|
+
q.close()
|
|
316
|
+
},
|
|
102
317
|
}
|
|
103
318
|
}
|
|
104
319
|
|
|
105
320
|
export class AgentCoordinator {
|
|
106
321
|
private readonly store: EventStore
|
|
107
322
|
private readonly onStateChange: () => void
|
|
323
|
+
private readonly codexManager: CodexAppServerManager
|
|
108
324
|
readonly activeTurns = new Map<string, ActiveTurn>()
|
|
109
325
|
|
|
110
326
|
constructor(args: AgentCoordinatorArgs) {
|
|
111
327
|
this.store = args.store
|
|
112
328
|
this.onStateChange = args.onStateChange
|
|
329
|
+
this.codexManager = args.codexManager ?? new CodexAppServerManager()
|
|
113
330
|
}
|
|
114
331
|
|
|
115
332
|
getActiveStatuses() {
|
|
@@ -123,153 +340,197 @@ export class AgentCoordinator {
|
|
|
123
340
|
getPendingTool(chatId: string): PendingToolSnapshot | null {
|
|
124
341
|
const pending = this.activeTurns.get(chatId)?.pendingTool
|
|
125
342
|
if (!pending) return null
|
|
126
|
-
return { toolUseId: pending.toolUseId,
|
|
343
|
+
return { toolUseId: pending.toolUseId, toolKind: pending.tool.toolKind }
|
|
127
344
|
}
|
|
128
345
|
|
|
129
|
-
|
|
130
|
-
|
|
346
|
+
private resolveProvider(command: Extract<ClientCommand, { type: "chat.send" }>, currentProvider: AgentProvider | null) {
|
|
347
|
+
if (currentProvider) return currentProvider
|
|
348
|
+
return command.provider ?? "claude"
|
|
349
|
+
}
|
|
131
350
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
351
|
+
private getProviderSettings(provider: AgentProvider, command: Extract<ClientCommand, { type: "chat.send" }>) {
|
|
352
|
+
const catalog = getServerProviderCatalog(provider)
|
|
353
|
+
if (provider === "claude") {
|
|
354
|
+
const modelOptions = normalizeClaudeModelOptions(command.modelOptions, command.effort)
|
|
355
|
+
return {
|
|
356
|
+
model: normalizeServerModel(provider, command.model),
|
|
357
|
+
effort: modelOptions.reasoningEffort,
|
|
358
|
+
serviceTier: undefined,
|
|
359
|
+
planMode: catalog.supportsPlanMode ? Boolean(command.planMode) : false,
|
|
135
360
|
}
|
|
136
|
-
const created = await this.store.createChat(command.projectId)
|
|
137
|
-
chatId = created.id
|
|
138
361
|
}
|
|
139
362
|
|
|
140
|
-
const
|
|
141
|
-
|
|
363
|
+
const modelOptions = normalizeCodexModelOptions(command.modelOptions, command.effort)
|
|
364
|
+
return {
|
|
365
|
+
model: normalizeServerModel(provider, command.model),
|
|
366
|
+
effort: modelOptions.reasoningEffort,
|
|
367
|
+
serviceTier: codexServiceTierFromModelOptions(modelOptions),
|
|
368
|
+
planMode: catalog.supportsPlanMode ? Boolean(command.planMode) : false,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async startTurnForChat(args: {
|
|
373
|
+
chatId: string
|
|
374
|
+
provider: AgentProvider
|
|
375
|
+
content: string
|
|
376
|
+
model: string
|
|
377
|
+
effort?: string
|
|
378
|
+
serviceTier?: "fast"
|
|
379
|
+
planMode: boolean
|
|
380
|
+
appendUserPrompt: boolean
|
|
381
|
+
}) {
|
|
382
|
+
const chat = this.store.requireChat(args.chatId)
|
|
383
|
+
if (this.activeTurns.has(args.chatId)) {
|
|
142
384
|
throw new Error("Chat is already running")
|
|
143
385
|
}
|
|
144
386
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Immediate placeholder: truncated first message
|
|
148
|
-
await this.store.renameChat(chatId, deriveChatTitle(command.content))
|
|
149
|
-
|
|
150
|
-
// Fire-and-forget: generate a better title with Haiku in parallel
|
|
151
|
-
void generateTitleForChat(command.content)
|
|
152
|
-
.then(async (title) => {
|
|
153
|
-
if (title) {
|
|
154
|
-
await this.store.renameChat(chatId!, title)
|
|
155
|
-
this.onStateChange()
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
.catch(() => undefined)
|
|
387
|
+
if (!chat.provider) {
|
|
388
|
+
await this.store.setChatProvider(args.chatId, args.provider)
|
|
159
389
|
}
|
|
390
|
+
await this.store.setPlanMode(args.chatId, args.planMode)
|
|
160
391
|
|
|
161
|
-
|
|
162
|
-
|
|
392
|
+
const existingMessages = this.store.getMessages(args.chatId)
|
|
393
|
+
if (args.appendUserPrompt && chat.title === "New Chat" && existingMessages.length === 0) {
|
|
394
|
+
await this.store.renameChat(args.chatId, deriveChatTitle(args.content))
|
|
395
|
+
}
|
|
163
396
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
updatedInput: input,
|
|
169
|
-
}
|
|
170
|
-
}
|
|
397
|
+
if (args.appendUserPrompt) {
|
|
398
|
+
await this.store.appendMessage(args.chatId, timestamped({ kind: "user_prompt", content: args.content }, Date.now()))
|
|
399
|
+
}
|
|
400
|
+
await this.store.recordTurnStarted(args.chatId)
|
|
171
401
|
|
|
172
|
-
|
|
402
|
+
const project = this.store.getProject(chat.projectId)
|
|
403
|
+
if (!project) {
|
|
404
|
+
throw new Error("Project not found")
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const onToolRequest = async (request: HarnessToolRequest): Promise<unknown> => {
|
|
408
|
+
const active = this.activeTurns.get(args.chatId)
|
|
173
409
|
if (!active) {
|
|
174
|
-
|
|
175
|
-
behavior: "deny",
|
|
176
|
-
message: "Chat turn ended unexpectedly",
|
|
177
|
-
}
|
|
410
|
+
throw new Error("Chat turn ended unexpectedly")
|
|
178
411
|
}
|
|
179
412
|
|
|
180
413
|
active.status = "waiting_for_user"
|
|
181
414
|
this.onStateChange()
|
|
182
415
|
|
|
183
|
-
return await new Promise<
|
|
416
|
+
return await new Promise<unknown>((resolve) => {
|
|
184
417
|
active.pendingTool = {
|
|
185
|
-
toolUseId:
|
|
186
|
-
|
|
187
|
-
input,
|
|
418
|
+
toolUseId: request.tool.toolId,
|
|
419
|
+
tool: request.tool,
|
|
188
420
|
resolve,
|
|
189
421
|
}
|
|
190
422
|
})
|
|
191
423
|
}
|
|
192
424
|
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
425
|
+
let turn: HarnessTurn
|
|
426
|
+
if (args.provider === "claude") {
|
|
427
|
+
turn = await startClaudeTurn({
|
|
428
|
+
content: args.content,
|
|
429
|
+
localPath: project.localPath,
|
|
430
|
+
model: args.model,
|
|
431
|
+
effort: args.effort,
|
|
432
|
+
planMode: args.planMode,
|
|
433
|
+
sessionToken: chat.sessionToken,
|
|
434
|
+
onToolRequest,
|
|
435
|
+
})
|
|
436
|
+
} else {
|
|
437
|
+
await this.codexManager.startSession({
|
|
438
|
+
chatId: args.chatId,
|
|
205
439
|
cwd: project.localPath,
|
|
206
|
-
model:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
440
|
+
model: args.model,
|
|
441
|
+
serviceTier: args.serviceTier,
|
|
442
|
+
sessionToken: chat.sessionToken,
|
|
443
|
+
})
|
|
444
|
+
turn = await this.codexManager.startTurn({
|
|
445
|
+
chatId: args.chatId,
|
|
446
|
+
content: args.content,
|
|
447
|
+
model: args.model,
|
|
448
|
+
effort: args.effort as any,
|
|
449
|
+
serviceTier: args.serviceTier,
|
|
450
|
+
planMode: args.planMode,
|
|
451
|
+
onToolRequest,
|
|
452
|
+
})
|
|
453
|
+
}
|
|
219
454
|
|
|
220
455
|
const active: ActiveTurn = {
|
|
221
|
-
chatId,
|
|
222
|
-
|
|
456
|
+
chatId: args.chatId,
|
|
457
|
+
provider: args.provider,
|
|
458
|
+
turn,
|
|
459
|
+
model: args.model,
|
|
460
|
+
effort: args.effort,
|
|
461
|
+
serviceTier: args.serviceTier,
|
|
462
|
+
planMode: args.planMode,
|
|
223
463
|
status: "starting",
|
|
224
464
|
pendingTool: null,
|
|
465
|
+
postToolFollowUp: null,
|
|
225
466
|
hasFinalResult: false,
|
|
226
467
|
cancelRequested: false,
|
|
227
468
|
cancelRecorded: false,
|
|
228
469
|
}
|
|
229
|
-
this.activeTurns.set(chatId, active)
|
|
470
|
+
this.activeTurns.set(args.chatId, active)
|
|
230
471
|
this.onStateChange()
|
|
231
472
|
|
|
232
|
-
|
|
233
|
-
.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
473
|
+
if (turn.getAccountInfo) {
|
|
474
|
+
void turn.getAccountInfo()
|
|
475
|
+
.then(async (accountInfo) => {
|
|
476
|
+
if (!accountInfo) return
|
|
477
|
+
await this.store.appendMessage(args.chatId, timestamped({ kind: "account_info", accountInfo }))
|
|
478
|
+
this.onStateChange()
|
|
479
|
+
})
|
|
480
|
+
.catch(() => undefined)
|
|
481
|
+
}
|
|
241
482
|
|
|
242
483
|
void this.runTurn(active)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async send(command: Extract<ClientCommand, { type: "chat.send" }>) {
|
|
487
|
+
let chatId = command.chatId
|
|
488
|
+
|
|
489
|
+
if (!chatId) {
|
|
490
|
+
if (!command.projectId) {
|
|
491
|
+
throw new Error("Missing projectId for new chat")
|
|
492
|
+
}
|
|
493
|
+
const created = await this.store.createChat(command.projectId)
|
|
494
|
+
chatId = created.id
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const chat = this.store.requireChat(chatId)
|
|
498
|
+
const provider = this.resolveProvider(command, chat.provider)
|
|
499
|
+
const settings = this.getProviderSettings(provider, command)
|
|
500
|
+
await this.startTurnForChat({
|
|
501
|
+
chatId,
|
|
502
|
+
provider,
|
|
503
|
+
content: command.content,
|
|
504
|
+
model: settings.model,
|
|
505
|
+
effort: settings.effort,
|
|
506
|
+
serviceTier: settings.serviceTier,
|
|
507
|
+
planMode: settings.planMode,
|
|
508
|
+
appendUserPrompt: true,
|
|
509
|
+
})
|
|
243
510
|
|
|
244
511
|
return { chatId }
|
|
245
512
|
}
|
|
246
513
|
|
|
247
514
|
private async runTurn(active: ActiveTurn) {
|
|
248
515
|
try {
|
|
249
|
-
for await (const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const sessionId = "session_id" in sdkMessage && typeof sdkMessage.session_id === "string"
|
|
256
|
-
? sdkMessage.session_id
|
|
257
|
-
: null
|
|
258
|
-
if (sessionId) {
|
|
259
|
-
await this.store.setResumeSession(active.chatId, sessionId)
|
|
516
|
+
for await (const event of active.turn.stream) {
|
|
517
|
+
if (event.type === "session_token" && event.sessionToken) {
|
|
518
|
+
await this.store.setSessionToken(active.chatId, event.sessionToken)
|
|
519
|
+
this.onStateChange()
|
|
520
|
+
continue
|
|
260
521
|
}
|
|
261
522
|
|
|
262
|
-
if (
|
|
523
|
+
if (!event.entry) continue
|
|
524
|
+
await this.store.appendMessage(active.chatId, event.entry)
|
|
525
|
+
|
|
526
|
+
if (event.entry.kind === "system_init") {
|
|
263
527
|
active.status = "running"
|
|
264
528
|
}
|
|
265
529
|
|
|
266
|
-
if (
|
|
530
|
+
if (event.entry.kind === "result") {
|
|
267
531
|
active.hasFinalResult = true
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
? sdkMessage.errors.join("\n")
|
|
271
|
-
: "Turn failed"
|
|
272
|
-
await this.store.recordTurnFailed(active.chatId, errorText)
|
|
532
|
+
if (event.entry.isError) {
|
|
533
|
+
await this.store.recordTurnFailed(active.chatId, event.entry.result || "Turn failed")
|
|
273
534
|
} else if (!active.cancelRequested) {
|
|
274
535
|
await this.store.recordTurnFinished(active.chatId)
|
|
275
536
|
}
|
|
@@ -280,16 +541,54 @@ export class AgentCoordinator {
|
|
|
280
541
|
} catch (error) {
|
|
281
542
|
if (!active.cancelRequested) {
|
|
282
543
|
const message = error instanceof Error ? error.message : String(error)
|
|
283
|
-
await this.store.appendMessage(
|
|
544
|
+
await this.store.appendMessage(
|
|
545
|
+
active.chatId,
|
|
546
|
+
timestamped({
|
|
547
|
+
kind: "result",
|
|
548
|
+
subtype: "error",
|
|
549
|
+
isError: true,
|
|
550
|
+
durationMs: 0,
|
|
551
|
+
result: message,
|
|
552
|
+
})
|
|
553
|
+
)
|
|
284
554
|
await this.store.recordTurnFailed(active.chatId, message)
|
|
285
555
|
}
|
|
286
556
|
} finally {
|
|
287
557
|
if (active.cancelRequested && !active.cancelRecorded) {
|
|
288
558
|
await this.store.recordTurnCancelled(active.chatId)
|
|
289
559
|
}
|
|
290
|
-
active.
|
|
560
|
+
active.turn.close()
|
|
291
561
|
this.activeTurns.delete(active.chatId)
|
|
292
562
|
this.onStateChange()
|
|
563
|
+
|
|
564
|
+
if (active.postToolFollowUp && !active.cancelRequested) {
|
|
565
|
+
try {
|
|
566
|
+
await this.startTurnForChat({
|
|
567
|
+
chatId: active.chatId,
|
|
568
|
+
provider: active.provider,
|
|
569
|
+
content: active.postToolFollowUp.content,
|
|
570
|
+
model: active.model,
|
|
571
|
+
effort: active.effort,
|
|
572
|
+
serviceTier: active.serviceTier,
|
|
573
|
+
planMode: active.postToolFollowUp.planMode,
|
|
574
|
+
appendUserPrompt: false,
|
|
575
|
+
})
|
|
576
|
+
} catch (error) {
|
|
577
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
578
|
+
await this.store.appendMessage(
|
|
579
|
+
active.chatId,
|
|
580
|
+
timestamped({
|
|
581
|
+
kind: "result",
|
|
582
|
+
subtype: "error",
|
|
583
|
+
isError: true,
|
|
584
|
+
durationMs: 0,
|
|
585
|
+
result: message,
|
|
586
|
+
})
|
|
587
|
+
)
|
|
588
|
+
await this.store.recordTurnFailed(active.chatId, message)
|
|
589
|
+
this.onStateChange()
|
|
590
|
+
}
|
|
591
|
+
}
|
|
293
592
|
}
|
|
294
593
|
}
|
|
295
594
|
|
|
@@ -300,15 +599,15 @@ export class AgentCoordinator {
|
|
|
300
599
|
active.cancelRequested = true
|
|
301
600
|
active.pendingTool = null
|
|
302
601
|
|
|
303
|
-
await this.store.appendMessage(chatId,
|
|
602
|
+
await this.store.appendMessage(chatId, timestamped({ kind: "interrupted" }))
|
|
304
603
|
await this.store.recordTurnCancelled(chatId)
|
|
305
604
|
active.cancelRecorded = true
|
|
306
605
|
active.hasFinalResult = true
|
|
307
606
|
|
|
308
607
|
try {
|
|
309
|
-
await active.
|
|
608
|
+
await active.turn.interrupt()
|
|
310
609
|
} catch {
|
|
311
|
-
active.
|
|
610
|
+
active.turn.close()
|
|
312
611
|
}
|
|
313
612
|
|
|
314
613
|
this.activeTurns.delete(chatId)
|
|
@@ -328,56 +627,46 @@ export class AgentCoordinator {
|
|
|
328
627
|
|
|
329
628
|
await this.store.appendMessage(
|
|
330
629
|
command.chatId,
|
|
331
|
-
|
|
630
|
+
timestamped({
|
|
631
|
+
kind: "tool_result",
|
|
632
|
+
toolId: command.toolUseId,
|
|
633
|
+
content: command.result,
|
|
634
|
+
})
|
|
332
635
|
)
|
|
333
636
|
|
|
334
637
|
active.pendingTool = null
|
|
335
638
|
active.status = "running"
|
|
336
639
|
|
|
337
|
-
if (pending.
|
|
338
|
-
const result = command.result
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
this.onStateChange()
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const result = (command.result ?? {}) as {
|
|
352
|
-
confirmed?: boolean
|
|
353
|
-
clearContext?: boolean
|
|
354
|
-
message?: string
|
|
355
|
-
}
|
|
640
|
+
if (pending.tool.toolKind === "exit_plan_mode") {
|
|
641
|
+
const result = (command.result ?? {}) as {
|
|
642
|
+
confirmed?: boolean
|
|
643
|
+
clearContext?: boolean
|
|
644
|
+
message?: string
|
|
645
|
+
}
|
|
646
|
+
if (result.confirmed && result.clearContext) {
|
|
647
|
+
await this.store.setSessionToken(command.chatId, null)
|
|
648
|
+
await this.store.appendMessage(command.chatId, timestamped({ kind: "context_cleared" }))
|
|
649
|
+
}
|
|
356
650
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
651
|
+
if (active.provider === "codex") {
|
|
652
|
+
active.postToolFollowUp = result.confirmed
|
|
653
|
+
? {
|
|
654
|
+
content: result.message
|
|
655
|
+
? `Proceed with the approved plan. Additional guidance: ${result.message}`
|
|
656
|
+
: "Proceed with the approved plan.",
|
|
657
|
+
planMode: false,
|
|
658
|
+
}
|
|
659
|
+
: {
|
|
660
|
+
content: result.message
|
|
661
|
+
? `Revise the plan using this feedback: ${result.message}`
|
|
662
|
+
: "Revise the plan using this feedback.",
|
|
663
|
+
planMode: true,
|
|
664
|
+
}
|
|
364
665
|
}
|
|
365
|
-
pending.resolve({
|
|
366
|
-
behavior: "allow",
|
|
367
|
-
updatedInput: {
|
|
368
|
-
...(pending.input ?? {}),
|
|
369
|
-
...result,
|
|
370
|
-
},
|
|
371
|
-
})
|
|
372
|
-
} else {
|
|
373
|
-
pending.resolve({
|
|
374
|
-
behavior: "deny",
|
|
375
|
-
message: result.message
|
|
376
|
-
? `User wants to suggest edits to the plan: ${result.message}`
|
|
377
|
-
: "User wants to suggest edits to the plan before approving.",
|
|
378
|
-
})
|
|
379
666
|
}
|
|
380
667
|
|
|
668
|
+
pending.resolve(command.result)
|
|
669
|
+
|
|
381
670
|
this.onStateChange()
|
|
382
671
|
}
|
|
383
672
|
}
|