@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.10
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 +7 -7
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1682 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/{db → adapter/cloudflare}/schema.ts +28 -80
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +190 -0
- package/src/config/index.ts +14 -0
- package/src/config/model-guidance.ts +21 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
- package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +99 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +113 -30
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +196 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +562 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/mcp.ts +1 -1
- package/src/lib/prompt.ts +7 -6
- package/src/lib/token-estimate.ts +8 -0
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +286 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/tools/README.md +44 -0
- package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
- package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
- package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
- package/src/tools/assistant/ask-user.ts +317 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/{pi/tool → tools/assistant}/time.ts +1 -1
- package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
- package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +32 -0
- package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
- package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
- package/src/tools/runtime/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
- package/src/tools/runtime/compiler.ts +264 -0
- package/src/tools/runtime/contracts.ts +117 -0
- package/src/tools/runtime/decision-gate.ts +337 -0
- package/src/tools/runtime/human-decisions.ts +127 -0
- package/src/tools/runtime/interaction/index.ts +1 -0
- package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
- package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
- package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
- package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
- package/src/tools/subagent/temporary/approvals.ts +123 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
- package/src/tools/subagent/temporary/core.ts +53 -0
- package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
- package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
- package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1320 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
- package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
- package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/base.ts +0 -550
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/tool/schedule.ts +0 -261
- package/src/pi/tool/skill.ts +0 -565
- package/src/pi/tool/web-search/api.ts +0 -1318
- package/src/pi/tool/web-search/index.ts +0 -2
- package/src/pi/tool/workspace-state-connector.ts +0 -146
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- package/src/{kernel → config}/degradation.ts +0 -0
- package/src/{kernel → config}/runtime-config.ts +0 -0
- package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
- /package/src/{workspace-versioning.ts → tools/workspace/versioning.ts} +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { HarnessClosed, type AgentLane, type LaneSnapshot, type WatchHandle } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core/harness/context";
|
|
3
|
+
import type { OperationResultRecord } from "@earendil-works/pi-agent-core/harness/session";
|
|
4
|
+
import type { Connection } from "agents";
|
|
5
|
+
import { MessageType } from "agents/chat";
|
|
6
|
+
import type { UIMessage, UIMessageChunk } from "ai";
|
|
7
|
+
import type { ConversationHistory } from "./history";
|
|
8
|
+
import { HarnessStreamProjection } from "./harness-stream";
|
|
9
|
+
|
|
10
|
+
interface ActiveConversation {
|
|
11
|
+
lane: AgentLane;
|
|
12
|
+
submissionId: string;
|
|
13
|
+
requestId: string;
|
|
14
|
+
assistantMessageId: string;
|
|
15
|
+
userMessageId?: string | null;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}
|
|
18
|
+
interface ResumeConnection {
|
|
19
|
+
connection: Connection;
|
|
20
|
+
source: ActiveConversation;
|
|
21
|
+
ready: Promise<void>;
|
|
22
|
+
watch?: WatchHandle<LaneSnapshot>;
|
|
23
|
+
projection?: HarnessStreamProjection;
|
|
24
|
+
acknowledging: boolean;
|
|
25
|
+
replay?: Promise<void>;
|
|
26
|
+
terminal?: OperationResultRecord;
|
|
27
|
+
released?: boolean;
|
|
28
|
+
finished?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** CF's resume handshake, with its replay prefix and subsequent events sourced only from Pi. */
|
|
33
|
+
export class HarnessConversationBridge {
|
|
34
|
+
private active?: ActiveConversation;
|
|
35
|
+
private readonly connections = new Map<string, ResumeConnection>();
|
|
36
|
+
|
|
37
|
+
constructor(private readonly host: {
|
|
38
|
+
history: Pick<ConversationHistory, "browserMessages" | "browserSnapshot">;
|
|
39
|
+
send: (connection: Connection, body: string) => void | Promise<void>;
|
|
40
|
+
broadcast: (body: string, excludedConnectionIds: string[]) => void;
|
|
41
|
+
}) {}
|
|
42
|
+
|
|
43
|
+
activate(lane: AgentLane, submission: Omit<ActiveConversation, "lane">): void {
|
|
44
|
+
this.active = { lane, ...submission };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private replayMessage(messages: UIMessage[], submissionId: string): UIMessage | undefined {
|
|
48
|
+
return messages.filter((message) => message.role === "assistant" &&
|
|
49
|
+
(message.metadata as { turnId?: string } | undefined)?.turnId === submissionId).at(-1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private historyBeforeReplay(messages: UIMessage[], submissionId: string): UIMessage[] {
|
|
53
|
+
const target = this.replayMessage(messages, submissionId);
|
|
54
|
+
return target ? messages.filter((message) => message.id !== target.id) : messages;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reads never release lifecycle state; hydration leaves the active reply to replay. */
|
|
58
|
+
async messages(view: "full" | "hydration" = "full"): Promise<UIMessage[]> {
|
|
59
|
+
for (;;) {
|
|
60
|
+
const source = this.active;
|
|
61
|
+
let watch: WatchHandle<LaneSnapshot> | undefined;
|
|
62
|
+
if (source) {
|
|
63
|
+
try { watch = await source.lane.watch(BACKGROUND_CONTEXT); }
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof HarnessClosed)) throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const messages = watch
|
|
70
|
+
? await this.host.history.browserSnapshot(watch.snapshot)
|
|
71
|
+
: await this.host.history.browserMessages();
|
|
72
|
+
if (this.active !== source) continue;
|
|
73
|
+
return view === "hydration" && source && watch?.snapshot.operation?.id === source.submissionId
|
|
74
|
+
? this.historyBeforeReplay(messages, source.submissionId)
|
|
75
|
+
: messages;
|
|
76
|
+
} finally { watch?.unsubscribe(); }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async send(connection: Connection, frame: unknown): Promise<void> {
|
|
81
|
+
const session = this.connections.get(connection.id);
|
|
82
|
+
try { await this.host.send(connection, JSON.stringify(frame)); }
|
|
83
|
+
catch { if (session) this.discard(session); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async sendHistory(connection: Connection, messages: UIMessage[]): Promise<void> {
|
|
87
|
+
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async sendIdleHistory(connection: Connection, probeId?: string): Promise<void> {
|
|
91
|
+
const source = this.active;
|
|
92
|
+
const session = this.connections.get(connection.id);
|
|
93
|
+
const messages = await this.host.history.browserMessages();
|
|
94
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
95
|
+
if (this.active !== source) return this.resume(connection, probeId);
|
|
96
|
+
await this.sendHistory(connection, messages);
|
|
97
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
98
|
+
if (this.active !== source) return this.resume(connection, probeId);
|
|
99
|
+
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, ...(probeId ? { probeId } : {}) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async sendChunks(session: ResumeConnection, chunks: readonly UIMessageChunk[], replay = false): Promise<void> {
|
|
103
|
+
for (const chunk of chunks) {
|
|
104
|
+
if (this.connections.get(session.connection.id) !== session) return;
|
|
105
|
+
await this.send(session.connection, {
|
|
106
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
107
|
+
id: session.source.requestId,
|
|
108
|
+
body: JSON.stringify(chunk), done: false,
|
|
109
|
+
...(replay ? { replay: true, continuation: false } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async resume(connection: Connection, probeId?: string): Promise<void> {
|
|
115
|
+
const source = this.active;
|
|
116
|
+
if (!source) {
|
|
117
|
+
return this.sendIdleHistory(connection, probeId);
|
|
118
|
+
}
|
|
119
|
+
let session = this.connections.get(connection.id);
|
|
120
|
+
if (!session || session.source !== source) {
|
|
121
|
+
this.disconnect(connection);
|
|
122
|
+
session = { connection, source, ready: Promise.resolve(), acknowledging: false };
|
|
123
|
+
this.connections.set(connection.id, session);
|
|
124
|
+
const current = session;
|
|
125
|
+
current.ready = (async () => {
|
|
126
|
+
const watch = await source.lane.watch(BACKGROUND_CONTEXT);
|
|
127
|
+
if (this.connections.get(connection.id) !== current) { watch.unsubscribe(); return; }
|
|
128
|
+
current.watch = watch;
|
|
129
|
+
})();
|
|
130
|
+
}
|
|
131
|
+
try { await session.ready; }
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
134
|
+
this.discard(session);
|
|
135
|
+
if (this.active !== source) return this.resume(connection, probeId);
|
|
136
|
+
if (error instanceof HarnessClosed) {
|
|
137
|
+
await this.sendIdleHistory(connection, probeId);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
142
|
+
if (this.active !== source) {
|
|
143
|
+
this.discard(session);
|
|
144
|
+
return this.resume(connection, probeId);
|
|
145
|
+
}
|
|
146
|
+
const snapshot = session.watch?.snapshot;
|
|
147
|
+
if (!snapshot?.operation || snapshot.operation.id !== source.submissionId) {
|
|
148
|
+
this.discard(session);
|
|
149
|
+
return this.sendIdleHistory(connection, probeId);
|
|
150
|
+
}
|
|
151
|
+
if (!session.acknowledging) {
|
|
152
|
+
const messages = await this.host.history.browserSnapshot(snapshot);
|
|
153
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
154
|
+
if (this.active !== source) { this.discard(session); return this.resume(connection, probeId); }
|
|
155
|
+
await this.sendHistory(connection, this.historyBeforeReplay(messages, source.submissionId));
|
|
156
|
+
}
|
|
157
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
158
|
+
if (this.active !== source) { this.discard(session); return this.resume(connection, probeId); }
|
|
159
|
+
await this.send(connection, {
|
|
160
|
+
type: MessageType.CF_AGENT_STREAM_RESUMING, id: source.requestId,
|
|
161
|
+
...(probeId ? { probeId } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async acknowledge(connection: Connection, requestId: string): Promise<void> {
|
|
166
|
+
const session = this.connections.get(connection.id);
|
|
167
|
+
if (!session || session.source.requestId !== requestId || session.acknowledging) return;
|
|
168
|
+
if (this.active && this.active.submissionId !== session.source.submissionId) {
|
|
169
|
+
// A queued Submission may already be live when an older offer is ACKed.
|
|
170
|
+
// End the old transport, then atomically resume the current lane view.
|
|
171
|
+
this.disconnect(connection);
|
|
172
|
+
const current = this.resume(connection);
|
|
173
|
+
await this.send(connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, id: requestId, body: "", done: true });
|
|
174
|
+
await current;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
session.acknowledging = true;
|
|
178
|
+
session.replay = this.replay(session);
|
|
179
|
+
try {
|
|
180
|
+
await session.replay;
|
|
181
|
+
if (session.terminal) await this.finish(session, session.terminal);
|
|
182
|
+
if (session.released) this.discard(session);
|
|
183
|
+
} catch { this.discard(session); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async replay(session: ResumeConnection): Promise<void> {
|
|
187
|
+
const connection = session.connection;
|
|
188
|
+
const requestId = session.source.requestId;
|
|
189
|
+
await session.ready;
|
|
190
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
191
|
+
if (!session.watch && this.active?.submissionId === session.source.submissionId) {
|
|
192
|
+
session.source = this.active;
|
|
193
|
+
session.released = false;
|
|
194
|
+
const watch = await this.active.lane.watch(BACKGROUND_CONTEXT);
|
|
195
|
+
if (this.connections.get(connection.id) !== session) { watch.unsubscribe(); return; }
|
|
196
|
+
session.watch = watch;
|
|
197
|
+
}
|
|
198
|
+
const watch = session.watch;
|
|
199
|
+
const messages = watch ? await this.host.history.browserSnapshot(watch.snapshot) : await this.host.history.browserMessages();
|
|
200
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
201
|
+
const source = session.source;
|
|
202
|
+
const message = this.replayMessage(messages, source.submissionId) ?? {
|
|
203
|
+
id: source.assistantMessageId, role: "assistant" as const, parts: [],
|
|
204
|
+
metadata: { turnId: source.submissionId, userMessageId: source.userMessageId,
|
|
205
|
+
createdAt: source.createdAt, turnStatus: "running" },
|
|
206
|
+
};
|
|
207
|
+
const projection = new HarnessStreamProjection({
|
|
208
|
+
messageId: message.id, startedAt: source.createdAt, turnId: source.submissionId,
|
|
209
|
+
userMessageId: source.userMessageId,
|
|
210
|
+
});
|
|
211
|
+
session.projection = projection;
|
|
212
|
+
const prefix = projection.restore(message, Boolean(watch?.snapshot.operation?.streamingMessage));
|
|
213
|
+
await this.sendChunks(session, prefix, true);
|
|
214
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
215
|
+
await this.send(connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
216
|
+
id: requestId, body: "", done: false, replayComplete: true });
|
|
217
|
+
if (this.connections.get(connection.id) !== session) return;
|
|
218
|
+
watch?.start(async (event) => {
|
|
219
|
+
if ("runId" in event && event.runId !== source.submissionId) return;
|
|
220
|
+
await this.sendChunks(session, projection.event(event));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async finish(session: ResumeConnection, record: OperationResultRecord): Promise<void> {
|
|
225
|
+
if (session.finished || !session.projection) return;
|
|
226
|
+
session.finished = true;
|
|
227
|
+
await session.watch?.drain();
|
|
228
|
+
await this.sendChunks(session, session.projection.finish(record));
|
|
229
|
+
if (this.connections.get(session.connection.id) === session) {
|
|
230
|
+
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
231
|
+
id: session.source.requestId, body: "", done: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Called only after the Host has persisted the Submission's terminal result. */
|
|
236
|
+
async settle(record: OperationResultRecord): Promise<void> {
|
|
237
|
+
await Promise.all([...this.connections.values()].filter((session) =>
|
|
238
|
+
session.source.submissionId === record.operationId).map(async (session) => {
|
|
239
|
+
session.terminal = record;
|
|
240
|
+
try {
|
|
241
|
+
if (session.replay) await session.replay;
|
|
242
|
+
await this.finish(session, record);
|
|
243
|
+
} catch { this.discard(session); }
|
|
244
|
+
}));
|
|
245
|
+
if (this.active?.submissionId === record.operationId) {
|
|
246
|
+
this.broadcastResponse({ id: this.active.requestId, body: "", done: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Called after a drive returns, outside Pi event listeners. */
|
|
251
|
+
async release(lane: AgentLane): Promise<void> {
|
|
252
|
+
if (this.active?.lane === lane) this.active = undefined;
|
|
253
|
+
await Promise.all([...this.connections.values()].filter((session) => session.source.lane === lane)
|
|
254
|
+
.map(async (session) => {
|
|
255
|
+
try {
|
|
256
|
+
await session.ready;
|
|
257
|
+
if (session.replay) await session.replay;
|
|
258
|
+
if (session.projection) {
|
|
259
|
+
await session.watch?.drain();
|
|
260
|
+
this.discard(session);
|
|
261
|
+
} else {
|
|
262
|
+
session.watch?.unsubscribe();
|
|
263
|
+
session.watch = undefined;
|
|
264
|
+
session.released = true;
|
|
265
|
+
}
|
|
266
|
+
} catch { this.discard(session); }
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
disconnect(connection: Pick<Connection, "id">): void {
|
|
271
|
+
const session = this.connections.get(connection.id);
|
|
272
|
+
if (session) this.discard(session);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private discard(session: ResumeConnection): void {
|
|
276
|
+
if (this.connections.get(session.connection.id) !== session) return;
|
|
277
|
+
session.watch?.unsubscribe();
|
|
278
|
+
this.connections.delete(session.connection.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
broadcastResponse(frame: { id: string; body: string; done: boolean; error?: boolean; continuation?: boolean }): void {
|
|
282
|
+
// Approval is a Host fact, so it has no duplicate Pi event for the watch to project.
|
|
283
|
+
const isApproval = frame.body && JSON.parse(frame.body).type === "tool-approval-request";
|
|
284
|
+
const excluded = isApproval ? [] : [...this.connections.values()]
|
|
285
|
+
.filter((session) => session.source.requestId === frame.id).map(({ connection }) => connection.id);
|
|
286
|
+
this.host.broadcast(JSON.stringify({ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, ...frame }), excluded);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AgentEvent, Events, HarnessEvent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { OperationResultRecord } from "@earendil-works/pi-agent-core/harness/session";
|
|
3
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { UIMessage, UIMessageChunk } from "ai";
|
|
5
|
+
import { PiChunkEncoder, type ToolInputPreparer } from "./message";
|
|
6
|
+
|
|
7
|
+
export interface HarnessStreamOptions {
|
|
8
|
+
readonly messageId: string;
|
|
9
|
+
readonly startedAt: number;
|
|
10
|
+
readonly turnId: string;
|
|
11
|
+
readonly userMessageId?: string | null;
|
|
12
|
+
readonly prepareToolInput?: ToolInputPreparer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Project the final Host decision without rewriting Pi's canonical operation result. */
|
|
16
|
+
export function projectSubmissionOutcome(
|
|
17
|
+
record: OperationResultRecord,
|
|
18
|
+
submission: { status: string; completedAt?: number | null; error?: string | null },
|
|
19
|
+
): OperationResultRecord {
|
|
20
|
+
const status = submission.status === "aborted" ? "aborted"
|
|
21
|
+
: submission.status === "error" ? "failed"
|
|
22
|
+
: submission.status === "skipped" ? "declined" : "completed";
|
|
23
|
+
const { error: _previousError, ...result } = record;
|
|
24
|
+
return {
|
|
25
|
+
...result, status, endedAt: submission.completedAt ?? record.endedAt,
|
|
26
|
+
...((status === "failed" || status === "aborted") && submission.error
|
|
27
|
+
? { error: { code: status, message: submission.error } } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One projection for both broadcast events and a resumed Pi watch. */
|
|
32
|
+
export class HarnessStreamProjection {
|
|
33
|
+
private readonly encoder: PiChunkEncoder;
|
|
34
|
+
private terminal: AssistantMessage | undefined;
|
|
35
|
+
|
|
36
|
+
constructor(options: HarnessStreamOptions) {
|
|
37
|
+
this.encoder = new PiChunkEncoder(options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
restore(message: UIMessage, streaming: boolean): UIMessageChunk[] {
|
|
41
|
+
return this.encoder.restore(message, streaming);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
event(event: HarnessEvent): UIMessageChunk[] {
|
|
45
|
+
switch (event.type) {
|
|
46
|
+
case "run_start":
|
|
47
|
+
case "run_resume": return this.encoder.start();
|
|
48
|
+
case "turn_start":
|
|
49
|
+
this.terminal = undefined;
|
|
50
|
+
return this.encoder.encode({ type: "turn_start" } as AgentEvent);
|
|
51
|
+
case "message_update":
|
|
52
|
+
return event.event.type === "error" ? [] : this.encoder.encode({
|
|
53
|
+
type: "message_update", message: event.message, assistantMessageEvent: event.event,
|
|
54
|
+
} as AgentEvent);
|
|
55
|
+
case "tool_start": return this.encoder.encode({
|
|
56
|
+
type: "tool_execution_start", toolCallId: event.toolCallId, toolName: event.toolName, args: event.args,
|
|
57
|
+
} as AgentEvent);
|
|
58
|
+
case "tool_update": return this.encoder.encode({
|
|
59
|
+
type: "tool_execution_update", toolCallId: event.toolCallId, toolName: event.toolName, args: {}, partialResult: event.partialResult,
|
|
60
|
+
} as AgentEvent);
|
|
61
|
+
case "tool_end": return this.encoder.encode({
|
|
62
|
+
type: "tool_execution_end", toolCallId: event.toolCallId, toolName: event.toolName, result: event.result, isError: event.isError,
|
|
63
|
+
} as AgentEvent);
|
|
64
|
+
case "message_end":
|
|
65
|
+
if (event.message.role !== "assistant") return [];
|
|
66
|
+
this.terminal = event.message;
|
|
67
|
+
return this.encoder.encode({ type: "message_end", message: event.message } as AgentEvent, { terminal: false });
|
|
68
|
+
default: return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
approvalRequested(toolCallId: string, approvalId: string): UIMessageChunk[] {
|
|
73
|
+
return this.encoder.approvalRequested(toolCallId, approvalId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
finish(result: Pick<OperationResultRecord, "status" | "endedAt" | "error">): UIMessageChunk[] {
|
|
77
|
+
return this.encoder.finish({
|
|
78
|
+
stopReason: result.status === "aborted" ? "aborted" : result.status === "failed" ? "error" :
|
|
79
|
+
this.terminal?.stopReason === "length" ? "length" : "stop",
|
|
80
|
+
timestamp: result.endedAt,
|
|
81
|
+
...(result.error?.message ? { errorMessage: result.error.message } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const STREAM_EVENTS = ["run_start", "run_resume", "turn_start", "message_update", "tool_start", "tool_update", "tool_end", "message_end"] as const;
|
|
87
|
+
|
|
88
|
+
export function bindHarnessStream(
|
|
89
|
+
events: Events,
|
|
90
|
+
options: HarnessStreamOptions & { readonly publish: (chunks: readonly UIMessageChunk[]) => Promise<void> },
|
|
91
|
+
) {
|
|
92
|
+
const projection = new HarnessStreamProjection(options);
|
|
93
|
+
const unsubscribers = STREAM_EVENTS.map((type) => events.on(type, (event) => options.publish(projection.event(event))));
|
|
94
|
+
return {
|
|
95
|
+
publishApprovalRequest: (toolCallId: string, approvalId: string) => options.publish(projection.approvalRequested(toolCallId, approvalId)),
|
|
96
|
+
flushTerminal: (result: OperationResultRecord) => options.publish(projection.finish(result)),
|
|
97
|
+
unsubscribe: () => { for (const unsubscribe of unsubscribers) unsubscribe(); },
|
|
98
|
+
};
|
|
99
|
+
}
|