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