@springbrand/agent-runtime 0.1.0
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 +28 -0
- package/src/db/approval.repo.ts +291 -0
- package/src/db/ext-context.repo.ts +34 -0
- package/src/db/index.ts +83 -0
- package/src/db/message-ui.repo.ts +39 -0
- package/src/db/milestone.repo.ts +96 -0
- package/src/db/runtime-event-outbox.repo.ts +89 -0
- package/src/db/schema.ts +164 -0
- package/src/db/settlement.repo.ts +104 -0
- package/src/db/steer.repo.ts +73 -0
- package/src/db/submission.repo.ts +323 -0
- package/src/index.ts +133 -0
- package/src/kernel/approval-lifecycle.ts +552 -0
- package/src/kernel/bindings.ts +898 -0
- package/src/kernel/degradation.ts +15 -0
- package/src/kernel/extensions.ts +108 -0
- package/src/kernel/profile.ts +116 -0
- package/src/kernel/public-contracts.ts +17 -0
- package/src/kernel/receipts.ts +124 -0
- package/src/kernel/recoverable-chat-agent.ts +899 -0
- package/src/kernel/state.ts +76 -0
- package/src/kernel/submission-lifecycle.ts +600 -0
- package/src/layers/context/budget/gate.ts +88 -0
- package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
- package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
- package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
- package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
- package/src/layers/orchestration/temporary-agent/core.ts +152 -0
- package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
- package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
- package/src/lib/artifacts.ts +54 -0
- package/src/lib/egress.ts +44 -0
- package/src/lib/execution-level.ts +27 -0
- package/src/lib/extension-name.ts +18 -0
- package/src/lib/host-actions.ts +57 -0
- package/src/lib/mcp.ts +86 -0
- package/src/lib/model-catalog.ts +7 -0
- package/src/lib/prompt.ts +139 -0
- package/src/lib/telemetry-dev.ts +44 -0
- package/src/pi/assembly/context.ts +510 -0
- package/src/pi/assembly/extensions.ts +661 -0
- package/src/pi/assembly/index.ts +19 -0
- package/src/pi/assembly/snapshot.ts +200 -0
- package/src/pi/message/contract.ts +8 -0
- package/src/pi/message/conversion.ts +73 -0
- package/src/pi/message/index.ts +3 -0
- package/src/pi/message/projection.ts +604 -0
- package/src/pi/runtime-adapter/assembly.ts +552 -0
- package/src/pi/runtime-adapter/execution.ts +683 -0
- package/src/pi/runtime-adapter/index.ts +232 -0
- package/src/pi/runtime-adapter/models.ts +243 -0
- package/src/pi/runtime-adapter/recovery.ts +805 -0
- package/src/pi/runtime-adapter/transcript.ts +825 -0
- package/src/pi/session/index.ts +24 -0
- package/src/pi/session/storage.ts +353 -0
- package/src/pi/tool/ai-adapter.ts +100 -0
- package/src/pi/tool/base.ts +110 -0
- package/src/pi/tool/compiler.ts +444 -0
- package/src/pi/tool/core-host.ts +48 -0
- package/src/pi/tool/core.ts +251 -0
- package/src/pi/tool/index.ts +32 -0
- package/src/pi/tool/mcp.ts +319 -0
- package/src/pi/tool/schedule.ts +198 -0
- package/src/pi/tool/skill.ts +455 -0
- package/src/pi/tool/subagent.ts +148 -0
- package/src/pi/tool/web-search/api.ts +1292 -0
- package/src/pi/tool/web-search/index.ts +2 -0
- package/src/pi/tool/web-search/web-search.ts +127 -0
- package/src/pi/tool/workspace-sandbox.ts +664 -0
- package/src/pi/turn/approval.ts +181 -0
- package/src/pi/turn/index.ts +62 -0
- package/src/pi/turn/tool-recovery.ts +792 -0
- package/src/plugins.ts +1024 -0
- package/src/runtime-agent.ts +654 -0
- package/src/runtime.ts +2880 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import type { AgentEvent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEvent,
|
|
5
|
+
ToolCall,
|
|
6
|
+
ToolResultMessage,
|
|
7
|
+
} from "@earendil-works/pi-ai";
|
|
8
|
+
import type {
|
|
9
|
+
DynamicToolUIPart,
|
|
10
|
+
FinishReason,
|
|
11
|
+
UIMessage,
|
|
12
|
+
UIMessageChunk,
|
|
13
|
+
} from "ai";
|
|
14
|
+
import { USER_STOP_REASON } from "../../kernel/receipts";
|
|
15
|
+
|
|
16
|
+
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
17
|
+
const MAX_OUTPUT_PREVIEW = 16 * 1024;
|
|
18
|
+
|
|
19
|
+
export interface PiToolApprovalView {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly approved?: boolean;
|
|
22
|
+
readonly reason?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeJson(value: unknown): unknown {
|
|
30
|
+
const seen = new WeakSet<object>();
|
|
31
|
+
let body: string | undefined;
|
|
32
|
+
try {
|
|
33
|
+
body = JSON.stringify(value, (_key, item: unknown) => {
|
|
34
|
+
if (typeof item === "bigint") return item.toString();
|
|
35
|
+
if (
|
|
36
|
+
item === undefined ||
|
|
37
|
+
typeof item === "function" ||
|
|
38
|
+
typeof item === "symbol"
|
|
39
|
+
) return null;
|
|
40
|
+
if (item instanceof Error) {
|
|
41
|
+
return { name: item.name, message: item.message };
|
|
42
|
+
}
|
|
43
|
+
if (item !== null && typeof item === "object") {
|
|
44
|
+
if (seen.has(item)) return "[Circular]";
|
|
45
|
+
seen.add(item);
|
|
46
|
+
}
|
|
47
|
+
return item;
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
serializationError:
|
|
52
|
+
error instanceof Error ? error.message : String(error),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (body === undefined) return null;
|
|
56
|
+
const bytes = new TextEncoder().encode(body).byteLength;
|
|
57
|
+
if (bytes > MAX_OUTPUT_BYTES) {
|
|
58
|
+
return {
|
|
59
|
+
truncated: true,
|
|
60
|
+
originalBytes: bytes,
|
|
61
|
+
preview: body.slice(0, MAX_OUTPUT_PREVIEW),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(body) as unknown;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function safeInput(value: unknown): Record<string, unknown> {
|
|
72
|
+
const safe = safeJson(value);
|
|
73
|
+
return isRecord(safe) ? safe : {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function contentText(result: unknown): string | undefined {
|
|
77
|
+
if (!isRecord(result) || !Array.isArray(result.content)) return undefined;
|
|
78
|
+
const text = result.content.flatMap((part) =>
|
|
79
|
+
isRecord(part) && part.type === "text" && typeof part.text === "string"
|
|
80
|
+
? [part.text]
|
|
81
|
+
: []
|
|
82
|
+
).join("");
|
|
83
|
+
return text || undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toolOutput(result: unknown): unknown {
|
|
87
|
+
if (!isRecord(result)) return safeJson(result);
|
|
88
|
+
if (result.details !== undefined) return safeJson(result.details);
|
|
89
|
+
return safeJson(contentText(result) ?? result.content ?? result);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function errorText(result: unknown): string {
|
|
93
|
+
const text = contentText(result);
|
|
94
|
+
if (text) return text;
|
|
95
|
+
const output = toolOutput(result);
|
|
96
|
+
if (typeof output === "string") return output;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.stringify(output ?? "Tool execution failed");
|
|
99
|
+
} catch {
|
|
100
|
+
return "Tool execution failed";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toolCallAt(
|
|
105
|
+
update: AssistantMessageEvent,
|
|
106
|
+
): Partial<ToolCall> | undefined {
|
|
107
|
+
if (!("contentIndex" in update) || !("partial" in update)) return undefined;
|
|
108
|
+
const part = update.partial.content[update.contentIndex];
|
|
109
|
+
return part?.type === "toolCall" ? part : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function finishReason(reason: string): FinishReason {
|
|
113
|
+
switch (reason) {
|
|
114
|
+
case "stop":
|
|
115
|
+
case "length":
|
|
116
|
+
return reason;
|
|
117
|
+
case "toolUse":
|
|
118
|
+
return "tool-calls";
|
|
119
|
+
case "error":
|
|
120
|
+
return "error";
|
|
121
|
+
default:
|
|
122
|
+
return "other";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface PendingToolInput {
|
|
127
|
+
toolName?: string;
|
|
128
|
+
deltas: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface StreamingToolInput extends PendingToolInput {
|
|
132
|
+
started: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Stateful Pi AgentEvent to official AI SDK 7 UIMessageChunk encoder. */
|
|
136
|
+
export class PiChunkEncoder {
|
|
137
|
+
private readonly messageId: string;
|
|
138
|
+
private readonly startedAt?: number;
|
|
139
|
+
private started = false;
|
|
140
|
+
private finished = false;
|
|
141
|
+
private firstTurnStarted = false;
|
|
142
|
+
private stepIndex = 0;
|
|
143
|
+
private readonly activeText = new Set<number>();
|
|
144
|
+
private readonly activeReasoning = new Set<number>();
|
|
145
|
+
private readonly pendingToolByIndex = new Map<number, PendingToolInput>();
|
|
146
|
+
private readonly toolCallIdByIndex = new Map<number, string>();
|
|
147
|
+
private readonly streamingToolById = new Map<string, StreamingToolInput>();
|
|
148
|
+
private readonly toolInputs = new Map<
|
|
149
|
+
string,
|
|
150
|
+
{ toolName: string; input: Record<string, unknown> }
|
|
151
|
+
>();
|
|
152
|
+
|
|
153
|
+
constructor(options: { messageId: string; startedAt?: number }) {
|
|
154
|
+
this.messageId = options.messageId;
|
|
155
|
+
this.startedAt = options.startedAt;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
start(): UIMessageChunk[] {
|
|
159
|
+
if (this.started) return [];
|
|
160
|
+
this.started = true;
|
|
161
|
+
this.firstTurnStarted = true;
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
type: "start",
|
|
165
|
+
messageId: this.messageId,
|
|
166
|
+
...(this.startedAt === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: {
|
|
169
|
+
messageMetadata: {
|
|
170
|
+
createdAt: this.startedAt,
|
|
171
|
+
turnStatus: "running",
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
{ type: "start-step" },
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
toolInput(
|
|
180
|
+
toolCallId: string,
|
|
181
|
+
toolName: string,
|
|
182
|
+
input: unknown,
|
|
183
|
+
): UIMessageChunk[] {
|
|
184
|
+
const safe = safeInput(input);
|
|
185
|
+
this.toolInputs.set(toolCallId, { toolName, input: safe });
|
|
186
|
+
return [{
|
|
187
|
+
type: "tool-input-available",
|
|
188
|
+
toolCallId,
|
|
189
|
+
toolName,
|
|
190
|
+
input: safe,
|
|
191
|
+
dynamic: true,
|
|
192
|
+
}];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
approvalRequested(
|
|
196
|
+
toolCallId: string,
|
|
197
|
+
approvalId: string,
|
|
198
|
+
): UIMessageChunk[] {
|
|
199
|
+
return [{ type: "tool-approval-request", toolCallId, approvalId }];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
approvalResponded(
|
|
203
|
+
toolCallId: string,
|
|
204
|
+
approvalId: string,
|
|
205
|
+
approved: boolean,
|
|
206
|
+
reason?: string,
|
|
207
|
+
): UIMessageChunk[] {
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
type: "tool-approval-response",
|
|
211
|
+
approvalId,
|
|
212
|
+
approved,
|
|
213
|
+
...(reason ? { reason } : {}),
|
|
214
|
+
},
|
|
215
|
+
...(approved
|
|
216
|
+
? []
|
|
217
|
+
: [{ type: "tool-output-denied", toolCallId } as const]),
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
encode(event: AgentEvent): UIMessageChunk[] {
|
|
222
|
+
if (this.finished) return [];
|
|
223
|
+
switch (event.type) {
|
|
224
|
+
case "agent_start":
|
|
225
|
+
return this.start();
|
|
226
|
+
case "turn_start":
|
|
227
|
+
if (!this.started) return this.start();
|
|
228
|
+
if (this.firstTurnStarted) {
|
|
229
|
+
this.firstTurnStarted = false;
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
this.stepIndex += 1;
|
|
233
|
+
return [{ type: "start-step" }];
|
|
234
|
+
case "message_update":
|
|
235
|
+
this.firstTurnStarted = false;
|
|
236
|
+
return this.encodeMessageUpdate(event.assistantMessageEvent);
|
|
237
|
+
case "tool_execution_start":
|
|
238
|
+
return this.toolInputs.has(event.toolCallId)
|
|
239
|
+
? []
|
|
240
|
+
: this.toolInput(event.toolCallId, event.toolName, event.args);
|
|
241
|
+
case "tool_execution_update": {
|
|
242
|
+
const chunks = this.toolInputs.has(event.toolCallId)
|
|
243
|
+
? []
|
|
244
|
+
: this.toolInput(event.toolCallId, event.toolName, event.args);
|
|
245
|
+
chunks.push({
|
|
246
|
+
type: "tool-output-available",
|
|
247
|
+
toolCallId: event.toolCallId,
|
|
248
|
+
output: toolOutput(event.partialResult),
|
|
249
|
+
preliminary: true,
|
|
250
|
+
dynamic: true,
|
|
251
|
+
});
|
|
252
|
+
return chunks;
|
|
253
|
+
}
|
|
254
|
+
case "tool_execution_end": {
|
|
255
|
+
const chunks = this.toolInputs.has(event.toolCallId)
|
|
256
|
+
? []
|
|
257
|
+
: this.toolInput(event.toolCallId, event.toolName, {});
|
|
258
|
+
chunks.push(event.isError
|
|
259
|
+
? {
|
|
260
|
+
type: "tool-output-error",
|
|
261
|
+
toolCallId: event.toolCallId,
|
|
262
|
+
errorText: errorText(event.result),
|
|
263
|
+
dynamic: true,
|
|
264
|
+
}
|
|
265
|
+
: {
|
|
266
|
+
type: "tool-output-available",
|
|
267
|
+
toolCallId: event.toolCallId,
|
|
268
|
+
output: toolOutput(event.result),
|
|
269
|
+
preliminary: false,
|
|
270
|
+
dynamic: true,
|
|
271
|
+
});
|
|
272
|
+
return chunks;
|
|
273
|
+
}
|
|
274
|
+
case "message_end":
|
|
275
|
+
this.firstTurnStarted = false;
|
|
276
|
+
return event.message.role === "assistant"
|
|
277
|
+
? this.endAssistant(event.message)
|
|
278
|
+
: [];
|
|
279
|
+
case "agent_end":
|
|
280
|
+
case "turn_end":
|
|
281
|
+
case "message_start":
|
|
282
|
+
return [];
|
|
283
|
+
default:
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Unsupported Pi AgentEvent: ${String((event as { type?: unknown }).type)}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private encodeMessageUpdate(update: AssistantMessageEvent): UIMessageChunk[] {
|
|
291
|
+
switch (update.type) {
|
|
292
|
+
case "text_start":
|
|
293
|
+
if (this.activeText.has(update.contentIndex)) return [];
|
|
294
|
+
this.activeText.add(update.contentIndex);
|
|
295
|
+
return [{ type: "text-start", id: this.partId("text", update.contentIndex) }];
|
|
296
|
+
case "text_delta":
|
|
297
|
+
return [
|
|
298
|
+
...this.ensureText(update.contentIndex),
|
|
299
|
+
{ type: "text-delta", id: this.partId("text", update.contentIndex), delta: update.delta },
|
|
300
|
+
];
|
|
301
|
+
case "text_end":
|
|
302
|
+
if (!this.activeText.delete(update.contentIndex)) return [];
|
|
303
|
+
return [{ type: "text-end", id: this.partId("text", update.contentIndex) }];
|
|
304
|
+
case "thinking_start":
|
|
305
|
+
if (this.activeReasoning.has(update.contentIndex)) return [];
|
|
306
|
+
this.activeReasoning.add(update.contentIndex);
|
|
307
|
+
return [{ type: "reasoning-start", id: this.partId("reasoning", update.contentIndex) }];
|
|
308
|
+
case "thinking_delta":
|
|
309
|
+
return [
|
|
310
|
+
...this.ensureReasoning(update.contentIndex),
|
|
311
|
+
{ type: "reasoning-delta", id: this.partId("reasoning", update.contentIndex), delta: update.delta },
|
|
312
|
+
];
|
|
313
|
+
case "thinking_end":
|
|
314
|
+
if (!this.activeReasoning.delete(update.contentIndex)) return [];
|
|
315
|
+
return [{ type: "reasoning-end", id: this.partId("reasoning", update.contentIndex) }];
|
|
316
|
+
case "toolcall_start":
|
|
317
|
+
case "toolcall_delta":
|
|
318
|
+
case "toolcall_end":
|
|
319
|
+
return this.encodeToolInput(update);
|
|
320
|
+
case "error":
|
|
321
|
+
return [{ type: "error", errorText: update.error.errorMessage ?? update.reason }];
|
|
322
|
+
default:
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private encodeToolInput(
|
|
328
|
+
update: Extract<
|
|
329
|
+
AssistantMessageEvent,
|
|
330
|
+
{ type: "toolcall_start" | "toolcall_delta" | "toolcall_end" }
|
|
331
|
+
>,
|
|
332
|
+
): UIMessageChunk[] {
|
|
333
|
+
const index = update.contentIndex;
|
|
334
|
+
if (update.type === "toolcall_start") {
|
|
335
|
+
this.pendingToolByIndex.set(index, { deltas: [] });
|
|
336
|
+
this.toolCallIdByIndex.delete(index);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const partial = toolCallAt(update);
|
|
340
|
+
const observedToolCallId = update.type === "toolcall_end"
|
|
341
|
+
? update.toolCall.id
|
|
342
|
+
: partial?.id;
|
|
343
|
+
const previousToolCallId = this.toolCallIdByIndex.get(index);
|
|
344
|
+
let pending = this.pendingToolByIndex.get(index) ?? { deltas: [] };
|
|
345
|
+
if (
|
|
346
|
+
observedToolCallId &&
|
|
347
|
+
previousToolCallId &&
|
|
348
|
+
observedToolCallId !== previousToolCallId
|
|
349
|
+
) {
|
|
350
|
+
pending = { deltas: [] };
|
|
351
|
+
}
|
|
352
|
+
const toolName = update.type === "toolcall_end"
|
|
353
|
+
? update.toolCall.name
|
|
354
|
+
: partial?.name;
|
|
355
|
+
if (toolName) pending.toolName = toolName;
|
|
356
|
+
if (update.type === "toolcall_delta") pending.deltas.push(update.delta);
|
|
357
|
+
this.pendingToolByIndex.set(index, pending);
|
|
358
|
+
|
|
359
|
+
const toolCallId = observedToolCallId ?? previousToolCallId;
|
|
360
|
+
if (!toolCallId) return [];
|
|
361
|
+
this.toolCallIdByIndex.set(index, toolCallId);
|
|
362
|
+
|
|
363
|
+
const streaming = this.streamingToolById.get(toolCallId) ?? {
|
|
364
|
+
deltas: [],
|
|
365
|
+
started: false,
|
|
366
|
+
};
|
|
367
|
+
if (pending.toolName) streaming.toolName = pending.toolName;
|
|
368
|
+
streaming.deltas.push(...pending.deltas);
|
|
369
|
+
pending.deltas.length = 0;
|
|
370
|
+
this.streamingToolById.set(toolCallId, streaming);
|
|
371
|
+
if (!streaming.toolName) return [];
|
|
372
|
+
|
|
373
|
+
const chunks: UIMessageChunk[] = [];
|
|
374
|
+
const completedOnly = update.type === "toolcall_end" &&
|
|
375
|
+
!streaming.started &&
|
|
376
|
+
streaming.deltas.length === 0;
|
|
377
|
+
if (!streaming.started && !completedOnly) {
|
|
378
|
+
streaming.started = true;
|
|
379
|
+
chunks.push({
|
|
380
|
+
type: "tool-input-start",
|
|
381
|
+
toolCallId,
|
|
382
|
+
toolName: streaming.toolName,
|
|
383
|
+
dynamic: true,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (streaming.started) {
|
|
387
|
+
for (const delta of streaming.deltas) {
|
|
388
|
+
chunks.push({
|
|
389
|
+
type: "tool-input-delta",
|
|
390
|
+
toolCallId,
|
|
391
|
+
inputTextDelta: delta,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
streaming.deltas.length = 0;
|
|
395
|
+
}
|
|
396
|
+
if (update.type === "toolcall_end") {
|
|
397
|
+
const input = safeInput(update.toolCall.arguments);
|
|
398
|
+
this.toolInputs.set(toolCallId, {
|
|
399
|
+
toolName: update.toolCall.name,
|
|
400
|
+
input,
|
|
401
|
+
});
|
|
402
|
+
chunks.push({
|
|
403
|
+
type: "tool-input-available",
|
|
404
|
+
toolCallId,
|
|
405
|
+
toolName: update.toolCall.name,
|
|
406
|
+
input,
|
|
407
|
+
dynamic: true,
|
|
408
|
+
});
|
|
409
|
+
this.pendingToolByIndex.delete(index);
|
|
410
|
+
this.toolCallIdByIndex.delete(index);
|
|
411
|
+
}
|
|
412
|
+
return chunks;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private endAssistant(message: AssistantMessage): UIMessageChunk[] {
|
|
416
|
+
this.pendingToolByIndex.clear();
|
|
417
|
+
this.toolCallIdByIndex.clear();
|
|
418
|
+
const chunks = this.closeOpenParts();
|
|
419
|
+
if (message.stopReason === "toolUse") {
|
|
420
|
+
chunks.push({ type: "finish-step" });
|
|
421
|
+
return chunks;
|
|
422
|
+
}
|
|
423
|
+
this.finished = true;
|
|
424
|
+
if (this.startedAt !== undefined) {
|
|
425
|
+
const turnStatus = message.stopReason === "aborted"
|
|
426
|
+
? "aborted"
|
|
427
|
+
: message.stopReason === "error"
|
|
428
|
+
? "error"
|
|
429
|
+
: "completed";
|
|
430
|
+
chunks.push({
|
|
431
|
+
type: "message-metadata",
|
|
432
|
+
messageMetadata: {
|
|
433
|
+
createdAt: this.startedAt,
|
|
434
|
+
completedAt: message.timestamp,
|
|
435
|
+
turnDurationMs: Math.max(0, message.timestamp - this.startedAt),
|
|
436
|
+
turnStatus,
|
|
437
|
+
...(message.errorMessage &&
|
|
438
|
+
(message.stopReason !== "aborted" ||
|
|
439
|
+
message.errorMessage !== USER_STOP_REASON)
|
|
440
|
+
? { error: message.errorMessage }
|
|
441
|
+
: {}),
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (message.stopReason === "aborted") {
|
|
446
|
+
chunks.push({ type: "abort", reason: message.errorMessage });
|
|
447
|
+
return chunks;
|
|
448
|
+
}
|
|
449
|
+
if (message.stopReason === "error") {
|
|
450
|
+
chunks.push({
|
|
451
|
+
type: "error",
|
|
452
|
+
errorText: message.errorMessage ?? "Pi turn failed",
|
|
453
|
+
});
|
|
454
|
+
return chunks;
|
|
455
|
+
}
|
|
456
|
+
chunks.push({ type: "finish-step" });
|
|
457
|
+
chunks.push({
|
|
458
|
+
type: "finish",
|
|
459
|
+
finishReason: finishReason(message.stopReason),
|
|
460
|
+
});
|
|
461
|
+
return chunks;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private closeOpenParts(): UIMessageChunk[] {
|
|
465
|
+
const chunks: UIMessageChunk[] = [];
|
|
466
|
+
for (const index of this.activeText) {
|
|
467
|
+
chunks.push({ type: "text-end", id: this.partId("text", index) });
|
|
468
|
+
}
|
|
469
|
+
for (const index of this.activeReasoning) {
|
|
470
|
+
chunks.push({ type: "reasoning-end", id: this.partId("reasoning", index) });
|
|
471
|
+
}
|
|
472
|
+
this.activeText.clear();
|
|
473
|
+
this.activeReasoning.clear();
|
|
474
|
+
return chunks;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private ensureText(contentIndex: number): UIMessageChunk[] {
|
|
478
|
+
if (this.activeText.has(contentIndex)) return [];
|
|
479
|
+
this.activeText.add(contentIndex);
|
|
480
|
+
return [{ type: "text-start", id: this.partId("text", contentIndex) }];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private ensureReasoning(contentIndex: number): UIMessageChunk[] {
|
|
484
|
+
if (this.activeReasoning.has(contentIndex)) return [];
|
|
485
|
+
this.activeReasoning.add(contentIndex);
|
|
486
|
+
return [{ type: "reasoning-start", id: this.partId("reasoning", contentIndex) }];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private partId(kind: "text" | "reasoning", index: number): string {
|
|
490
|
+
return `${this.messageId}:${kind}:${this.stepIndex}:${index}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function projectedTool(
|
|
495
|
+
call: ToolCall,
|
|
496
|
+
approval?: PiToolApprovalView,
|
|
497
|
+
): DynamicToolUIPart {
|
|
498
|
+
const input = safeInput(call.arguments);
|
|
499
|
+
const base = {
|
|
500
|
+
type: "dynamic-tool" as const,
|
|
501
|
+
toolCallId: call.id,
|
|
502
|
+
toolName: call.name,
|
|
503
|
+
input,
|
|
504
|
+
};
|
|
505
|
+
if (!approval) return { ...base, state: "input-available" };
|
|
506
|
+
if (approval.approved === false) {
|
|
507
|
+
return {
|
|
508
|
+
...base,
|
|
509
|
+
state: "output-denied",
|
|
510
|
+
approval: {
|
|
511
|
+
id: approval.id,
|
|
512
|
+
approved: false,
|
|
513
|
+
...(approval.reason ? { reason: approval.reason } : {}),
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (approval.approved === true) {
|
|
518
|
+
return {
|
|
519
|
+
...base,
|
|
520
|
+
state: "approval-responded",
|
|
521
|
+
approval: {
|
|
522
|
+
id: approval.id,
|
|
523
|
+
approved: true,
|
|
524
|
+
...(approval.reason ? { reason: approval.reason } : {}),
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
...base,
|
|
530
|
+
state: "approval-requested",
|
|
531
|
+
approval: { id: approval.id },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Project one canonical assistant message without mutating model context. */
|
|
536
|
+
export function piAssistantToUIMessage(
|
|
537
|
+
message: AssistantMessage,
|
|
538
|
+
options: {
|
|
539
|
+
readonly id: string;
|
|
540
|
+
readonly metadata?: unknown;
|
|
541
|
+
readonly approvals?: Readonly<Record<string, PiToolApprovalView | undefined>>;
|
|
542
|
+
},
|
|
543
|
+
): UIMessage {
|
|
544
|
+
const parts = message.content.flatMap(
|
|
545
|
+
(content): Array<UIMessage["parts"][number]> => {
|
|
546
|
+
if (content.type === "text") {
|
|
547
|
+
return [{ type: "text", text: content.text, state: "done" }];
|
|
548
|
+
}
|
|
549
|
+
if (content.type === "thinking") {
|
|
550
|
+
return [{ type: "reasoning", text: content.thinking, state: "done" }];
|
|
551
|
+
}
|
|
552
|
+
if (content.type === "toolCall") {
|
|
553
|
+
return [projectedTool(content, options.approvals?.[content.id])];
|
|
554
|
+
}
|
|
555
|
+
return [];
|
|
556
|
+
},
|
|
557
|
+
);
|
|
558
|
+
return {
|
|
559
|
+
id: options.id,
|
|
560
|
+
role: "assistant",
|
|
561
|
+
parts,
|
|
562
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Merge a canonical ToolResult onto its single dynamic Tool UI part. */
|
|
567
|
+
export function applyPiToolResult(
|
|
568
|
+
message: UIMessage,
|
|
569
|
+
result: ToolResultMessage,
|
|
570
|
+
): UIMessage {
|
|
571
|
+
return {
|
|
572
|
+
...message,
|
|
573
|
+
parts: message.parts.map((part): UIMessage["parts"][number] => {
|
|
574
|
+
if (
|
|
575
|
+
part.type !== "dynamic-tool" ||
|
|
576
|
+
part.toolCallId !== result.toolCallId ||
|
|
577
|
+
part.state === "output-denied"
|
|
578
|
+
) return part;
|
|
579
|
+
const approval = "approval" in part && part.approval?.approved === true
|
|
580
|
+
? { ...part.approval, approved: true as const }
|
|
581
|
+
: undefined;
|
|
582
|
+
return result.isError
|
|
583
|
+
? {
|
|
584
|
+
type: "dynamic-tool",
|
|
585
|
+
toolCallId: part.toolCallId,
|
|
586
|
+
toolName: part.toolName,
|
|
587
|
+
state: "output-error",
|
|
588
|
+
input: part.input,
|
|
589
|
+
errorText: errorText(result),
|
|
590
|
+
...(approval ? { approval } : {}),
|
|
591
|
+
}
|
|
592
|
+
: {
|
|
593
|
+
type: "dynamic-tool",
|
|
594
|
+
toolCallId: part.toolCallId,
|
|
595
|
+
toolName: part.toolName,
|
|
596
|
+
state: "output-available",
|
|
597
|
+
input: part.input,
|
|
598
|
+
output: toolOutput({ content: result.content, details: result.details }),
|
|
599
|
+
preliminary: false,
|
|
600
|
+
...(approval ? { approval } : {}),
|
|
601
|
+
};
|
|
602
|
+
}),
|
|
603
|
+
};
|
|
604
|
+
}
|