@springbrand/agent-runtime 0.1.3-alpha.7 → 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/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/schedule.ts +30 -18
- 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
|
@@ -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/schedule.ts
CHANGED
|
@@ -94,7 +94,10 @@ function result<T>(details: T): AgentToolResult<T> {
|
|
|
94
94
|
function candidate<T extends TSchema>(
|
|
95
95
|
tool: AgentTool<T>,
|
|
96
96
|
options: Partial<
|
|
97
|
-
Pick<
|
|
97
|
+
Pick<
|
|
98
|
+
PiToolCandidate,
|
|
99
|
+
"alwaysRequiresApproval" | "owner" | "requiredExecutionLevel" | "summary"
|
|
100
|
+
>
|
|
98
101
|
> = {},
|
|
99
102
|
): PiToolCandidate {
|
|
100
103
|
return {
|
|
@@ -102,6 +105,9 @@ function candidate<T extends TSchema>(
|
|
|
102
105
|
requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
|
|
103
106
|
source: "action",
|
|
104
107
|
tool,
|
|
108
|
+
...(options.alwaysRequiresApproval
|
|
109
|
+
? { alwaysRequiresApproval: true }
|
|
110
|
+
: {}),
|
|
105
111
|
...(options.summary ? { summary: options.summary } : {}),
|
|
106
112
|
};
|
|
107
113
|
}
|
|
@@ -110,24 +116,30 @@ export function schedulePiToolCandidates(
|
|
|
110
116
|
schedule: RuntimeSchedulePort,
|
|
111
117
|
): PiToolCandidate[] {
|
|
112
118
|
return [
|
|
113
|
-
candidate(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
signal
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
candidate(
|
|
120
|
+
{
|
|
121
|
+
name: "schedule",
|
|
122
|
+
label: "Schedule prompt",
|
|
123
|
+
description:
|
|
124
|
+
"Schedule a prompt to run LATER, autonomously. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
|
|
125
|
+
parameters: scheduleParameters,
|
|
126
|
+
async execute(_toolCallId, input, signal) {
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
const spec: ScheduleSpec = {
|
|
129
|
+
trigger: input.trigger,
|
|
130
|
+
job: { kind: "prompt", prompt: input.prompt },
|
|
131
|
+
label: input.label,
|
|
132
|
+
...(input.timezone ? { tz: input.timezone } : {}),
|
|
133
|
+
};
|
|
134
|
+
const { id } = await schedule.create(spec);
|
|
135
|
+
return result({ scheduled: true, id });
|
|
136
|
+
},
|
|
129
137
|
},
|
|
130
|
-
|
|
138
|
+
{
|
|
139
|
+
alwaysRequiresApproval: true,
|
|
140
|
+
summary: "Create a scheduled task",
|
|
141
|
+
},
|
|
142
|
+
),
|
|
131
143
|
candidate({
|
|
132
144
|
name: "list_schedules",
|
|
133
145
|
label: "List schedules",
|
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>;
|