@springbrand/agent-runtime 0.3.0-alpha.16 → 0.3.0-alpha.18
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 +6 -4
- package/src/adapter/cloudflare/agent.ts +81 -123
- package/src/adapter/cloudflare/subagent/definition.ts +21 -9
- package/src/adapter/cloudflare/subagent/runner.ts +42 -49
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/config/harness-runtime.ts +28 -15
- package/src/config/model-guidance.ts +16 -17
- package/src/config/ports.ts +2 -0
- package/src/conversation/harness-resume.ts +69 -234
- package/src/conversation/harness-stream.ts +85 -70
- package/src/conversation/history.ts +153 -153
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/session-entry.ts +29 -0
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +251 -607
- package/src/harness/submission-operation.ts +15 -40
- package/src/models/index.ts +14 -3
- package/src/models/openrouter/protocol-response.ts +36 -112
- package/src/session/cloudflare/migration.ts +382 -924
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/submission/finalization.ts +29 -13
- package/src/submission/repository.ts +1 -22
- package/src/submission/steer-delivery.ts +32 -76
- package/src/telemetry/turn-events.ts +34 -24
- package/src/tools/runtime/compiler.ts +32 -50
- package/src/tools/runtime/decision-gate.ts +5 -5
|
@@ -91,8 +91,10 @@ export class HarnessRuntimeConfig {
|
|
|
91
91
|
prepared: PreparedPiRuntime,
|
|
92
92
|
pinnedDescriptor: string,
|
|
93
93
|
request: {
|
|
94
|
-
readonly modelSessionId?: string;
|
|
94
|
+
readonly modelSessionId?: string | (() => string);
|
|
95
95
|
readonly onGeneration?: PiGenerationLifecycleObserver;
|
|
96
|
+
readonly assertRequestAllowed?: () => void;
|
|
97
|
+
readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
|
|
96
98
|
} = {},
|
|
97
99
|
) {
|
|
98
100
|
const { state, descriptor } = readPinnedPiRuntime(
|
|
@@ -102,6 +104,19 @@ export class HarnessRuntimeConfig {
|
|
|
102
104
|
);
|
|
103
105
|
const pinnedModel = state.snapshot.pi.model;
|
|
104
106
|
let models!: MutableModels;
|
|
107
|
+
const requestStream = (observer = request.onGeneration) => {
|
|
108
|
+
const stream = withProviderBoundary(
|
|
109
|
+
this.models.streamSimple.bind(this.models), request.modelSessionId, observer,
|
|
110
|
+
);
|
|
111
|
+
return ((model, context, options) => lazyStream(model, async () => {
|
|
112
|
+
options?.signal?.throwIfAborted();
|
|
113
|
+
request.assertRequestAllowed?.();
|
|
114
|
+
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
115
|
+
options?.signal?.throwIfAborted();
|
|
116
|
+
request.assertRequestAllowed?.();
|
|
117
|
+
return stream(model, refreshed, options);
|
|
118
|
+
})) satisfies typeof stream;
|
|
119
|
+
};
|
|
105
120
|
models = new Proxy(this.models, {
|
|
106
121
|
get: (target, property) => {
|
|
107
122
|
if (property === "getModel") {
|
|
@@ -110,21 +125,19 @@ export class HarnessRuntimeConfig {
|
|
|
110
125
|
? pinnedModel
|
|
111
126
|
: target.getModel(provider, modelId);
|
|
112
127
|
}
|
|
113
|
-
if (property === "streamSimple")
|
|
114
|
-
const stream = withProviderBoundary(
|
|
115
|
-
target.streamSimple.bind(target),
|
|
116
|
-
request.modelSessionId,
|
|
117
|
-
request.onGeneration,
|
|
118
|
-
);
|
|
119
|
-
return ((model, context, options) => lazyStream(model, async () => {
|
|
120
|
-
options?.signal?.throwIfAborted();
|
|
121
|
-
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
122
|
-
options?.signal?.throwIfAborted();
|
|
123
|
-
return stream(model, refreshed, options);
|
|
124
|
-
})) satisfies typeof stream;
|
|
125
|
-
}
|
|
128
|
+
if (property === "streamSimple") return requestStream();
|
|
126
129
|
if (property === "completeSimple") {
|
|
127
|
-
|
|
130
|
+
const compactionModels = new Proxy(models, {
|
|
131
|
+
get: (base, key) => {
|
|
132
|
+
if (key === "streamSimple") return requestStream((event) => {
|
|
133
|
+
request.onGeneration?.(event);
|
|
134
|
+
request.onCompactionGeneration?.(event);
|
|
135
|
+
});
|
|
136
|
+
const value = Reflect.get(base, key, base);
|
|
137
|
+
return typeof value === "function" ? value.bind(base) : value;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
return target.completeSimple.bind(compactionModels);
|
|
128
141
|
}
|
|
129
142
|
const value = Reflect.get(target, property, target);
|
|
130
143
|
return typeof value === "function" ? value.bind(target) : value;
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { getCurrentTools, normalizeContext, type Context, type Tool, type TranscriptContext } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { PiToolCandidate } from "../tools/runtime/contracts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
export async function refreshModelGuidance(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
)
|
|
9
|
-
const guidance = new Map(await Promise.all(candidates
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
tools: context.tools?.map(tool => {
|
|
17
|
-
const prompt = guidance.get(tool.name);
|
|
18
|
-
return prompt ? { ...tool, description: prompt.description } : tool;
|
|
4
|
+
/** Request-only presentation; Pi remains the source of transcript and active Tool selection. */
|
|
5
|
+
export async function refreshModelGuidance(candidates: readonly PiToolCandidate[], context: Context): Promise<TranscriptContext> {
|
|
6
|
+
const transcript = normalizeContext(context);
|
|
7
|
+
const active = new Set(getCurrentTools(transcript.messages).map((tool) => tool.name));
|
|
8
|
+
const byName = new Map(candidates.map((candidate) => [candidate.tool.name, candidate]));
|
|
9
|
+
const guidance = new Map(await Promise.all(candidates.filter((candidate) => active.has(candidate.tool.name) && candidate.modelGuidance)
|
|
10
|
+
.map(async (candidate) => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
|
|
11
|
+
const messages = transcript.messages.map((message) => message.role !== "system" || !message.toolsAdded ? message : {
|
|
12
|
+
...message,
|
|
13
|
+
toolsAdded: message.toolsAdded.map((tool): Tool => {
|
|
14
|
+
const candidate = byName.get(tool.name), prompt = guidance.get(tool.name);
|
|
15
|
+
return { ...tool, ...(candidate?.deferLoading ? { deferLoading: true as const } : {}), ...(prompt ? { description: prompt.description } : {}) };
|
|
19
16
|
}),
|
|
20
|
-
};
|
|
17
|
+
});
|
|
18
|
+
const sections = [...new Set([...guidance.values()].flatMap((value) => value.systemPrompt ? [value.systemPrompt] : []))];
|
|
19
|
+
return normalizeContext({ messages: sections.length ? [...messages, { role: "system", content: "", sections: { "springbrand.live-services": sections.join("\n\n") }, timestamp: 1 }] : messages });
|
|
21
20
|
}
|
package/src/config/ports.ts
CHANGED
|
@@ -1004,6 +1004,8 @@ export interface RuntimeModelEndpoint {
|
|
|
1004
1004
|
>;
|
|
1005
1005
|
/** OpenRouter upstream provider pins keyed by model ID; pinned requests never fall back. */
|
|
1006
1006
|
openRouterProviderPins?: Readonly<Record<string, string>>;
|
|
1007
|
+
/** OpenRouter provider ordering; omitted keeps OpenRouter's default routing. */
|
|
1008
|
+
openRouterProviderSort?: "price" | "throughput" | "latency";
|
|
1007
1009
|
modelMetadata?: Readonly<Record<string, RuntimeModelMetadata>>;
|
|
1008
1010
|
}
|
|
1009
1011
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { BACKGROUND_CONTEXT } from "@earendil-works/
|
|
3
|
-
import type { OperationResultRecord } from "@earendil-works/pi-agent-core/harness/session";
|
|
1
|
+
import type { AgentEventStream } from "@earendil-works/pi-durable";
|
|
2
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
4
3
|
import type { Connection } from "agents";
|
|
5
|
-
import { MessageType } from "agents/chat";
|
|
4
|
+
import { MessageType, STREAM_RESUME_NONE_REASONS } from "agents/chat";
|
|
6
5
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
6
|
+
import type { SessionHarness, SessionRunOutcome } from "../harness/session-harness";
|
|
7
7
|
import type { ConversationHistory } from "./history";
|
|
8
8
|
import { HarnessStreamProjection } from "./harness-stream";
|
|
9
9
|
|
|
10
10
|
interface ActiveConversation {
|
|
11
|
-
|
|
11
|
+
runtime: SessionHarness;
|
|
12
12
|
submissionId: string;
|
|
13
13
|
requestId: string;
|
|
14
14
|
assistantMessageId: string;
|
|
@@ -18,271 +18,106 @@ interface ActiveConversation {
|
|
|
18
18
|
interface ResumeConnection {
|
|
19
19
|
connection: Connection;
|
|
20
20
|
source: ActiveConversation;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
replay?: Promise<void>;
|
|
26
|
-
terminal?: OperationResultRecord;
|
|
27
|
-
released?: boolean;
|
|
28
|
-
finished?: boolean;
|
|
21
|
+
watch: AgentEventStream;
|
|
22
|
+
projection: HarnessStreamProjection;
|
|
23
|
+
delivery: Promise<void>;
|
|
24
|
+
started: boolean;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
/** CF's resume handshake, with its replay prefix and subsequent events sourced only from Pi. */
|
|
27
|
+
/** Pi supplies the atomic snapshot and deltas; this boundary owns the CF transport handshake. */
|
|
33
28
|
export class HarnessConversationBridge {
|
|
34
29
|
private active?: ActiveConversation;
|
|
35
30
|
private readonly connections = new Map<string, ResumeConnection>();
|
|
36
|
-
|
|
37
31
|
constructor(private readonly host: {
|
|
38
32
|
history: Pick<ConversationHistory, "browserMessages" | "browserSnapshot">;
|
|
39
33
|
send: (connection: Connection, body: string) => void | Promise<void>;
|
|
40
34
|
broadcast: (body: string, excludedConnectionIds: string[]) => void;
|
|
41
35
|
}) {}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
activate(runtime: SessionHarness, submission: Omit<ActiveConversation, "runtime">): void { this.active = { runtime, ...submission }; }
|
|
37
|
+
private replayMessage(messages: UIMessage[], submissionId: string) {
|
|
38
|
+
return [...messages].reverse().find((message) => message.role === "assistant" && (message.metadata as { turnId?: string } | undefined)?.turnId === submissionId);
|
|
45
39
|
}
|
|
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
40
|
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
41
|
const source = this.active;
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, ...(probeId ? { probeId } : {}) });
|
|
42
|
+
if (!source) return this.host.history.browserMessages();
|
|
43
|
+
const watch = await source.runtime.watch(BACKGROUND_CONTEXT);
|
|
44
|
+
try {
|
|
45
|
+
const messages = await this.host.history.browserSnapshot(watch.snapshot, source.submissionId);
|
|
46
|
+
const replay = this.replayMessage(messages, source.submissionId);
|
|
47
|
+
return view === "hydration" && watch.snapshot.run && replay ? messages.filter((message) => message.id !== replay.id) : messages;
|
|
48
|
+
} finally { await watch.stop(); }
|
|
100
49
|
}
|
|
101
|
-
|
|
102
|
-
private async
|
|
50
|
+
private send(connection: Connection, frame: unknown) { return this.host.send(connection, JSON.stringify(frame)); }
|
|
51
|
+
private async chunks(session: ResumeConnection, chunks: readonly UIMessageChunk[], replay = false) {
|
|
103
52
|
for (const chunk of chunks) {
|
|
104
53
|
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
|
-
});
|
|
54
|
+
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, id: session.source.requestId, body: JSON.stringify(chunk), done: false, ...(replay ? { replay: true, continuation: false } : {}) });
|
|
111
55
|
}
|
|
112
56
|
}
|
|
113
|
-
|
|
114
57
|
async resume(connection: Connection, probeId?: string): Promise<void> {
|
|
58
|
+
this.disconnect(connection);
|
|
115
59
|
const source = this.active;
|
|
116
60
|
if (!source) {
|
|
117
|
-
|
|
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
|
-
}
|
|
61
|
+
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: await this.host.history.browserMessages() });
|
|
62
|
+
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, reason: STREAM_RESUME_NONE_REASONS.IDLE, ...(probeId ? { probeId } : {}) });
|
|
139
63
|
return;
|
|
140
64
|
}
|
|
141
|
-
|
|
142
|
-
if (this.active !== source) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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));
|
|
65
|
+
const watch = await source.runtime.watch(BACKGROUND_CONTEXT);
|
|
66
|
+
if (this.active !== source) { await watch.stop(); return this.resume(connection, probeId); }
|
|
67
|
+
if (!watch.snapshot.run) {
|
|
68
|
+
await watch.stop();
|
|
69
|
+
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: await this.host.history.browserMessages() });
|
|
70
|
+
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, reason: STREAM_RESUME_NONE_REASONS.IDLE, ...(probeId ? { probeId } : {}) });
|
|
71
|
+
return;
|
|
156
72
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
73
|
+
const messages = await this.host.history.browserSnapshot(watch.snapshot, source.submissionId);
|
|
74
|
+
const replay = this.replayMessage(messages, source.submissionId);
|
|
75
|
+
const session: ResumeConnection = { connection, source, watch, projection: new HarnessStreamProjection({ messageId: source.assistantMessageId, userMessageId: source.userMessageId, startedAt: source.createdAt, turnId: source.submissionId }), delivery: Promise.resolve(), started: false };
|
|
76
|
+
this.connections.set(connection.id, session);
|
|
77
|
+
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: replay ? messages.filter((message) => message.id !== replay.id) : messages });
|
|
78
|
+
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUMING, id: source.requestId, ...(probeId ? { probeId } : {}) });
|
|
163
79
|
}
|
|
164
|
-
|
|
165
80
|
async acknowledge(connection: Connection, requestId: string): Promise<void> {
|
|
166
81
|
const session = this.connections.get(connection.id);
|
|
167
|
-
if (!session || session.source.requestId !== requestId || session.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
});
|
|
82
|
+
if (!session || session.source.requestId !== requestId || session.started) return;
|
|
83
|
+
session.started = true;
|
|
84
|
+
const messages = await this.host.history.browserSnapshot(session.watch.snapshot, session.source.submissionId);
|
|
85
|
+
const replay = this.replayMessage(messages, session.source.submissionId);
|
|
86
|
+
if (replay) await this.chunks(session, session.projection.restore(replay, Boolean(session.watch.snapshot.generation?.message), session.watch.snapshot), true);
|
|
87
|
+
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, id: session.source.requestId, body: "", done: false, replayComplete: true });
|
|
88
|
+
session.watch.start((events) => session.delivery = (async () => {
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.type === "snapshot") {
|
|
91
|
+
await this.resume(session.connection);
|
|
92
|
+
return;
|
|
93
|
+
} else await this.chunks(session, session.projection.event(event));
|
|
94
|
+
}
|
|
95
|
+
})());
|
|
222
96
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
231
|
-
id: session.source.requestId, body: "", done: true });
|
|
97
|
+
async settle(outcome: SessionRunOutcome): Promise<void> {
|
|
98
|
+
for (const session of this.connections.values()) {
|
|
99
|
+
await session.watch.stop(); await session.delivery;
|
|
100
|
+
if (session.started) {
|
|
101
|
+
await this.chunks(session, session.projection.finish(outcome));
|
|
102
|
+
}
|
|
103
|
+
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, id: session.source.requestId, body: "", done: true });
|
|
232
104
|
}
|
|
105
|
+
if (this.active) this.broadcastResponse({ id: this.active.requestId, body: "", done: true });
|
|
233
106
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
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 });
|
|
107
|
+
async release(runtime: SessionHarness): Promise<void> {
|
|
108
|
+
if (this.active?.runtime === runtime) this.active = undefined;
|
|
109
|
+
for (const session of [...this.connections.values()]) {
|
|
110
|
+
if (session.source.runtime !== runtime) continue;
|
|
111
|
+
await session.watch.stop(); await session.delivery; this.connections.delete(session.connection.id);
|
|
247
112
|
}
|
|
248
113
|
}
|
|
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
114
|
disconnect(connection: Pick<Connection, "id">): void {
|
|
271
115
|
const session = this.connections.get(connection.id);
|
|
272
|
-
if (session) this.
|
|
116
|
+
if (session) { this.connections.delete(connection.id); void session.watch.stop(); }
|
|
273
117
|
}
|
|
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
118
|
broadcastResponse(frame: { id: string; body: string; done: boolean; error?: boolean; continuation?: boolean }): void {
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const excluded = isApproval ? [] : [...this.connections.values()]
|
|
285
|
-
.filter((session) => session.source.requestId === frame.id).map(({ connection }) => connection.id);
|
|
119
|
+
const approval = frame.body && JSON.parse(frame.body).type === "tool-approval-request";
|
|
120
|
+
const excluded = approval ? [] : [...this.connections.values()].filter((session) => session.source.requestId === frame.id).map((session) => session.connection.id);
|
|
286
121
|
this.host.broadcast(JSON.stringify({ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, ...frame }), excluded);
|
|
287
122
|
}
|
|
288
123
|
}
|