@frebreco/canvas 0.6.0-next.2 → 0.6.0-next.4
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/package.json +1 -1
- package/src/server/agents.ts +34 -8
- package/src/server/todo-plan.ts +30 -0
- package/src/shared/protocol.ts +25 -0
package/package.json
CHANGED
package/src/server/agents.ts
CHANGED
|
@@ -45,6 +45,8 @@ import type {
|
|
|
45
45
|
SessionMeta,
|
|
46
46
|
SessionSnapshot,
|
|
47
47
|
} from "../shared/protocol";
|
|
48
|
+
import { PLAN_EVENT } from "../shared/protocol";
|
|
49
|
+
import { todoPlan } from "./todo-plan";
|
|
48
50
|
import { fromAcp, pendingChanges, settingsOf } from "./agent-config";
|
|
49
51
|
import { SKILLS_DIR } from "./skills";
|
|
50
52
|
import { trimEvent } from "./trim-event";
|
|
@@ -187,12 +189,18 @@ export class AgentManager {
|
|
|
187
189
|
const session = this.sessions.get(sessionId);
|
|
188
190
|
if (!session) throw new Error(`no session ${sessionId}`);
|
|
189
191
|
const live = await this.connect(session);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
try {
|
|
193
|
+
const response = await live.acp.setSessionConfigOption({
|
|
194
|
+
sessionId: live.sessionId,
|
|
195
|
+
configId,
|
|
196
|
+
...(typeof value === "boolean" ? { type: "boolean" as const, value } : { value }),
|
|
197
|
+
});
|
|
198
|
+
// Sent even unchanged: it is the answer the browser waits for.
|
|
199
|
+
this.setOptions(session, response.configOptions, true);
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
if (session.options) this.options.onOptions(session.meta.id, session.options);
|
|
202
|
+
throw cause;
|
|
203
|
+
}
|
|
196
204
|
this.touch(session);
|
|
197
205
|
}
|
|
198
206
|
|
|
@@ -264,6 +272,7 @@ export class AgentManager {
|
|
|
264
272
|
labels: {
|
|
265
273
|
sessionIdEvent: `${session.meta.agent}.session-id`,
|
|
266
274
|
contentEvent: `${session.meta.agent}.message-content`,
|
|
275
|
+
planEvent: PLAN_EVENT,
|
|
267
276
|
},
|
|
268
277
|
});
|
|
269
278
|
for await (const chunk of chunks) {
|
|
@@ -280,6 +289,7 @@ export class AgentManager {
|
|
|
280
289
|
this.append(session, {
|
|
281
290
|
kind: "turn-end",
|
|
282
291
|
turnId,
|
|
292
|
+
at: Date.now(),
|
|
283
293
|
...(error !== undefined && !cancelled && { error }),
|
|
284
294
|
...(cancelled && { cancelled: true }),
|
|
285
295
|
});
|
|
@@ -424,7 +434,19 @@ export class AgentManager {
|
|
|
424
434
|
private onUpdate(session: Session, update: SessionUpdate): void {
|
|
425
435
|
if (update.sessionUpdate === "config_option_update")
|
|
426
436
|
return this.setOptions(session, update.configOptions);
|
|
437
|
+
if (update.sessionUpdate === "usage_update") {
|
|
438
|
+
const { used, size, cost } = update;
|
|
439
|
+
const usage = {
|
|
440
|
+
used,
|
|
441
|
+
size,
|
|
442
|
+
...(cost && { cost: { amount: cost.amount, currency: cost.currency } }),
|
|
443
|
+
};
|
|
444
|
+
session.meta = { ...session.meta, usage };
|
|
445
|
+
return this.options.onMeta(session.meta);
|
|
446
|
+
}
|
|
427
447
|
session.turn?.queue.push({ kind: "update", update: update as AcpSessionUpdate });
|
|
448
|
+
const plan = todoPlan(update);
|
|
449
|
+
if (plan) session.turn?.queue.push({ kind: "update", update: plan as AcpSessionUpdate });
|
|
428
450
|
}
|
|
429
451
|
|
|
430
452
|
/** Park the agent's ask until the host answers it in the browser. */
|
|
@@ -457,9 +479,13 @@ export class AgentManager {
|
|
|
457
479
|
};
|
|
458
480
|
}
|
|
459
481
|
|
|
460
|
-
private setOptions(
|
|
482
|
+
private setOptions(
|
|
483
|
+
session: Session,
|
|
484
|
+
raw: ReadonlyArray<SessionConfigOption>,
|
|
485
|
+
always = false,
|
|
486
|
+
): void {
|
|
461
487
|
const options = fromAcp(raw);
|
|
462
|
-
if (JSON.stringify(options) !== JSON.stringify(session.options)) {
|
|
488
|
+
if (always || JSON.stringify(options) !== JSON.stringify(session.options)) {
|
|
463
489
|
session.options = options;
|
|
464
490
|
this.options.onOptions(session.meta.id, options);
|
|
465
491
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents that keep a todo list without sending ACP `plan` updates (opencode's
|
|
3
|
+
* `todowrite`) still have a plan: the list, in the tool call's input. This
|
|
4
|
+
* reads it as the plan update the agent would have sent, so a thread knows
|
|
5
|
+
* one kind of plan only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PlanEntry, SessionUpdate } from "@agentclientprotocol/sdk";
|
|
9
|
+
|
|
10
|
+
const STATUSES = new Set(["pending", "in_progress", "completed"]);
|
|
11
|
+
|
|
12
|
+
export function todoPlan(update: SessionUpdate): SessionUpdate | null {
|
|
13
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update")
|
|
14
|
+
return null;
|
|
15
|
+
const input = update.rawInput as { todos?: unknown } | null | undefined;
|
|
16
|
+
if (!Array.isArray(input?.todos)) return null;
|
|
17
|
+
const entries = input.todos.flatMap((todo: unknown): PlanEntry[] => {
|
|
18
|
+
const { content, status, priority } = (todo ?? {}) as Record<string, unknown>;
|
|
19
|
+
if (typeof content !== "string" || typeof status !== "string" || !STATUSES.has(status))
|
|
20
|
+
return [];
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
content,
|
|
24
|
+
status: status as PlanEntry["status"],
|
|
25
|
+
priority: priority === "high" || priority === "low" ? priority : "medium",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
});
|
|
29
|
+
return entries.length ? { sessionUpdate: "plan", entries } : null;
|
|
30
|
+
}
|
package/src/shared/protocol.ts
CHANGED
|
@@ -37,6 +37,17 @@ export interface SessionMeta {
|
|
|
37
37
|
* shown while the agent is not connected, and re-applied when it reconnects.
|
|
38
38
|
*/
|
|
39
39
|
readonly settings?: ReadonlyArray<AgentSetting>;
|
|
40
|
+
/** How full the agent's context is, as it last said (ACP `usage_update`); not every agent does. */
|
|
41
|
+
readonly usage?: ContextUsage;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ContextUsage {
|
|
45
|
+
/** Tokens in the context now. */
|
|
46
|
+
readonly used: number;
|
|
47
|
+
/** The context window, in tokens. */
|
|
48
|
+
readonly size: number;
|
|
49
|
+
/** The session's cost so far, if the agent knows it. */
|
|
50
|
+
readonly cost?: { readonly amount: number; readonly currency: string };
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export type AgentConfigValue = string | boolean;
|
|
@@ -115,10 +126,24 @@ export type AgentEvent =
|
|
|
115
126
|
| {
|
|
116
127
|
readonly kind: "turn-end";
|
|
117
128
|
readonly turnId: string;
|
|
129
|
+
/** When it ended (absent in logs from before it was kept). */
|
|
130
|
+
readonly at?: number;
|
|
118
131
|
readonly error?: string;
|
|
119
132
|
readonly cancelled?: boolean;
|
|
120
133
|
};
|
|
121
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The name of the AG-UI `CUSTOM` chunk that carries the agent's plan (an ACP
|
|
137
|
+
* `plan` update): `{ entries: PlanEntry[] }`, the whole plan each time.
|
|
138
|
+
*/
|
|
139
|
+
export const PLAN_EVENT = "plan";
|
|
140
|
+
|
|
141
|
+
export interface PlanEntry {
|
|
142
|
+
readonly content: string;
|
|
143
|
+
readonly status: "pending" | "in_progress" | "completed";
|
|
144
|
+
readonly priority?: "high" | "medium" | "low";
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
export interface SessionSnapshot {
|
|
123
148
|
readonly meta: SessionMeta;
|
|
124
149
|
readonly events: ReadonlyArray<AgentEvent>;
|