@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.
@@ -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
- return target.completeSimple.bind(models);
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 { Context } from "@earendil-works/pi-ai";
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
- /** Only descriptions and guidance may vary inside a pinned Submission. */
5
- export async function refreshModelGuidance(
6
- candidates: readonly PiToolCandidate[],
7
- context: Context,
8
- ): Promise<Context> {
9
- const guidance = new Map(await Promise.all(candidates
10
- .filter(candidate => candidate.modelGuidance && context.tools?.some(tool => tool.name === candidate.tool.name))
11
- .map(async candidate => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
12
- if (!guidance.size) return context;
13
- return {
14
- ...context,
15
- systemPrompt: [context.systemPrompt, ...new Set([...guidance.values()].map(item => item.systemPrompt).filter(Boolean))].filter(Boolean).join("\n\n"),
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
  }
@@ -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 { 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";
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
- lane: AgentLane;
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
- 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;
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
- activate(lane: AgentLane, submission: Omit<ActiveConversation, "lane">): void {
44
- this.active = { lane, ...submission };
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
- 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 } : {}) });
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 sendChunks(session: ResumeConnection, chunks: readonly UIMessageChunk[], replay = false): Promise<void> {
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
- 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
- }
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
- 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));
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
- 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
- });
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.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
- });
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
- 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 });
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
- /** 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 });
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.discard(session);
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
- // 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);
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
  }