@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const { createAgentEventStream } = await jiti.import("./agent-event-stream.ts");
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
function deferred() {
|
|
10
|
+
let resolve;
|
|
11
|
+
let reject;
|
|
12
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
13
|
+
resolve = resolvePromise;
|
|
14
|
+
reject = rejectPromise;
|
|
15
|
+
});
|
|
16
|
+
return { promise, resolve, reject };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readWithin(reader, timeoutMs = 1_000) {
|
|
20
|
+
let timeout;
|
|
21
|
+
try {
|
|
22
|
+
return await Promise.race([
|
|
23
|
+
reader.read(),
|
|
24
|
+
new Promise((_, reject) => {
|
|
25
|
+
timeout = setTimeout(() => reject(new Error("Timed out reading SSE chunk")), timeoutMs);
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decodeData(chunk) {
|
|
34
|
+
const text = decoder.decode(chunk.value);
|
|
35
|
+
assert.match(text, /^data: /);
|
|
36
|
+
return JSON.parse(text.slice("data: ".length));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("opens the transport before a slow session is ready and snapshots after subscribing", async () => {
|
|
40
|
+
const startup = deferred();
|
|
41
|
+
const abortController = new AbortController();
|
|
42
|
+
const stream = createAgentEventStream(
|
|
43
|
+
new Request("http://localhost/events", { signal: abortController.signal }),
|
|
44
|
+
"session-id",
|
|
45
|
+
startup.promise,
|
|
46
|
+
);
|
|
47
|
+
const reader = stream.getReader();
|
|
48
|
+
|
|
49
|
+
const transport = await readWithin(reader);
|
|
50
|
+
assert.equal(decoder.decode(transport.value), ":\n\n");
|
|
51
|
+
|
|
52
|
+
const snapshot = { role: "assistant", content: [{ type: "text", text: "Hello" }] };
|
|
53
|
+
let listener;
|
|
54
|
+
let subscribeCount = 0;
|
|
55
|
+
let unsubscribeCount = 0;
|
|
56
|
+
startup.resolve({
|
|
57
|
+
isStreaming: true,
|
|
58
|
+
streamingMessage: snapshot,
|
|
59
|
+
onEvent(nextListener) {
|
|
60
|
+
subscribeCount += 1;
|
|
61
|
+
listener = nextListener;
|
|
62
|
+
nextListener({
|
|
63
|
+
type: "message_update",
|
|
64
|
+
message: snapshot,
|
|
65
|
+
assistantMessageEvent: { type: "text_delta", delta: "ignored" },
|
|
66
|
+
});
|
|
67
|
+
nextListener({ type: "agent_start" });
|
|
68
|
+
return () => { unsubscribeCount += 1; };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const connected = decodeData(await readWithin(reader));
|
|
73
|
+
const messageStart = decodeData(await readWithin(reader));
|
|
74
|
+
const replayedEvent = decodeData(await readWithin(reader));
|
|
75
|
+
assert.equal(subscribeCount, 1);
|
|
76
|
+
assert.deepEqual(connected, {
|
|
77
|
+
type: "connected",
|
|
78
|
+
sessionId: "session-id",
|
|
79
|
+
isStreaming: true,
|
|
80
|
+
});
|
|
81
|
+
assert.deepEqual(messageStart, { type: "agent_start" });
|
|
82
|
+
assert.deepEqual(replayedEvent, { type: "message_start", message: snapshot });
|
|
83
|
+
|
|
84
|
+
listener({
|
|
85
|
+
type: "message_update",
|
|
86
|
+
message: { ...snapshot },
|
|
87
|
+
assistantMessageEvent: {
|
|
88
|
+
type: "text_delta",
|
|
89
|
+
delta: "!",
|
|
90
|
+
partial: { ...snapshot },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
assert.deepEqual(decodeData(await readWithin(reader)), {
|
|
94
|
+
type: "message_update",
|
|
95
|
+
assistantMessageEvent: { type: "text_delta", delta: "!" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
abortController.abort();
|
|
99
|
+
assert.equal(unsubscribeCount, 1);
|
|
100
|
+
assert.equal((await readWithin(reader)).done, true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("reports a startup failure in-band after opening the transport", async () => {
|
|
104
|
+
const stream = createAgentEventStream(
|
|
105
|
+
new Request("http://localhost/events"),
|
|
106
|
+
"session-id",
|
|
107
|
+
Promise.reject(new Error("broken config")),
|
|
108
|
+
);
|
|
109
|
+
const reader = stream.getReader();
|
|
110
|
+
|
|
111
|
+
assert.equal(decoder.decode((await readWithin(reader)).value), ":\n\n");
|
|
112
|
+
assert.deepEqual(decodeData(await readWithin(reader)), {
|
|
113
|
+
type: "startup_error",
|
|
114
|
+
errorMessage: "Failed to start agent: broken config",
|
|
115
|
+
});
|
|
116
|
+
assert.equal((await readWithin(reader)).done, true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("does not subscribe when the client cancels during startup", async () => {
|
|
120
|
+
const startup = deferred();
|
|
121
|
+
let subscribeCount = 0;
|
|
122
|
+
const stream = createAgentEventStream(
|
|
123
|
+
new Request("http://localhost/events"),
|
|
124
|
+
"session-id",
|
|
125
|
+
startup.promise,
|
|
126
|
+
);
|
|
127
|
+
const reader = stream.getReader();
|
|
128
|
+
|
|
129
|
+
await readWithin(reader);
|
|
130
|
+
await reader.cancel();
|
|
131
|
+
startup.resolve({
|
|
132
|
+
isStreaming: false,
|
|
133
|
+
streamingMessage: undefined,
|
|
134
|
+
onEvent() {
|
|
135
|
+
subscribeCount += 1;
|
|
136
|
+
return () => {};
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
140
|
+
|
|
141
|
+
assert.equal(subscribeCount, 0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("closes an already-aborted request and handles a later startup rejection", async () => {
|
|
145
|
+
const abortController = new AbortController();
|
|
146
|
+
abortController.abort();
|
|
147
|
+
const stream = createAgentEventStream(
|
|
148
|
+
new Request("http://localhost/events", { signal: abortController.signal }),
|
|
149
|
+
"session-id",
|
|
150
|
+
Promise.reject(new Error("startup failed after disconnect")),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
assert.equal((await readWithin(stream.getReader())).done, true);
|
|
154
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
155
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isEventIncludedInSnapshot,
|
|
3
|
+
toClientAgentEvent,
|
|
4
|
+
type AgentEventLike,
|
|
5
|
+
} from "./agent-event-wire";
|
|
6
|
+
|
|
7
|
+
export interface AgentEventStreamSession {
|
|
8
|
+
readonly isStreaming: boolean;
|
|
9
|
+
readonly streamingMessage: unknown;
|
|
10
|
+
onEvent(listener: (event: AgentEventLike) => void): () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
function errorMessage(error: unknown): string {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Open the SSE transport immediately, then publish the session snapshot only
|
|
21
|
+
* after the agent is ready and its event listener has been installed.
|
|
22
|
+
*/
|
|
23
|
+
export function createAgentEventStream(
|
|
24
|
+
req: Request,
|
|
25
|
+
sessionId: string,
|
|
26
|
+
sessionPromise: Promise<AgentEventStreamSession>,
|
|
27
|
+
): ReadableStream<Uint8Array> {
|
|
28
|
+
let cancelStream: (closeController: boolean) => void = () => {};
|
|
29
|
+
|
|
30
|
+
return new ReadableStream<Uint8Array>({
|
|
31
|
+
start(controller) {
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
let closed = false;
|
|
34
|
+
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
35
|
+
let unsubscribe: (() => void) | null = null;
|
|
36
|
+
let abortHandler: (() => void) | null = null;
|
|
37
|
+
|
|
38
|
+
const cleanup = (closeController: boolean) => {
|
|
39
|
+
if (closed) return;
|
|
40
|
+
closed = true;
|
|
41
|
+
if (heartbeat !== null) clearInterval(heartbeat);
|
|
42
|
+
unsubscribe?.();
|
|
43
|
+
unsubscribe = null;
|
|
44
|
+
if (abortHandler) req.signal.removeEventListener("abort", abortHandler);
|
|
45
|
+
if (closeController) {
|
|
46
|
+
try { controller.close(); } catch { /* stream already closed */ }
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
cancelStream = cleanup;
|
|
50
|
+
|
|
51
|
+
const enqueueText = (text: string) => {
|
|
52
|
+
if (closed) return;
|
|
53
|
+
try {
|
|
54
|
+
controller.enqueue(encoder.encode(text));
|
|
55
|
+
} catch {
|
|
56
|
+
cleanup(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const encode = (data: unknown) => {
|
|
60
|
+
enqueueText(`data: ${JSON.stringify(data)}\n\n`);
|
|
61
|
+
};
|
|
62
|
+
const forwardEvent = (event: AgentEventLike, snapshot: unknown) => {
|
|
63
|
+
if (isEventIncludedInSnapshot(event, snapshot)) return;
|
|
64
|
+
const clientEvent = toClientAgentEvent(event);
|
|
65
|
+
if (clientEvent) encode(clientEvent);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const publishSession = async () => {
|
|
69
|
+
try {
|
|
70
|
+
const session = await sessionPromise;
|
|
71
|
+
if (closed) return;
|
|
72
|
+
|
|
73
|
+
const bufferedEvents: AgentEventLike[] = [];
|
|
74
|
+
let snapshotPublished = false;
|
|
75
|
+
const handleEvent = (event: AgentEventLike) => {
|
|
76
|
+
if (!snapshotPublished) {
|
|
77
|
+
bufferedEvents.push(event);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
forwardEvent(event, snapshot);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const stopListening = session.onEvent(handleEvent);
|
|
84
|
+
if (closed) {
|
|
85
|
+
stopListening();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
unsubscribe = stopListening;
|
|
89
|
+
|
|
90
|
+
const snapshot = session.streamingMessage;
|
|
91
|
+
encode({
|
|
92
|
+
type: "connected",
|
|
93
|
+
sessionId,
|
|
94
|
+
isStreaming: session.isStreaming,
|
|
95
|
+
});
|
|
96
|
+
for (const event of bufferedEvents) forwardEvent(event, snapshot);
|
|
97
|
+
if (snapshot !== undefined && snapshot !== null) {
|
|
98
|
+
encode({ type: "message_start", message: snapshot });
|
|
99
|
+
}
|
|
100
|
+
snapshotPublished = true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (closed) return;
|
|
103
|
+
encode({
|
|
104
|
+
type: "startup_error",
|
|
105
|
+
errorMessage: `Failed to start agent: ${errorMessage(error)}`,
|
|
106
|
+
});
|
|
107
|
+
cleanup(true);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Attach the rejection handler before checking the request signal. The
|
|
112
|
+
// route may already have started a shared cold-start promise.
|
|
113
|
+
void publishSession();
|
|
114
|
+
|
|
115
|
+
abortHandler = () => cleanup(true);
|
|
116
|
+
if (req.signal.aborted) {
|
|
117
|
+
cleanup(true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
req.signal.addEventListener("abort", abortHandler, { once: true });
|
|
121
|
+
|
|
122
|
+
heartbeat = setInterval(() => enqueueText(":\n\n"), HEARTBEAT_INTERVAL_MS);
|
|
123
|
+
|
|
124
|
+
// Force the response headers through without claiming that the agent is
|
|
125
|
+
// ready. The client waits for the later `connected` data event.
|
|
126
|
+
enqueueText(":\n\n");
|
|
127
|
+
},
|
|
128
|
+
cancel() {
|
|
129
|
+
cancelStream(false);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const {
|
|
7
|
+
isEventIncludedInSnapshot,
|
|
8
|
+
toClientAgentEvent,
|
|
9
|
+
} = await jiti.import("./agent-event-wire.ts");
|
|
10
|
+
|
|
11
|
+
function assistantMessage(text) {
|
|
12
|
+
return {
|
|
13
|
+
role: "assistant",
|
|
14
|
+
content: [{ type: "text", text }],
|
|
15
|
+
api: "anthropic-messages",
|
|
16
|
+
provider: "anthropic",
|
|
17
|
+
model: "claude-sonnet-4-6",
|
|
18
|
+
usage: {
|
|
19
|
+
input: 0,
|
|
20
|
+
output: 0,
|
|
21
|
+
cacheRead: 0,
|
|
22
|
+
cacheWrite: 0,
|
|
23
|
+
totalTokens: 0,
|
|
24
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
25
|
+
},
|
|
26
|
+
stopReason: "pending",
|
|
27
|
+
timestamp: 1,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("projects message_update onto Pi 0.84's JSON/RPC delta shape", () => {
|
|
32
|
+
const partial = assistantMessage("Hello");
|
|
33
|
+
const projected = toClientAgentEvent({
|
|
34
|
+
type: "message_update",
|
|
35
|
+
message: partial,
|
|
36
|
+
assistantMessageEvent: {
|
|
37
|
+
type: "text_delta",
|
|
38
|
+
contentIndex: 0,
|
|
39
|
+
delta: "o",
|
|
40
|
+
partial,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(projected, {
|
|
45
|
+
type: "message_update",
|
|
46
|
+
assistantMessageEvent: {
|
|
47
|
+
type: "text_delta",
|
|
48
|
+
contentIndex: 0,
|
|
49
|
+
delta: "o",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
assert.equal(Object.hasOwn(projected, "message"), false);
|
|
53
|
+
assert.equal(Object.hasOwn(projected.assistantMessageEvent, "partial"), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("keeps tool identity on toolcall_start without sending the cumulative partial", () => {
|
|
57
|
+
const partial = {
|
|
58
|
+
...assistantMessage(""),
|
|
59
|
+
content: [{
|
|
60
|
+
type: "toolCall",
|
|
61
|
+
id: "call-write-1",
|
|
62
|
+
name: "write",
|
|
63
|
+
arguments: {},
|
|
64
|
+
partialJson: "",
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
const projected = toClientAgentEvent({
|
|
68
|
+
type: "message_update",
|
|
69
|
+
message: partial,
|
|
70
|
+
assistantMessageEvent: {
|
|
71
|
+
type: "toolcall_start",
|
|
72
|
+
contentIndex: 0,
|
|
73
|
+
partial,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.deepEqual(projected, {
|
|
78
|
+
type: "message_update",
|
|
79
|
+
assistantMessageEvent: {
|
|
80
|
+
type: "toolcall_start",
|
|
81
|
+
contentIndex: 0,
|
|
82
|
+
id: "call-write-1",
|
|
83
|
+
toolName: "write",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
assert.equal(Object.hasOwn(projected, "message"), false);
|
|
87
|
+
assert.equal(Object.hasOwn(projected.assistantMessageEvent, "partial"), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("refreshes late tool identity on toolcall_delta", () => {
|
|
91
|
+
const partial = {
|
|
92
|
+
...assistantMessage(""),
|
|
93
|
+
content: [{
|
|
94
|
+
type: "toolCall",
|
|
95
|
+
id: "call-late-1",
|
|
96
|
+
name: "write",
|
|
97
|
+
arguments: { path: "/tmp/file" },
|
|
98
|
+
partialJson: '{"path":"/tmp/file"',
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
const projected = toClientAgentEvent({
|
|
102
|
+
type: "message_update",
|
|
103
|
+
message: partial,
|
|
104
|
+
assistantMessageEvent: {
|
|
105
|
+
type: "toolcall_delta",
|
|
106
|
+
contentIndex: 0,
|
|
107
|
+
delta: '"/tmp/file"',
|
|
108
|
+
partial,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.deepEqual(projected, {
|
|
113
|
+
type: "message_update",
|
|
114
|
+
assistantMessageEvent: {
|
|
115
|
+
type: "toolcall_delta",
|
|
116
|
+
contentIndex: 0,
|
|
117
|
+
delta: '"/tmp/file"',
|
|
118
|
+
id: "call-late-1",
|
|
119
|
+
toolName: "write",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("does not whitelist assistant delta event types", () => {
|
|
125
|
+
const projected = toClientAgentEvent({
|
|
126
|
+
type: "message_update",
|
|
127
|
+
message: assistantMessage("future"),
|
|
128
|
+
assistantMessageEvent: {
|
|
129
|
+
type: "future_delta",
|
|
130
|
+
contentIndex: 3,
|
|
131
|
+
value: "kept",
|
|
132
|
+
partial: assistantMessage("future"),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.deepEqual(projected, {
|
|
137
|
+
type: "message_update",
|
|
138
|
+
assistantMessageEvent: {
|
|
139
|
+
type: "future_delta",
|
|
140
|
+
contentIndex: 3,
|
|
141
|
+
value: "kept",
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("rejects a malformed message_update without breaking the stream", () => {
|
|
147
|
+
assert.equal(toClientAgentEvent({
|
|
148
|
+
type: "message_update",
|
|
149
|
+
assistantMessageEvent: null,
|
|
150
|
+
}), null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("recognizes only the in-flight event already covered by a snapshot", () => {
|
|
154
|
+
const snapshot = assistantMessage("Hello");
|
|
155
|
+
assert.equal(isEventIncludedInSnapshot({
|
|
156
|
+
type: "message_update",
|
|
157
|
+
message: snapshot,
|
|
158
|
+
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "o" },
|
|
159
|
+
}, snapshot), true);
|
|
160
|
+
assert.equal(isEventIncludedInSnapshot({
|
|
161
|
+
type: "message_update",
|
|
162
|
+
message: { ...snapshot },
|
|
163
|
+
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "!" },
|
|
164
|
+
}, snapshot), false);
|
|
165
|
+
assert.equal(isEventIncludedInSnapshot({
|
|
166
|
+
type: "message_end",
|
|
167
|
+
message: snapshot,
|
|
168
|
+
}, snapshot), false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("keeps the existing event omissions and slim agent_end", () => {
|
|
172
|
+
assert.equal(toClientAgentEvent({ type: "turn_start" }), null);
|
|
173
|
+
assert.equal(toClientAgentEvent({ type: "turn_end" }), null);
|
|
174
|
+
assert.deepEqual(
|
|
175
|
+
toClientAgentEvent({ type: "agent_end", messages: [assistantMessage("done")] }),
|
|
176
|
+
{ type: "agent_end" },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const messageStart = { type: "message_start", message: assistantMessage("") };
|
|
180
|
+
assert.strictEqual(toClientAgentEvent(messageStart), messageStart);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("forwards tool execution progress without repeating tool arguments", () => {
|
|
184
|
+
const partialResult = {
|
|
185
|
+
content: [{ type: "text", text: "Running phase 2" }],
|
|
186
|
+
details: { phase: 2 },
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
assert.deepEqual(toClientAgentEvent({
|
|
190
|
+
type: "tool_execution_update",
|
|
191
|
+
toolCallId: "call-workflow-1",
|
|
192
|
+
toolName: "workflow",
|
|
193
|
+
args: { large: "repeated input" },
|
|
194
|
+
partialResult,
|
|
195
|
+
}), {
|
|
196
|
+
type: "tool_execution_update",
|
|
197
|
+
toolCallId: "call-workflow-1",
|
|
198
|
+
toolName: "workflow",
|
|
199
|
+
partialResult,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function projectedStreamBytes(totalLength) {
|
|
204
|
+
const chunkSize = 64;
|
|
205
|
+
let text = "";
|
|
206
|
+
let bytes = 0;
|
|
207
|
+
|
|
208
|
+
for (let offset = 0; offset < totalLength; offset += chunkSize) {
|
|
209
|
+
const delta = "x".repeat(Math.min(chunkSize, totalLength - offset));
|
|
210
|
+
text += delta;
|
|
211
|
+
const partial = assistantMessage(text);
|
|
212
|
+
const projected = toClientAgentEvent({
|
|
213
|
+
type: "message_update",
|
|
214
|
+
message: partial,
|
|
215
|
+
assistantMessageEvent: {
|
|
216
|
+
type: "text_delta",
|
|
217
|
+
contentIndex: 0,
|
|
218
|
+
delta,
|
|
219
|
+
partial,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
assert.equal(Object.hasOwn(projected, "message"), false);
|
|
224
|
+
assert.equal(Object.hasOwn(projected.assistantMessageEvent, "partial"), false);
|
|
225
|
+
bytes += Buffer.byteLength(JSON.stringify(projected));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return bytes;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test("serialized streaming traffic grows linearly with response length", () => {
|
|
232
|
+
const twoKiB = projectedStreamBytes(2 * 1024);
|
|
233
|
+
const fourKiB = projectedStreamBytes(4 * 1024);
|
|
234
|
+
assert.ok(fourKiB / twoKiB < 2.2, `expected near-linear growth, got ${fourKiB / twoKiB}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
function projectedToolStreamBytes(totalLength) {
|
|
238
|
+
const chunkSize = 64;
|
|
239
|
+
let rawInput = "";
|
|
240
|
+
let bytes = 0;
|
|
241
|
+
|
|
242
|
+
for (let offset = 0; offset < totalLength; offset += chunkSize) {
|
|
243
|
+
const delta = "x".repeat(Math.min(chunkSize, totalLength - offset));
|
|
244
|
+
rawInput += delta;
|
|
245
|
+
const partial = {
|
|
246
|
+
...assistantMessage(""),
|
|
247
|
+
content: [{
|
|
248
|
+
type: "toolCall",
|
|
249
|
+
id: "call-write-1",
|
|
250
|
+
name: "write",
|
|
251
|
+
arguments: { content: rawInput },
|
|
252
|
+
partialJson: rawInput,
|
|
253
|
+
}],
|
|
254
|
+
};
|
|
255
|
+
const projected = toClientAgentEvent({
|
|
256
|
+
type: "message_update",
|
|
257
|
+
message: partial,
|
|
258
|
+
assistantMessageEvent: {
|
|
259
|
+
type: "toolcall_delta",
|
|
260
|
+
contentIndex: 0,
|
|
261
|
+
delta,
|
|
262
|
+
partial,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.equal(Object.hasOwn(projected, "message"), false);
|
|
267
|
+
assert.equal(Object.hasOwn(projected.assistantMessageEvent, "partial"), false);
|
|
268
|
+
bytes += Buffer.byteLength(JSON.stringify(projected));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return bytes;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
test("serialized tool-input traffic grows linearly with write content", () => {
|
|
275
|
+
const twoKiB = projectedToolStreamBytes(2 * 1024);
|
|
276
|
+
const fourKiB = projectedToolStreamBytes(4 * 1024);
|
|
277
|
+
assert.ok(fourKiB / twoKiB < 2.2, `expected near-linear growth, got ${fourKiB / twoKiB}`);
|
|
278
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { JsonAgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface AgentEventLike {
|
|
4
|
+
type: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type JsonMessageUpdateEvent = Extract<
|
|
9
|
+
JsonAgentSessionEvent,
|
|
10
|
+
{ type: "message_update" }
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
type JsonAssistantMessageEvent = JsonMessageUpdateEvent["assistantMessageEvent"];
|
|
14
|
+
type JsonToolCallStartEvent = Extract<JsonAssistantMessageEvent, { type: "toolcall_start" }>;
|
|
15
|
+
type JsonToolCallDeltaEvent = Extract<JsonAssistantMessageEvent, { type: "toolcall_delta" }>;
|
|
16
|
+
|
|
17
|
+
export type ClientAssistantMessageEvent =
|
|
18
|
+
| Exclude<JsonAssistantMessageEvent, { type: "toolcall_start" | "toolcall_delta" }>
|
|
19
|
+
| (JsonToolCallStartEvent & { id?: string; toolName?: string })
|
|
20
|
+
| (JsonToolCallDeltaEvent & { id?: string; toolName?: string });
|
|
21
|
+
|
|
22
|
+
export type ClientMessageUpdateEvent = Omit<JsonMessageUpdateEvent, "assistantMessageEvent"> & {
|
|
23
|
+
assistantMessageEvent: ClientAssistantMessageEvent;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const OMITTED_EVENT_TYPES = new Set([
|
|
27
|
+
"turn_start",
|
|
28
|
+
"turn_end",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toolCallMetadata(
|
|
36
|
+
event: Record<string, unknown>,
|
|
37
|
+
): { id: string; toolName: string } | null {
|
|
38
|
+
if (
|
|
39
|
+
(event.type !== "toolcall_start" && event.type !== "toolcall_delta")
|
|
40
|
+
|| !isObject(event.partial)
|
|
41
|
+
) return null;
|
|
42
|
+
const content = event.partial.content;
|
|
43
|
+
const contentIndex = event.contentIndex;
|
|
44
|
+
if (!Array.isArray(content) || typeof contentIndex !== "number") return null;
|
|
45
|
+
|
|
46
|
+
const block = content[contentIndex];
|
|
47
|
+
if (!isObject(block) || block.type !== "toolCall") return null;
|
|
48
|
+
const id = typeof block.id === "string"
|
|
49
|
+
? block.id
|
|
50
|
+
: (typeof block.toolCallId === "string" ? block.toolCallId : null);
|
|
51
|
+
const toolName = typeof block.name === "string"
|
|
52
|
+
? block.name
|
|
53
|
+
: (typeof block.toolName === "string" ? block.toolName : null);
|
|
54
|
+
return id !== null && toolName !== null ? { id, toolName } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Apply pi-web's event filters plus Pi 0.84's message_update projection. */
|
|
58
|
+
export function toClientAgentEvent(
|
|
59
|
+
event: AgentEventLike,
|
|
60
|
+
): AgentEventLike | ClientMessageUpdateEvent | null {
|
|
61
|
+
if (OMITTED_EVENT_TYPES.has(event.type)) return null;
|
|
62
|
+
|
|
63
|
+
if (event.type === "message_update") {
|
|
64
|
+
const assistantMessageEvent = event.assistantMessageEvent;
|
|
65
|
+
if (
|
|
66
|
+
typeof assistantMessageEvent !== "object"
|
|
67
|
+
|| assistantMessageEvent === null
|
|
68
|
+
|| Array.isArray(assistantMessageEvent)
|
|
69
|
+
) return null;
|
|
70
|
+
|
|
71
|
+
if (!("partial" in assistantMessageEvent)) {
|
|
72
|
+
return {
|
|
73
|
+
type: "message_update",
|
|
74
|
+
assistantMessageEvent,
|
|
75
|
+
} as ClientMessageUpdateEvent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const metadata = toolCallMetadata(assistantMessageEvent as Record<string, unknown>);
|
|
79
|
+
const { partial: _partial, ...deltaEvent } = assistantMessageEvent;
|
|
80
|
+
void _partial;
|
|
81
|
+
return {
|
|
82
|
+
type: "message_update",
|
|
83
|
+
assistantMessageEvent: metadata ? { ...deltaEvent, ...metadata } : deltaEvent,
|
|
84
|
+
} as ClientMessageUpdateEvent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (event.type === "tool_execution_update") {
|
|
88
|
+
return {
|
|
89
|
+
type: "tool_execution_update",
|
|
90
|
+
toolCallId: event.toolCallId,
|
|
91
|
+
toolName: event.toolName,
|
|
92
|
+
partialResult: event.partialResult,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (event.type === "agent_end") return { type: "agent_end" };
|
|
97
|
+
return event;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isEventIncludedInSnapshot(
|
|
101
|
+
event: AgentEventLike,
|
|
102
|
+
snapshot: unknown,
|
|
103
|
+
): boolean {
|
|
104
|
+
return snapshot !== undefined
|
|
105
|
+
&& (event.type === "message_start" || event.type === "message_update")
|
|
106
|
+
&& event.message === snapshot;
|
|
107
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentPhase } from "@/hooks/useAgentSession";
|
|
2
|
+
|
|
3
|
+
/** 播报槽用阶段文案(从 ChatWindow 抽出,供 ChatInput 使用,避免循环依赖) */
|
|
4
|
+
export function phaseLabel(phase: AgentPhase, t: (key: string, params?: Record<string, string | number>) => string): string | null {
|
|
5
|
+
if (phase?.kind === "running_tools") {
|
|
6
|
+
const latest = phase.tools[phase.tools.length - 1];
|
|
7
|
+
if (latest?.progress) {
|
|
8
|
+
return `${t("chat.runningNamedTool", { name: latest.name })} ${latest.progress}`;
|
|
9
|
+
}
|
|
10
|
+
const names = phase.tools.map((t) => t.name);
|
|
11
|
+
if (names.length === 0) return t("chat.runningTool");
|
|
12
|
+
if (names.length === 1) return t("chat.runningNamedTool", { name: names[0] });
|
|
13
|
+
if (names.length <= 3) return t("chat.runningTools", { names: names.join(", ") });
|
|
14
|
+
return t("chat.runningToolsMore", { names: names.slice(0, 2).join(", "), count: names.length - 2 });
|
|
15
|
+
}
|
|
16
|
+
if (phase?.kind === "waiting_model") return t("chat.waitingModel");
|
|
17
|
+
if (phase?.kind === "running_command") return t("chat.runningCommand");
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 状态行的思考球:等待模型 = breathing,执行工具/命令 = working */
|
|
22
|
+
export function orbModeForPhase(phase: AgentPhase): "breathing" | "working" {
|
|
23
|
+
return phase?.kind === "waiting_model" ? "breathing" : "working";
|
|
24
|
+
}
|