@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
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { UIMessage } from "ai";
|
|
3
|
+
import type { RequestedCapability } from "./contract";
|
|
4
|
+
|
|
5
|
+
export function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
|
|
6
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const record = metadata as Record<string, unknown>;
|
|
10
|
+
if (!("requestedCapabilities" in record)) return [];
|
|
11
|
+
if (!Array.isArray(record.requestedCapabilities)) {
|
|
12
|
+
throw new Error("requestedCapabilities must be an array");
|
|
13
|
+
}
|
|
14
|
+
const result: RequestedCapability[] = [];
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (const value of record.requestedCapabilities) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
18
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
19
|
+
}
|
|
20
|
+
const capability = value as Record<string, unknown>;
|
|
21
|
+
if (
|
|
22
|
+
(capability.kind !== "skill" && capability.kind !== "tool" && capability.kind !== "plan") ||
|
|
23
|
+
typeof capability.name !== "string" ||
|
|
24
|
+
!capability.name.trim() ||
|
|
25
|
+
typeof capability.label !== "string" ||
|
|
26
|
+
!capability.label.trim()
|
|
27
|
+
) {
|
|
28
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
29
|
+
}
|
|
30
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
31
|
+
if (seen.has(key)) continue;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
result.push({ kind: capability.kind, name: capability.name, label: capability.label });
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve user-selected capabilities against the loaded Runtime before model input conversion. */
|
|
39
|
+
export function normalizeUIUserInput(
|
|
40
|
+
message: UIMessage & { role: "user" },
|
|
41
|
+
installed: {
|
|
42
|
+
readonly hasSkill: (name: string) => boolean;
|
|
43
|
+
readonly hasTool: (name: string) => boolean;
|
|
44
|
+
},
|
|
45
|
+
): UserMessage {
|
|
46
|
+
const context: string[] = [];
|
|
47
|
+
for (const capability of requestedCapabilitiesOf(message.metadata)) {
|
|
48
|
+
if (capability.kind === "skill") {
|
|
49
|
+
if (!installed.hasSkill(capability.name)) {
|
|
50
|
+
throw new Error(`Requested Skill is not installed: ${capability.name}`);
|
|
51
|
+
}
|
|
52
|
+
context.push(
|
|
53
|
+
`requested capability: skill/${capability.name}`,
|
|
54
|
+
`required action: call activate_skill for "${capability.name}" before handling the task`,
|
|
55
|
+
);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (capability.kind === "tool") {
|
|
59
|
+
if (!installed.hasTool(capability.name)) {
|
|
60
|
+
throw new Error(`Requested Tool is not installed: ${capability.name}`);
|
|
61
|
+
}
|
|
62
|
+
context.push(
|
|
63
|
+
`requested capability: tool/${capability.name}`,
|
|
64
|
+
`required action: call tool "${capability.name}" when handling the task`,
|
|
65
|
+
);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (capability.name !== "plan") {
|
|
69
|
+
throw new Error(`Unknown plan capability: ${capability.name}`);
|
|
70
|
+
}
|
|
71
|
+
context.push(
|
|
72
|
+
"requested capability: plan/plan",
|
|
73
|
+
"required action: call update_plan with the complete plan before other work, then wait for user confirmation before execution",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return uiUserMessageToPi(message, context.join("\n"));
|
|
77
|
+
}
|
|
3
78
|
|
|
4
79
|
export interface UIUserSidecar {
|
|
5
80
|
readonly metadata?: unknown;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message, UserMessage } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { RuntimeModelInputModality } from "../../
|
|
2
|
+
import type { RuntimeModelInputModality } from "../../config/profile";
|
|
3
3
|
|
|
4
4
|
const UNSUPPORTED_FILE_PLACEHOLDER =
|
|
5
5
|
"(file omitted: model API does not support this file type)";
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
UIMessage,
|
|
12
12
|
UIMessageChunk,
|
|
13
13
|
} from "ai";
|
|
14
|
-
import { USER_STOP_REASON } from "../../
|
|
14
|
+
import { USER_STOP_REASON } from "../../submission/contracts";
|
|
15
15
|
|
|
16
16
|
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
17
17
|
const MAX_OUTPUT_PREVIEW = 16 * 1024;
|
|
@@ -75,6 +75,35 @@ function safeInput(value: unknown): Record<string, unknown> {
|
|
|
75
75
|
return isRecord(safe) ? safe : {};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export type ToolInputPreparer = (
|
|
79
|
+
toolName: string,
|
|
80
|
+
input: unknown,
|
|
81
|
+
) => unknown;
|
|
82
|
+
|
|
83
|
+
function safeToolInput(
|
|
84
|
+
toolName: string,
|
|
85
|
+
value: unknown,
|
|
86
|
+
prepare?: ToolInputPreparer,
|
|
87
|
+
): Record<string, unknown> {
|
|
88
|
+
if (prepare) {
|
|
89
|
+
try {
|
|
90
|
+
value = prepare(toolName, value);
|
|
91
|
+
} catch {
|
|
92
|
+
// Preserve invalid provider output for the normal Tool validation path.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const input = safeInput(value);
|
|
96
|
+
if (toolName === "ask_user" && typeof input.questions === "string") {
|
|
97
|
+
try {
|
|
98
|
+
const questions: unknown = JSON.parse(input.questions);
|
|
99
|
+
if (Array.isArray(questions)) input.questions = questions;
|
|
100
|
+
} catch {
|
|
101
|
+
// Preserve malformed provider output for the normal unavailable state.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return input;
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
function contentText(result: unknown): string | undefined {
|
|
79
108
|
if (!isRecord(result) || !Array.isArray(result.content)) return undefined;
|
|
80
109
|
const text = result.content.flatMap((part) =>
|
|
@@ -174,6 +203,8 @@ export class PiChunkEncoder {
|
|
|
174
203
|
private readonly messageId: string;
|
|
175
204
|
private readonly startedAt?: number;
|
|
176
205
|
private readonly turnId?: string;
|
|
206
|
+
private readonly userMessageId?: string;
|
|
207
|
+
private readonly prepareToolInput?: ToolInputPreparer;
|
|
177
208
|
private started = false;
|
|
178
209
|
private finished = false;
|
|
179
210
|
private firstTurnStarted = false;
|
|
@@ -192,10 +223,14 @@ export class PiChunkEncoder {
|
|
|
192
223
|
messageId: string;
|
|
193
224
|
startedAt?: number;
|
|
194
225
|
turnId?: string;
|
|
226
|
+
userMessageId?: string | null;
|
|
227
|
+
prepareToolInput?: ToolInputPreparer;
|
|
195
228
|
}) {
|
|
196
229
|
this.messageId = options.messageId;
|
|
197
230
|
this.startedAt = options.startedAt;
|
|
198
231
|
this.turnId = options.turnId;
|
|
232
|
+
this.userMessageId = options.userMessageId ?? undefined;
|
|
233
|
+
this.prepareToolInput = options.prepareToolInput;
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
start(): UIMessageChunk[] {
|
|
@@ -213,6 +248,7 @@ export class PiChunkEncoder {
|
|
|
213
248
|
createdAt: this.startedAt,
|
|
214
249
|
turnStatus: "running",
|
|
215
250
|
...(this.turnId ? { turnId: this.turnId } : {}),
|
|
251
|
+
...(this.userMessageId ? { userMessageId: this.userMessageId } : {}),
|
|
216
252
|
},
|
|
217
253
|
}),
|
|
218
254
|
},
|
|
@@ -220,12 +256,60 @@ export class PiChunkEncoder {
|
|
|
220
256
|
];
|
|
221
257
|
}
|
|
222
258
|
|
|
259
|
+
/** Seed this encoder and its open part IDs from one Pi-backed UI snapshot. */
|
|
260
|
+
restore(message: UIMessage, streaming: boolean): UIMessageChunk[] {
|
|
261
|
+
if (this.started) throw new Error("Cannot restore an already-started stream encoder");
|
|
262
|
+
this.started = true;
|
|
263
|
+
this.firstTurnStarted = message.parts.length === 0;
|
|
264
|
+
const chunks: UIMessageChunk[] = [{ type: "start", messageId: this.messageId,
|
|
265
|
+
...(message.metadata === undefined ? {} : { messageMetadata: message.metadata }) }];
|
|
266
|
+
let contentIndex = 0;
|
|
267
|
+
let hasStep = false;
|
|
268
|
+
let lastContentIndex = message.parts.length - 1;
|
|
269
|
+
while (lastContentIndex >= 0 && message.parts[lastContentIndex]?.type === "step-start") lastContentIndex -= 1;
|
|
270
|
+
for (const [partIndex, part] of message.parts.entries()) {
|
|
271
|
+
if (part.type === "step-start") {
|
|
272
|
+
if (hasStep) { chunks.push({ type: "finish-step" }); this.stepIndex += 1; }
|
|
273
|
+
chunks.push({ type: "start-step" });
|
|
274
|
+
hasStep = true;
|
|
275
|
+
contentIndex = 0;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!hasStep) { chunks.push({ type: "start-step" }); hasStep = true; }
|
|
279
|
+
if (part.type === "text" || part.type === "reasoning") {
|
|
280
|
+
const kind = part.type;
|
|
281
|
+
const id = this.partId(kind, contentIndex);
|
|
282
|
+
chunks.push({ type: `${kind}-start`, id });
|
|
283
|
+
if (part.text) chunks.push({ type: `${kind}-delta`, id, delta: part.text });
|
|
284
|
+
if (streaming && partIndex === lastContentIndex) {
|
|
285
|
+
(kind === "text" ? this.activeText : this.activeReasoning).add(contentIndex);
|
|
286
|
+
} else chunks.push({ type: `${kind}-end`, id });
|
|
287
|
+
} else if (part.type === "dynamic-tool") {
|
|
288
|
+
if (part.state === "input-streaming") {
|
|
289
|
+
chunks.push({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName, dynamic: true });
|
|
290
|
+
this.toolCallIdByIndex.set(contentIndex, part.toolCallId);
|
|
291
|
+
this.streamingToolById.set(part.toolCallId, { toolName: part.toolName, started: true, deltas: [] });
|
|
292
|
+
} else {
|
|
293
|
+
chunks.push(...this.toolInput(part.toolCallId, part.toolName, part.input));
|
|
294
|
+
if (part.state === "approval-requested") chunks.push(...this.approvalRequested(part.toolCallId, part.approval.id));
|
|
295
|
+
else if (part.state === "output-available") chunks.push({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output, preliminary: part.preliminary, dynamic: true });
|
|
296
|
+
else if (part.state === "output-error") chunks.push({ type: "tool-output-error", toolCallId: part.toolCallId, errorText: part.errorText, dynamic: true });
|
|
297
|
+
else if (part.state === "output-denied") chunks.push({ type: "tool-output-denied", toolCallId: part.toolCallId });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
contentIndex += 1;
|
|
301
|
+
}
|
|
302
|
+
if (!hasStep) chunks.push({ type: "start-step" });
|
|
303
|
+
else if (!streaming) chunks.push({ type: "finish-step" });
|
|
304
|
+
return chunks;
|
|
305
|
+
}
|
|
306
|
+
|
|
223
307
|
toolInput(
|
|
224
308
|
toolCallId: string,
|
|
225
309
|
toolName: string,
|
|
226
310
|
input: unknown,
|
|
227
311
|
): UIMessageChunk[] {
|
|
228
|
-
const safe =
|
|
312
|
+
const safe = safeToolInput(toolName, input, this.prepareToolInput);
|
|
229
313
|
this.toolInputs.set(toolCallId, { toolName, input: safe });
|
|
230
314
|
return [{
|
|
231
315
|
type: "tool-input-available",
|
|
@@ -262,7 +346,10 @@ export class PiChunkEncoder {
|
|
|
262
346
|
];
|
|
263
347
|
}
|
|
264
348
|
|
|
265
|
-
encode(
|
|
349
|
+
encode(
|
|
350
|
+
event: AgentEvent,
|
|
351
|
+
options: { readonly terminal?: boolean } = {},
|
|
352
|
+
): UIMessageChunk[] {
|
|
266
353
|
if (this.finished) return [];
|
|
267
354
|
switch (event.type) {
|
|
268
355
|
case "agent_start":
|
|
@@ -318,7 +405,7 @@ export class PiChunkEncoder {
|
|
|
318
405
|
case "message_end":
|
|
319
406
|
this.firstTurnStarted = false;
|
|
320
407
|
return event.message.role === "assistant"
|
|
321
|
-
? this.endAssistant(event.message)
|
|
408
|
+
? this.endAssistant(event.message, options.terminal !== false)
|
|
322
409
|
: [];
|
|
323
410
|
case "agent_end":
|
|
324
411
|
case "turn_end":
|
|
@@ -450,33 +537,36 @@ export class PiChunkEncoder {
|
|
|
450
537
|
streaming.deltas.length = 0;
|
|
451
538
|
}
|
|
452
539
|
if (update.type === "toolcall_end") {
|
|
453
|
-
|
|
454
|
-
this.toolInputs.set(toolCallId, {
|
|
455
|
-
toolName: update.toolCall.name,
|
|
456
|
-
input,
|
|
457
|
-
});
|
|
458
|
-
chunks.push({
|
|
459
|
-
type: "tool-input-available",
|
|
540
|
+
chunks.push(...this.toolInput(
|
|
460
541
|
toolCallId,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
});
|
|
542
|
+
update.toolCall.name,
|
|
543
|
+
update.toolCall.arguments,
|
|
544
|
+
));
|
|
465
545
|
this.pendingToolByIndex.delete(index);
|
|
466
546
|
this.toolCallIdByIndex.delete(index);
|
|
467
547
|
}
|
|
468
548
|
return chunks;
|
|
469
549
|
}
|
|
470
550
|
|
|
471
|
-
private endAssistant(message: AssistantMessage): UIMessageChunk[] {
|
|
551
|
+
private endAssistant(message: AssistantMessage, terminal = true): UIMessageChunk[] {
|
|
472
552
|
this.pendingToolByIndex.clear();
|
|
473
553
|
this.toolCallIdByIndex.clear();
|
|
474
554
|
const chunks = this.closeOpenParts();
|
|
475
|
-
if (message.stopReason === "toolUse") {
|
|
555
|
+
if (message.stopReason === "toolUse" || !terminal) {
|
|
476
556
|
chunks.push({ type: "finish-step" });
|
|
477
557
|
return chunks;
|
|
478
558
|
}
|
|
559
|
+
const outcome = classifyPiTerminalStopReason(message.stopReason);
|
|
560
|
+
if (outcome.outcome === "succeeded") chunks.push({ type: "finish-step" });
|
|
561
|
+
chunks.push(...this.finish(message));
|
|
562
|
+
return chunks;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Finalize the UI operation after its last model step has already closed. */
|
|
566
|
+
finish(message: Pick<AssistantMessage, "stopReason" | "timestamp" | "errorMessage">): UIMessageChunk[] {
|
|
567
|
+
if (this.finished) return [];
|
|
479
568
|
this.finished = true;
|
|
569
|
+
const chunks: UIMessageChunk[] = [];
|
|
480
570
|
const publicError = publicAssistantError(message.errorMessage);
|
|
481
571
|
const terminal = classifyPiTerminalStopReason(message.stopReason);
|
|
482
572
|
if (this.startedAt !== undefined) {
|
|
@@ -507,7 +597,6 @@ export class PiChunkEncoder {
|
|
|
507
597
|
});
|
|
508
598
|
return chunks;
|
|
509
599
|
}
|
|
510
|
-
chunks.push({ type: "finish-step" });
|
|
511
600
|
chunks.push({
|
|
512
601
|
type: "finish",
|
|
513
602
|
finishReason: terminal.finishReason ?? "other",
|
|
@@ -549,15 +638,7 @@ function projectedTool(
|
|
|
549
638
|
call: ToolCall,
|
|
550
639
|
approval?: PiToolApprovalView,
|
|
551
640
|
): DynamicToolUIPart {
|
|
552
|
-
const input =
|
|
553
|
-
if (call.name === "ask_user" && typeof input.questions === "string") {
|
|
554
|
-
try {
|
|
555
|
-
const questions: unknown = JSON.parse(input.questions);
|
|
556
|
-
if (Array.isArray(questions)) input.questions = questions;
|
|
557
|
-
} catch {
|
|
558
|
-
// Preserve malformed legacy input for the normal unavailable state.
|
|
559
|
-
}
|
|
560
|
-
}
|
|
641
|
+
const input = safeToolInput(call.name, call.arguments);
|
|
561
642
|
const base = {
|
|
562
643
|
type: "dynamic-tool" as const,
|
|
563
644
|
toolCallId: call.id,
|
|
@@ -601,15 +682,16 @@ export function piAssistantToUIMessage(
|
|
|
601
682
|
readonly id: string;
|
|
602
683
|
readonly metadata?: unknown;
|
|
603
684
|
readonly approvals?: Readonly<Record<string, PiToolApprovalView | undefined>>;
|
|
685
|
+
readonly state?: "streaming" | "done";
|
|
604
686
|
},
|
|
605
687
|
): UIMessage {
|
|
606
688
|
const parts = message.content.flatMap(
|
|
607
689
|
(content): Array<UIMessage["parts"][number]> => {
|
|
608
690
|
if (content.type === "text") {
|
|
609
|
-
return [{ type: "text", text: content.text, state: "done" }];
|
|
691
|
+
return [{ type: "text", text: content.text, state: options.state ?? "done" }];
|
|
610
692
|
}
|
|
611
693
|
if (content.type === "thinking") {
|
|
612
|
-
return [{ type: "reasoning", text: content.thinking, state: "done" }];
|
|
694
|
+
return [{ type: "reasoning", text: content.thinking, state: options.state ?? "done" }];
|
|
613
695
|
}
|
|
614
696
|
if (content.type === "toolCall") {
|
|
615
697
|
return [projectedTool(content, options.approvals?.[content.id])];
|
|
@@ -629,6 +711,7 @@ export function piAssistantToUIMessage(
|
|
|
629
711
|
export function applyPiToolResult(
|
|
630
712
|
message: UIMessage,
|
|
631
713
|
result: ToolResultMessage,
|
|
714
|
+
options: { readonly preliminary?: boolean } = {},
|
|
632
715
|
): UIMessage {
|
|
633
716
|
return {
|
|
634
717
|
...message,
|
|
@@ -658,7 +741,7 @@ export function applyPiToolResult(
|
|
|
658
741
|
state: "output-available",
|
|
659
742
|
input: part.input,
|
|
660
743
|
output: toolOutput({ content: result.content, details: result.details }),
|
|
661
|
-
preliminary: false,
|
|
744
|
+
preliminary: options.preliminary ?? false,
|
|
662
745
|
...(approval ? { approval } : {}),
|
|
663
746
|
};
|
|
664
747
|
}),
|
|
@@ -30,6 +30,10 @@ export class MessageUiRepository {
|
|
|
30
30
|
`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
delete(messageId: string): void {
|
|
34
|
+
this.sql`DELETE FROM pi_message_ui WHERE message_id = ${messageId}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
// 删除全部 Message UI 补充数据。
|
|
34
38
|
// Transcript.clear 在清空标准会话时调用,并由上层事务把两类数据一起提交。
|
|
35
39
|
// 该责任曾属于 RuntimeDatabase.clearAll,现在归 Transcript 所有;不能两处分别维护清理规则。
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ApprovalReceipt } from "../submission/contracts";
|
|
2
|
+
import type { StoredSubmission } from "../submission/repository";
|
|
3
|
+
import type { PendingApprovalSummary } from "../tools/runtime/approval/repository";
|
|
4
|
+
import type { PiCanonicalUserInput } from "./message/contract";
|
|
5
|
+
|
|
6
|
+
export type RuntimeLoadPhase = "config" | "assembly" | "mcp" | "pi";
|
|
7
|
+
|
|
8
|
+
export type RuntimeActivity = "idle" | "working" | "needs-input";
|
|
9
|
+
|
|
10
|
+
export interface RuntimeActivityProjection {
|
|
11
|
+
activity: RuntimeActivity;
|
|
12
|
+
/**
|
|
13
|
+
* 是否仍有 Agent Tool 子运行在执行。
|
|
14
|
+
*
|
|
15
|
+
* `activity` 三态互斥,表达不了「等人回应的同时后台还在跑」。Host 的列表需要
|
|
16
|
+
* 这两件事各自成立,所以它是一个独立维度,而不是第四种活动值。
|
|
17
|
+
*/
|
|
18
|
+
backgroundWork: boolean;
|
|
19
|
+
revision: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runtime 最近一次装载尝试的公开状态。
|
|
24
|
+
*
|
|
25
|
+
* `available` 与本次尝试分开表达:强制重载失败时,旧 Runtime 仍可能继续服务。
|
|
26
|
+
* 该状态由 Agents SDK 的 Agent state 同步给浏览器,不包含底层异常文本。
|
|
27
|
+
*/
|
|
28
|
+
export type RuntimeLoadState =
|
|
29
|
+
| {
|
|
30
|
+
status: "idle";
|
|
31
|
+
available: false;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
status: "loading";
|
|
35
|
+
phase: RuntimeLoadPhase;
|
|
36
|
+
available: boolean;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
updatedAt: number;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
status: "ready";
|
|
42
|
+
available: true;
|
|
43
|
+
startedAt: number;
|
|
44
|
+
completedAt: number;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
status: "error";
|
|
48
|
+
phase: RuntimeLoadPhase;
|
|
49
|
+
available: boolean;
|
|
50
|
+
startedAt: number;
|
|
51
|
+
failedAt: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface RuntimeQueuedSubmission {
|
|
55
|
+
submissionId: string;
|
|
56
|
+
messageId: string;
|
|
57
|
+
preview: string;
|
|
58
|
+
position: number;
|
|
59
|
+
createdAt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RuntimeTurnState {
|
|
63
|
+
activeSubmissionId?: string;
|
|
64
|
+
activeRequestId?: string;
|
|
65
|
+
activeMessageId?: string;
|
|
66
|
+
steerable: boolean;
|
|
67
|
+
hasPendingSteer: boolean;
|
|
68
|
+
queued: RuntimeQueuedSubmission[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 本 Session 当前是否拒绝普通用户输入。
|
|
73
|
+
*
|
|
74
|
+
* 定时 Run 未结束时它独占输入:宿主据此禁用输入框并显示原因。
|
|
75
|
+
* 服务端准入用同一事实拒绝消息,所以客户端状态与后端判定不会漂移。
|
|
76
|
+
*/
|
|
77
|
+
export interface RuntimeInputLock {
|
|
78
|
+
/** 稳定原因码,由宿主/UI 决定文案。 */
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 保存 Kernel 在当前会话里自己维护的审批状态。
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* `RuntimeAgent` 把它作为 Agent state 的形状,并在审批记录变化时读写。
|
|
87
|
+
*
|
|
88
|
+
* 它故意不保存用户、Agent 或 Session 等业务身份,避免 Runtime 越过 Host 的归属边界。
|
|
89
|
+
*
|
|
90
|
+
* `Runtime`、`Kernel` 与 `Host` 等核心术语见 `../index.ts`。
|
|
91
|
+
*/
|
|
92
|
+
export interface RuntimeState {
|
|
93
|
+
/** 当前实例的 Runtime 装载状态;旧状态记录没有该字段时等同 `idle`。 */
|
|
94
|
+
runtimeLoad?: RuntimeLoadState;
|
|
95
|
+
/** 供 Host 列表持久化的粗粒度活动状态;Submission 与 Approval 仍是执行真相。 */
|
|
96
|
+
activity?: RuntimeActivityProjection;
|
|
97
|
+
approvals?: ApprovalReceipt[];
|
|
98
|
+
turn?: RuntimeTurnState;
|
|
99
|
+
/** 有值时普通输入被锁定,直到锁定的 Submission 进入终态。 */
|
|
100
|
+
inputLock?: RuntimeInputLock;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type SubmissionProjection = Pick<
|
|
104
|
+
StoredSubmission,
|
|
105
|
+
"submissionId" | "requestId" | "assistantMessageId" | "userMessageId" |
|
|
106
|
+
"queuedInputJson" | "createdAt"
|
|
107
|
+
>;
|
|
108
|
+
|
|
109
|
+
/** Rebuild client-visible activity from current facts without retaining lifecycle state. */
|
|
110
|
+
export function projectRuntimeState(input: {
|
|
111
|
+
readonly approvals: readonly PendingApprovalSummary[];
|
|
112
|
+
readonly temporaryApprovals: readonly ApprovalReceipt[];
|
|
113
|
+
readonly running: SubmissionProjection | null | undefined;
|
|
114
|
+
readonly pending: readonly SubmissionProjection[];
|
|
115
|
+
readonly hasPendingApproval: (submissionId: string) => boolean;
|
|
116
|
+
readonly hasPendingInteraction: (submissionId: string) => boolean;
|
|
117
|
+
readonly hasPendingSteer: (submissionId: string) => boolean;
|
|
118
|
+
readonly hasRunningAgentTools: boolean;
|
|
119
|
+
/** A non-terminal scheduled Submission owns ordinary input until it ends. */
|
|
120
|
+
readonly hasUnfinishedScheduled?: boolean;
|
|
121
|
+
readonly previousState: RuntimeState;
|
|
122
|
+
}): { state: RuntimeState; changed: boolean } {
|
|
123
|
+
const approvals: ApprovalReceipt[] = [
|
|
124
|
+
...input.approvals.map((approval) => ({
|
|
125
|
+
executionId: approval.executionId,
|
|
126
|
+
source: approval.source as ApprovalReceipt["source"],
|
|
127
|
+
action: approval.toolName,
|
|
128
|
+
summary: approval.summary,
|
|
129
|
+
executionLevel: approval.executionLevel,
|
|
130
|
+
requiredExecutionLevel: approval.requiredExecutionLevel,
|
|
131
|
+
inputJson: approval.inputJson,
|
|
132
|
+
requestId: approval.requestId,
|
|
133
|
+
})),
|
|
134
|
+
...input.temporaryApprovals,
|
|
135
|
+
];
|
|
136
|
+
const current = input.running ?? input.pending[0];
|
|
137
|
+
const queued = (input.running ? input.pending : input.pending.slice(1)).map(
|
|
138
|
+
(submission, index): RuntimeQueuedSubmission => {
|
|
139
|
+
let preview = "Queued message";
|
|
140
|
+
if (submission.queuedInputJson) {
|
|
141
|
+
try {
|
|
142
|
+
const message = JSON.parse(submission.queuedInputJson) as PiCanonicalUserInput;
|
|
143
|
+
preview = (typeof message.content === "string" ? message.content :
|
|
144
|
+
message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join(" "))
|
|
145
|
+
.trim().slice(0, 160) || preview;
|
|
146
|
+
} catch {
|
|
147
|
+
// The queue pump reports corrupt payloads when it activates the Submission.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
submissionId: submission.submissionId,
|
|
152
|
+
messageId: submission.userMessageId ?? submission.submissionId,
|
|
153
|
+
preview,
|
|
154
|
+
position: index + 1,
|
|
155
|
+
createdAt: submission.createdAt,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
const turn: RuntimeTurnState = {
|
|
160
|
+
...(current ? {
|
|
161
|
+
activeSubmissionId: current.submissionId,
|
|
162
|
+
activeMessageId: current.assistantMessageId,
|
|
163
|
+
...(current.requestId ? { activeRequestId: current.requestId } : {}),
|
|
164
|
+
} : {}),
|
|
165
|
+
steerable: Boolean(current && !input.hasPendingApproval(current.submissionId)),
|
|
166
|
+
hasPendingSteer: Boolean(current && input.hasPendingSteer(current.submissionId)),
|
|
167
|
+
queued,
|
|
168
|
+
};
|
|
169
|
+
const activity: RuntimeActivity = approvals.length > 0 ||
|
|
170
|
+
(current && input.hasPendingInteraction(current.submissionId))
|
|
171
|
+
? "needs-input"
|
|
172
|
+
: current || queued.length > 0 ? "working" : "idle";
|
|
173
|
+
const previousActivity = input.previousState.activity;
|
|
174
|
+
const nextActivity: RuntimeActivityProjection = previousActivity?.activity === activity &&
|
|
175
|
+
previousActivity.backgroundWork === input.hasRunningAgentTools
|
|
176
|
+
? previousActivity
|
|
177
|
+
: {
|
|
178
|
+
activity,
|
|
179
|
+
backgroundWork: input.hasRunningAgentTools,
|
|
180
|
+
revision: (previousActivity?.revision ?? 0) + 1,
|
|
181
|
+
};
|
|
182
|
+
const inputLock: RuntimeInputLock | undefined = input.hasUnfinishedScheduled
|
|
183
|
+
? { reason: "scheduled-run" }
|
|
184
|
+
: undefined;
|
|
185
|
+
const changed = input.previousState.approvals === undefined ||
|
|
186
|
+
JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
|
|
187
|
+
JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
|
|
188
|
+
JSON.stringify(input.previousState.inputLock) !== JSON.stringify(inputLock) ||
|
|
189
|
+
nextActivity !== previousActivity;
|
|
190
|
+
return {
|
|
191
|
+
state: changed
|
|
192
|
+
? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
|
|
193
|
+
: input.previousState,
|
|
194
|
+
changed,
|
|
195
|
+
};
|
|
196
|
+
}
|