@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.13
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 +4 -3
- package/src/adapter/cloudflare/index.ts +1 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
- package/src/adapter/cloudflare/subagent/definition.ts +60 -5
- package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +34 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +7 -7
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/db/index.ts +4 -0
- package/src/db/runtime-event-outbox.repo.ts +34 -1
- package/src/db/schema.ts +42 -0
- package/src/db/submission-admission.repo.ts +127 -0
- package/src/db/submission.repo.ts +54 -3
- package/src/index.ts +3 -0
- package/src/kernel/bindings.ts +52 -0
- package/src/kernel/durable-lifecycle.ts +100 -0
- package/src/kernel/public-contracts.ts +11 -0
- package/src/kernel/receipts.ts +1 -0
- package/src/kernel/recoverable-chat-agent.ts +0 -19
- package/src/kernel/subagent-runtime.ts +137 -0
- package/src/kernel/submission-authority.ts +114 -0
- package/src/kernel/submission-lifecycle.ts +17 -5
- package/src/lib/prompt.ts +3 -0
- package/src/pi/runtime-adapter/assembly.ts +2 -1
- package/src/pi/runtime-adapter/execution.ts +8 -1
- package/src/pi/runtime-adapter/index.ts +33 -0
- package/src/pi/runtime-adapter/models.ts +9 -0
- package/src/pi/tool/core-host.ts +5 -1
- package/src/pi/tool/core.ts +1 -10
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/subagent.ts +142 -16
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/runtime-agent-context.ts +1 -0
- package/src/runtime-assembler.ts +16 -1
- package/src/runtime-definition.ts +12 -0
- package/src/runtime.ts +427 -46
- package/src/tool-registry.ts +2 -0
- package/src/workspace-versioning.ts +46 -0
|
@@ -31,6 +31,7 @@ export interface SubmissionRecord {
|
|
|
31
31
|
idempotencyKey?: string | null;
|
|
32
32
|
status: SubmissionStatus;
|
|
33
33
|
accepted: boolean;
|
|
34
|
+
admissionRejected?: boolean;
|
|
34
35
|
abortReason?: string | null;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -266,9 +267,9 @@ export class SubmissionLifecycle<
|
|
|
266
267
|
if (admitted.admitted) this.pump();
|
|
267
268
|
return {
|
|
268
269
|
receipt: admitted.submission,
|
|
269
|
-
completion:
|
|
270
|
-
admitted.submission
|
|
271
|
-
|
|
270
|
+
completion: admitted.rejected
|
|
271
|
+
? Promise.resolve(admitted.submission)
|
|
272
|
+
: this.waitForTerminal(admitted.submission.submissionId),
|
|
272
273
|
};
|
|
273
274
|
}
|
|
274
275
|
|
|
@@ -317,7 +318,11 @@ export class SubmissionLifecycle<
|
|
|
317
318
|
private async admit(
|
|
318
319
|
input: SubmissionInput<TSubmission>,
|
|
319
320
|
create: () => TSubmission,
|
|
320
|
-
): Promise<{
|
|
321
|
+
): Promise<{
|
|
322
|
+
submission: TSubmission;
|
|
323
|
+
admitted: boolean;
|
|
324
|
+
rejected: boolean;
|
|
325
|
+
}> {
|
|
321
326
|
const result = this.options.store.transaction(() => {
|
|
322
327
|
if (input.idempotencyKey) {
|
|
323
328
|
const duplicate = this.options.store.findByIdempotencyKey(
|
|
@@ -327,6 +332,7 @@ export class SubmissionLifecycle<
|
|
|
327
332
|
return {
|
|
328
333
|
submission: { ...duplicate, accepted: false } as TSubmission,
|
|
329
334
|
admitted: false,
|
|
335
|
+
rejected: false,
|
|
330
336
|
};
|
|
331
337
|
}
|
|
332
338
|
}
|
|
@@ -340,6 +346,7 @@ export class SubmissionLifecycle<
|
|
|
340
346
|
accepted: false,
|
|
341
347
|
} as TSubmission,
|
|
342
348
|
admitted: false,
|
|
349
|
+
rejected: false,
|
|
343
350
|
};
|
|
344
351
|
}
|
|
345
352
|
if (
|
|
@@ -348,7 +355,12 @@ export class SubmissionLifecycle<
|
|
|
348
355
|
) {
|
|
349
356
|
throw new SubmissionQueueFullError();
|
|
350
357
|
}
|
|
351
|
-
|
|
358
|
+
const submission = create();
|
|
359
|
+
return {
|
|
360
|
+
submission,
|
|
361
|
+
admitted: submission.admissionRejected !== true,
|
|
362
|
+
rejected: submission.admissionRejected === true,
|
|
363
|
+
};
|
|
352
364
|
});
|
|
353
365
|
if (result.admitted) await this.options.clearTerminal();
|
|
354
366
|
return result;
|
package/src/lib/prompt.ts
CHANGED
|
@@ -63,6 +63,9 @@ export const TOOLS =
|
|
|
63
63
|
"raw or customized network cases described above; every response and failure it sees is visible to you. " +
|
|
64
64
|
"bash is a shell over the workspace filesystem only " +
|
|
65
65
|
"(no network, no system utilities) and is approval-gated — don't reach for it to read files or fetch. " +
|
|
66
|
+
"For more than three related changes in one file, use one write or bash operation instead of serial edits; " +
|
|
67
|
+
"do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
|
|
68
|
+
"prioritize verification, saving durable results, and the final response. " +
|
|
66
69
|
"When tool calls are independent, issue them in one turn so they run in parallel. When you reference " +
|
|
67
70
|
"code, cite it as file_path:line_number.";
|
|
68
71
|
|
|
@@ -234,7 +234,8 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
|
|
|
234
234
|
export function piToolRetryPolicy(
|
|
235
235
|
candidate: PiToolCandidate,
|
|
236
236
|
): "idempotent" | "non-idempotent" {
|
|
237
|
-
return IDEMPOTENT_TOOL_NAMES.has(candidate.tool.name)
|
|
237
|
+
return IDEMPOTENT_TOOL_NAMES.has(candidate.tool.name) ||
|
|
238
|
+
candidate.owner.startsWith("subagent:")
|
|
238
239
|
? "idempotent"
|
|
239
240
|
: "non-idempotent";
|
|
240
241
|
}
|
|
@@ -371,6 +371,11 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
371
371
|
*/
|
|
372
372
|
readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
|
|
373
373
|
readonly durability: PiTurnDurability;
|
|
374
|
+
/** Per-Submission executors for tools whose metadata is fixed at assembly time. */
|
|
375
|
+
readonly toolExecutors?: Readonly<Record<
|
|
376
|
+
string,
|
|
377
|
+
NonNullable<PiToolCandidate["tool"]["execute"]>
|
|
378
|
+
>>;
|
|
374
379
|
/**
|
|
375
380
|
* 上报受治理工具的耗时、结果大小和成败。
|
|
376
381
|
*
|
|
@@ -617,7 +622,9 @@ export class PreparedPiTurnAdapter {
|
|
|
617
622
|
`Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
|
|
618
623
|
);
|
|
619
624
|
}
|
|
620
|
-
|
|
625
|
+
const execute = options.toolExecutors?.[candidate.tool.name] ??
|
|
626
|
+
candidate.tool.execute;
|
|
627
|
+
return execute(
|
|
621
628
|
toolCallId,
|
|
622
629
|
input,
|
|
623
630
|
signal,
|
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
PiRuntimeTranscript,
|
|
34
34
|
type PiTranscriptDurability,
|
|
35
35
|
} from "./transcript";
|
|
36
|
+
import { subagentPiToolCandidates } from "../tool/subagent";
|
|
37
|
+
import type {
|
|
38
|
+
RuntimeSubagentLifecyclePort,
|
|
39
|
+
RuntimeSubagentPort,
|
|
40
|
+
} from "../../kernel/bindings";
|
|
36
41
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -201,6 +206,34 @@ export class PiRuntimeAdapter {
|
|
|
201
206
|
* 实现理由:构造时会校验 Prepared Runtime 的 owner 和 pinned descriptor 格式,再建立本 Turn 独享的 Tool governance 与执行状态。
|
|
202
207
|
* 不要跨 Turn 复用返回对象;abort、steer、assistant ordinal 和 Tool governance 都是单次执行状态。
|
|
203
208
|
*/
|
|
209
|
+
/**
|
|
210
|
+
* 为一个已准入的 Submission 生成绑定 durable lifecycle 的 SubAgent Tool 执行器。
|
|
211
|
+
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* 调用方:Runtime 在创建 Turn 前调用一次,并把结果作为 `createTurn` 的 `toolExecutors` 传入。
|
|
214
|
+
*
|
|
215
|
+
* 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool 层,而 accountId、
|
|
216
|
+
* rateVersion 和 slot identity 这些计费事实属于 Submission。两者在这里汇合:Pi 提供工具,
|
|
217
|
+
* Runtime 提供本次 Submission 的 lifecycle 端口,因此 Runtime 不需要认识 Tool 模块本身。
|
|
218
|
+
* 没有可用 SubAgent 类型时返回 `undefined`,让 Turn 继续使用装配期的无 lifecycle 执行器。
|
|
219
|
+
*/
|
|
220
|
+
subagentToolExecutors(
|
|
221
|
+
subagents: RuntimeSubagentPort,
|
|
222
|
+
enabledSubagents: readonly string[],
|
|
223
|
+
lifecycle: RuntimeSubagentLifecyclePort,
|
|
224
|
+
): CreatePreparedPiTurnOptions["toolExecutors"] {
|
|
225
|
+
const entries = subagentPiToolCandidates(
|
|
226
|
+
subagents,
|
|
227
|
+
enabledSubagents,
|
|
228
|
+
lifecycle,
|
|
229
|
+
).flatMap((candidate) =>
|
|
230
|
+
candidate.tool.execute
|
|
231
|
+
? [[candidate.tool.name, candidate.tool.execute] as const]
|
|
232
|
+
: []
|
|
233
|
+
);
|
|
234
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
204
237
|
createTurn(
|
|
205
238
|
options: CreatePreparedPiTurnOptions,
|
|
206
239
|
): PreparedPiTurnAdapter {
|
|
@@ -513,6 +513,7 @@ function configuredModel(
|
|
|
513
513
|
...catalogHeaders,
|
|
514
514
|
...endpoint.headers,
|
|
515
515
|
};
|
|
516
|
+
const openRouterProviderPin = endpoint.openRouterProviderPins?.[modelId];
|
|
516
517
|
return {
|
|
517
518
|
...metadata,
|
|
518
519
|
api: apiFor(endpoint.protocol),
|
|
@@ -524,6 +525,14 @@ function configuredModel(
|
|
|
524
525
|
...compat,
|
|
525
526
|
sendSessionAffinityHeaders: true,
|
|
526
527
|
sessionAffinityFormat: "openrouter" as const,
|
|
528
|
+
...(openRouterProviderPin
|
|
529
|
+
? {
|
|
530
|
+
openRouterRouting: {
|
|
531
|
+
order: [openRouterProviderPin],
|
|
532
|
+
allow_fallbacks: false,
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
: {}),
|
|
527
536
|
},
|
|
528
537
|
}
|
|
529
538
|
: {}),
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -36,11 +36,15 @@ export function createWorkspaceCodeExecutionPort(options: {
|
|
|
36
36
|
),
|
|
37
37
|
name: "execute",
|
|
38
38
|
});
|
|
39
|
-
const execute = tool
|
|
39
|
+
const { description, execute } = tool;
|
|
40
|
+
if (typeof description !== "string" || description.length === 0) {
|
|
41
|
+
throw new Error("Think createExecuteRuntime returned a tool without a description");
|
|
42
|
+
}
|
|
40
43
|
if (typeof execute !== "function") {
|
|
41
44
|
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
42
45
|
}
|
|
43
46
|
return {
|
|
47
|
+
description,
|
|
44
48
|
execute: (input) => Promise.resolve(execute(input, {
|
|
45
49
|
toolCallId: "execute",
|
|
46
50
|
messages: [],
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -125,16 +125,7 @@ export function codeExecutionPiToolCandidate(
|
|
|
125
125
|
const tool: AgentTool<typeof executeParameters> = {
|
|
126
126
|
name: "execute",
|
|
127
127
|
label: "Execute JavaScript",
|
|
128
|
-
description:
|
|
129
|
-
"Run JavaScript in a short-lived Code Mode Dynamic Worker and return a value. This is your " +
|
|
130
|
-
"raw-network and tool-composition instrument, not a general web-search tool. Call fetch directly " +
|
|
131
|
-
"when you need a raw response, custom headers/method/body, a structured API, or content that " +
|
|
132
|
-
"web_search could not retrieve. Check res.status; when several known endpoints are necessary, " +
|
|
133
|
-
"fetch them in one invocation. Also available: " +
|
|
134
|
-
"state.* for your workspace filesystem (readFile/writeFile/glob/searchFiles/replaceInFiles, " +
|
|
135
|
-
"each taking one object argument), and codemode.step(name, fn) to run a side-effecting block " +
|
|
136
|
-
"exactly once so it survives replay. Write plain JavaScript — TypeScript type annotations are " +
|
|
137
|
-
"a syntax error. Use return for the result and console.log for notes.",
|
|
128
|
+
description: runtime.description,
|
|
138
129
|
parameters: executeParameters,
|
|
139
130
|
// 把模型提供的 JavaScript 交给 Codemode Runtime 执行。
|
|
140
131
|
// Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
|
package/src/pi/tool/index.ts
CHANGED
package/src/pi/tool/mcp.ts
CHANGED
|
@@ -59,7 +59,8 @@ export interface PiMcpHost {
|
|
|
59
59
|
* 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
|
|
60
60
|
*/
|
|
61
61
|
callTool(
|
|
62
|
-
|
|
62
|
+
params: Parameters<MCPClientManager["callTool"]>[0],
|
|
63
|
+
options?: { signal?: AbortSignal },
|
|
63
64
|
): Promise<unknown>;
|
|
64
65
|
};
|
|
65
66
|
}
|
|
@@ -333,7 +334,6 @@ export function createPiMcpToolCandidates(
|
|
|
333
334
|
name: mcpTool.name,
|
|
334
335
|
arguments: args,
|
|
335
336
|
},
|
|
336
|
-
undefined,
|
|
337
337
|
{ signal },
|
|
338
338
|
),
|
|
339
339
|
{
|
package/src/pi/tool/subagent.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RuntimeSubagentLifecyclePort,
|
|
5
|
+
RuntimeSubagentPort,
|
|
6
|
+
RuntimeSubagentRunResult,
|
|
7
|
+
RuntimeSubagentUsageEvent,
|
|
8
|
+
} from "../../kernel/bindings";
|
|
4
9
|
import { AGENT_TYPES } from "../../layers/orchestration/subagents/agent-types/registry";
|
|
5
10
|
import { serializeOutput } from "../../lib/artifacts";
|
|
11
|
+
import {
|
|
12
|
+
isZeroSubagentUsage,
|
|
13
|
+
subagentTerminalStatus,
|
|
14
|
+
} from "../../kernel/subagent-runtime";
|
|
6
15
|
import type { PiToolCandidate } from "./compiler";
|
|
7
16
|
import {
|
|
8
17
|
toolRegistryFromPiCandidates,
|
|
@@ -38,6 +47,61 @@ function failure(agentType: string, status: string, error?: string) {
|
|
|
38
47
|
};
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
async function runRegisteredSubagent(
|
|
51
|
+
lifecycle: RuntimeSubagentLifecyclePort,
|
|
52
|
+
subagentRunId: string,
|
|
53
|
+
run: () => Promise<RuntimeSubagentRunResult>,
|
|
54
|
+
): Promise<RuntimeSubagentRunResult> {
|
|
55
|
+
let result: RuntimeSubagentRunResult;
|
|
56
|
+
try {
|
|
57
|
+
result = await run();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await lifecycle.onTerminal({
|
|
60
|
+
subagentRunId,
|
|
61
|
+
status: "failed",
|
|
62
|
+
zeroUsage: true,
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (result.runId !== subagentRunId) {
|
|
67
|
+
await recordSubagentResult(lifecycle, {
|
|
68
|
+
...result,
|
|
69
|
+
runId: subagentRunId,
|
|
70
|
+
status: "error",
|
|
71
|
+
});
|
|
72
|
+
throw new Error("SubAgent run ID changed after durable registration");
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function recordSubagentResult(
|
|
78
|
+
lifecycle: RuntimeSubagentLifecyclePort,
|
|
79
|
+
run: RuntimeSubagentRunResult,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const status = subagentTerminalStatus(run.status);
|
|
82
|
+
if (run.usage) {
|
|
83
|
+
const event: RuntimeSubagentUsageEvent = {
|
|
84
|
+
eventId: `${run.runId}:usage`,
|
|
85
|
+
submissionId: lifecycle.submissionId,
|
|
86
|
+
runId: run.runId,
|
|
87
|
+
subagentRunId: run.runId,
|
|
88
|
+
parentRunId: lifecycle.parentRunId,
|
|
89
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
90
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
91
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
92
|
+
kind: "subagent",
|
|
93
|
+
status,
|
|
94
|
+
usage: run.usage,
|
|
95
|
+
};
|
|
96
|
+
await lifecycle.onUsage(event);
|
|
97
|
+
}
|
|
98
|
+
await lifecycle.onTerminal({
|
|
99
|
+
subagentRunId: run.runId,
|
|
100
|
+
status,
|
|
101
|
+
zeroUsage: !run.usage || isZeroSubagentUsage(run.usage),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
41
105
|
/**
|
|
42
106
|
* 把已启用的 SubAgent 类型转换成 Pi 候选工具。
|
|
43
107
|
*
|
|
@@ -53,6 +117,7 @@ function failure(agentType: string, status: string, error?: string) {
|
|
|
53
117
|
export function subagentPiToolCandidates(
|
|
54
118
|
subagents: RuntimeSubagentPort | undefined,
|
|
55
119
|
enabledSubagents: readonly string[],
|
|
120
|
+
lifecycle?: RuntimeSubagentLifecyclePort,
|
|
56
121
|
): PiToolCandidate[] {
|
|
57
122
|
if (!subagents) return [];
|
|
58
123
|
const types = [...new Set(enabledSubagents)].flatMap((name) => {
|
|
@@ -79,10 +144,38 @@ export function subagentPiToolCandidates(
|
|
|
79
144
|
// 适配层只固定类型名并统一结果语义。
|
|
80
145
|
async execute(_toolCallId, input, signal) {
|
|
81
146
|
signal?.throwIfAborted();
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
147
|
+
const childInput = input as Record<string, unknown>;
|
|
148
|
+
if (lifecycle && !subagents.reserve) {
|
|
149
|
+
throw new Error("Durable SubAgent execution requires run reservation");
|
|
150
|
+
}
|
|
151
|
+
const reserved = lifecycle
|
|
152
|
+
? await subagents.reserve!(type.name, childInput, {
|
|
153
|
+
parentRunId: lifecycle.parentRunId,
|
|
154
|
+
submissionId: lifecycle.submissionId,
|
|
155
|
+
toolCallId: _toolCallId,
|
|
156
|
+
})
|
|
157
|
+
: undefined;
|
|
158
|
+
if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
|
|
159
|
+
const runOptions = lifecycle
|
|
160
|
+
? {
|
|
161
|
+
parentRunId: lifecycle.parentRunId,
|
|
162
|
+
submissionId: lifecycle.submissionId,
|
|
163
|
+
...(reserved ? { subagentRunId: reserved.runId } : {}),
|
|
164
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
165
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
166
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
167
|
+
}
|
|
168
|
+
: undefined;
|
|
169
|
+
const run = lifecycle && reserved
|
|
170
|
+
? await runRegisteredSubagent(
|
|
171
|
+
lifecycle,
|
|
172
|
+
reserved.runId,
|
|
173
|
+
() => subagents.run(type.name, childInput, runOptions),
|
|
174
|
+
)
|
|
175
|
+
: await subagents.run(type.name, childInput);
|
|
176
|
+
if (lifecycle && !run.childStillRunning) {
|
|
177
|
+
await recordSubagentResult(lifecycle, run);
|
|
178
|
+
}
|
|
86
179
|
if (run.status !== "completed" || run.output === undefined) {
|
|
87
180
|
return result(failure(type.name, run.status, run.error));
|
|
88
181
|
}
|
|
@@ -112,7 +205,7 @@ export function subagentPiToolCandidates(
|
|
|
112
205
|
// 调用:Pi 需要子任务异步进行、不应阻塞当前 Turn 时调用。
|
|
113
206
|
// 原因:先用具体 AgentType Schema 复核嵌套 input,
|
|
114
207
|
// 再把 detached 和通知语义交给 Host,避免后台入口绕过类型约束。
|
|
115
|
-
async execute(
|
|
208
|
+
async execute(toolCallId, value, signal) {
|
|
116
209
|
signal?.throwIfAborted();
|
|
117
210
|
const input = value as z.infer<typeof parameters>;
|
|
118
211
|
const parsed = AGENT_TYPES.byName[input.agentType]!.inputSchema.safeParse(
|
|
@@ -123,15 +216,47 @@ export function subagentPiToolCandidates(
|
|
|
123
216
|
`SubAgent input failed schema validation: ${parsed.error.message}`,
|
|
124
217
|
);
|
|
125
218
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
219
|
+
if (lifecycle && !subagents.reserve) {
|
|
220
|
+
throw new Error("Durable SubAgent execution requires run reservation");
|
|
221
|
+
}
|
|
222
|
+
const reserved = lifecycle
|
|
223
|
+
? await subagents.reserve!(
|
|
224
|
+
input.agentType,
|
|
225
|
+
parsed.data as Record<string, unknown>,
|
|
226
|
+
{
|
|
227
|
+
parentRunId: lifecycle.parentRunId,
|
|
228
|
+
submissionId: lifecycle.submissionId,
|
|
229
|
+
toolCallId,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
: undefined;
|
|
233
|
+
if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
|
|
234
|
+
const runInput = parsed.data as Record<string, unknown>;
|
|
235
|
+
const runOptions = {
|
|
236
|
+
detached: true,
|
|
237
|
+
maxBudgetMs: BACKGROUND_MAX_BUDGET_MS,
|
|
238
|
+
notifySource: "dispatch-background",
|
|
239
|
+
...(lifecycle
|
|
240
|
+
? {
|
|
241
|
+
parentRunId: lifecycle.parentRunId,
|
|
242
|
+
submissionId: lifecycle.submissionId,
|
|
243
|
+
...(reserved ? { subagentRunId: reserved.runId } : {}),
|
|
244
|
+
...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
|
|
245
|
+
...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
|
|
246
|
+
...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
};
|
|
250
|
+
const run = lifecycle && reserved
|
|
251
|
+
? await runRegisteredSubagent(
|
|
252
|
+
lifecycle,
|
|
253
|
+
reserved.runId,
|
|
254
|
+
() => subagents.run(input.agentType, runInput, runOptions),
|
|
255
|
+
)
|
|
256
|
+
: await subagents.run(input.agentType, runInput, runOptions);
|
|
257
|
+
if (lifecycle && !run.childStillRunning && run.status !== "running") {
|
|
258
|
+
await recordSubagentResult(lifecycle, run);
|
|
259
|
+
}
|
|
135
260
|
return result({
|
|
136
261
|
runId: run.runId,
|
|
137
262
|
status: run.status,
|
|
@@ -153,8 +278,9 @@ export function subagentPiToolCandidates(
|
|
|
153
278
|
export function createSubagentTools(
|
|
154
279
|
subagents: RuntimeSubagentPort | undefined,
|
|
155
280
|
enabledSubagents: readonly string[],
|
|
281
|
+
lifecycle?: RuntimeSubagentLifecyclePort,
|
|
156
282
|
): ToolRegistry {
|
|
157
283
|
return toolRegistryFromPiCandidates(
|
|
158
|
-
subagentPiToolCandidates(subagents, enabledSubagents),
|
|
284
|
+
subagentPiToolCandidates(subagents, enabledSubagents, lifecycle),
|
|
159
285
|
);
|
|
160
286
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { WorkspaceRevisionRestorePort } from "../../workspace-versioning";
|
|
4
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
5
|
+
import {
|
|
6
|
+
toolRegistryFromPiCandidates,
|
|
7
|
+
type ToolRegistry,
|
|
8
|
+
} from "../../tool-registry";
|
|
9
|
+
import type { PiToolCandidate } from "./compiler";
|
|
10
|
+
|
|
11
|
+
const parameters = Type.Object({
|
|
12
|
+
target: Type.Union([
|
|
13
|
+
Type.Literal("previous"),
|
|
14
|
+
Type.String({
|
|
15
|
+
pattern: "^[0-9a-f]{40}$",
|
|
16
|
+
description: "A complete revision ID shown in Workspace history.",
|
|
17
|
+
}),
|
|
18
|
+
], {
|
|
19
|
+
description: "Use previous for the revision immediately before the current one.",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function result<T>(details: T): AgentToolResult<T> {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
26
|
+
details,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function workspaceRevisionPiToolCandidate(
|
|
31
|
+
versions: WorkspaceRevisionRestorePort,
|
|
32
|
+
): PiToolCandidate {
|
|
33
|
+
const tool: AgentTool<typeof parameters> = {
|
|
34
|
+
name: "restore_workspace_revision",
|
|
35
|
+
label: "Restore Workspace revision",
|
|
36
|
+
description:
|
|
37
|
+
"Restore the configured Workspace tree to the previous revision or an exact revision. Use only when the person explicitly asks to roll back. This always asks for approval and changes only the Workspace files managed by the Host.",
|
|
38
|
+
parameters,
|
|
39
|
+
async execute(_toolCallId, input, signal) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
return result({
|
|
42
|
+
status: "restored",
|
|
43
|
+
...await versions.restore(input.target),
|
|
44
|
+
note: "The approved Workspace revision is now current.",
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
owner: "workspace",
|
|
50
|
+
tool,
|
|
51
|
+
requiredExecutionLevel: "safe",
|
|
52
|
+
alwaysRequiresApproval: true,
|
|
53
|
+
source: "action",
|
|
54
|
+
summary: "Restore the Workspace tree",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createWorkspaceRevisionTools(
|
|
59
|
+
versions: WorkspaceRevisionRestorePort,
|
|
60
|
+
): ToolRegistry {
|
|
61
|
+
return toolRegistryFromPiCandidates([
|
|
62
|
+
workspaceRevisionPiToolCandidate(versions),
|
|
63
|
+
]);
|
|
64
|
+
}
|
package/src/runtime-assembler.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "./pi/tool/core";
|
|
56
56
|
import { skillPiToolCandidates } from "./pi/tool/skill";
|
|
57
57
|
import { createWebSearch } from "./pi/tool/web-search";
|
|
58
|
+
import { subagentPiToolCandidates } from "./pi/tool/subagent";
|
|
58
59
|
import { resolvePiModel } from "./pi/runtime-adapter/models";
|
|
59
60
|
|
|
60
61
|
/** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
|
|
@@ -474,7 +475,13 @@ class RuntimeBuilder {
|
|
|
474
475
|
...(this.memoryProfile.enabled && this.memory
|
|
475
476
|
? { memory: { port: this.memory, profile: this.memoryProfile } }
|
|
476
477
|
: {}),
|
|
477
|
-
hostTools:
|
|
478
|
+
hostTools: [
|
|
479
|
+
...this.hostTools,
|
|
480
|
+
...subagentPiToolCandidates(
|
|
481
|
+
this.subagents,
|
|
482
|
+
[...this.enabledSubagents],
|
|
483
|
+
),
|
|
484
|
+
],
|
|
478
485
|
skills: skillSources,
|
|
479
486
|
enabledSubagents: [...this.enabledSubagents],
|
|
480
487
|
webSearch,
|
|
@@ -492,6 +499,7 @@ class RuntimeBuilder {
|
|
|
492
499
|
...(this.memoryProfile.enabled && this.memory
|
|
493
500
|
? { memory: this.memory }
|
|
494
501
|
: {}),
|
|
502
|
+
...(this.subagents ? { subagents: this.subagents } : {}),
|
|
495
503
|
skills: Object.freeze({
|
|
496
504
|
sources: Object.freeze(skillSources),
|
|
497
505
|
}),
|
|
@@ -632,6 +640,8 @@ function hooksToTurnEventsPort<
|
|
|
632
640
|
if (
|
|
633
641
|
!hooks.onTurnEnd &&
|
|
634
642
|
!hooks.onModelUsage &&
|
|
643
|
+
!hooks.onSubagentUsage &&
|
|
644
|
+
!hooks.onLifecycleFact &&
|
|
635
645
|
!hooks.onToolStart &&
|
|
636
646
|
!hooks.onToolSettled &&
|
|
637
647
|
!hooks.onApproval &&
|
|
@@ -645,6 +655,8 @@ function hooksToTurnEventsPort<
|
|
|
645
655
|
? (messages) => hooks.onTurnEnd!(ctx, messages)
|
|
646
656
|
: async () => undefined,
|
|
647
657
|
...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
|
|
658
|
+
...(hooks.onSubagentUsage ? { onSubagentUsage: hooks.onSubagentUsage } : {}),
|
|
659
|
+
...(hooks.onLifecycleFact ? { onLifecycleFact: hooks.onLifecycleFact } : {}),
|
|
648
660
|
...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
|
|
649
661
|
...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
|
|
650
662
|
...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
|
|
@@ -757,6 +769,9 @@ export async function assembleRuntimeSnapshot<
|
|
|
757
769
|
...(memoryProfile.enabled && toolAssembly.bindings?.memory
|
|
758
770
|
? { memory: toolAssembly.bindings.memory }
|
|
759
771
|
: {}),
|
|
772
|
+
...(toolAssembly.bindings?.subagents
|
|
773
|
+
? { subagents: toolAssembly.bindings.subagents }
|
|
774
|
+
: {}),
|
|
760
775
|
hostTools,
|
|
761
776
|
skills: resources.skills,
|
|
762
777
|
connectors: resources.connectors.servers,
|
|
@@ -3,6 +3,10 @@ import type { ExecutionLevel } from "./lib/execution-level";
|
|
|
3
3
|
import type {
|
|
4
4
|
RuntimeGatewayPort,
|
|
5
5
|
RuntimeModelUsageEvent,
|
|
6
|
+
RuntimeSubmissionAdmissionHook,
|
|
7
|
+
RuntimeSubagentUsageEvent,
|
|
8
|
+
RuntimeLifecycleFact,
|
|
9
|
+
RuntimeEventConfirmation,
|
|
6
10
|
RuntimePlatformPort,
|
|
7
11
|
RuntimeProviderPort,
|
|
8
12
|
RuntimeSkillScriptPolicy,
|
|
@@ -136,6 +140,14 @@ export interface RuntimeAgentHooks<
|
|
|
136
140
|
messages: readonly RuntimeTurnMessage[],
|
|
137
141
|
) => Promise<void>;
|
|
138
142
|
readonly onModelUsage?: (event: RuntimeModelUsageEvent) => Promise<void>;
|
|
143
|
+
readonly onSubagentUsage?: (
|
|
144
|
+
event: RuntimeSubagentUsageEvent,
|
|
145
|
+
) => Promise<RuntimeEventConfirmation | void>;
|
|
146
|
+
readonly onLifecycleFact?: (
|
|
147
|
+
event: RuntimeLifecycleFact,
|
|
148
|
+
) => Promise<RuntimeEventConfirmation | void>;
|
|
149
|
+
/** Single admission seam shared by chat, RPC, regenerate and scheduled submissions. */
|
|
150
|
+
readonly onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
|
|
139
151
|
readonly onToolStart?: (event: RuntimeToolStartEvent) => Promise<void>;
|
|
140
152
|
readonly onToolSettled?: (event: RuntimeToolSettlementEvent) => Promise<void>;
|
|
141
153
|
readonly onApproval?: (input: ApprovalHookInput) => Promise<void>;
|