@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.20
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 +11 -9
- package/src/adapter/cloudflare/agent.ts +177 -122
- package/src/adapter/cloudflare/define-agent.ts +47 -26
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +51 -13
- package/src/config/model-guidance.ts +20 -0
- package/src/config/pinning.ts +39 -5
- package/src/config/ports.ts +57 -7
- package/src/config/public-contracts.ts +2 -2
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/message/conversion.ts +3 -3
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +255 -446
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +12 -1
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/repository.ts +64 -25
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- 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/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → 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/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
type FiberRecoveryContext,
|
|
8
8
|
type FiberRecoveryResult,
|
|
9
9
|
} from "agents";
|
|
10
|
-
import type { HarnessEvent } from "@earendil-works/pi-
|
|
10
|
+
import type { AgentEvent as HarnessEvent } from "@earendil-works/pi-durable";
|
|
11
11
|
import type { UIMessageChunk } from "ai";
|
|
12
12
|
import {
|
|
13
13
|
AGENT_TYPES,
|
|
14
|
-
} from "../../../tools/subagent
|
|
15
|
-
import type { AgentType } from "../../../tools/subagent
|
|
14
|
+
} from "../../../tools/subagent/types/registry";
|
|
15
|
+
import type { AgentType } from "../../../tools/subagent/types/contract";
|
|
16
16
|
import type {
|
|
17
17
|
RuntimeProviderPort,
|
|
18
18
|
WorkspacePort,
|
|
@@ -30,8 +30,8 @@ import {
|
|
|
30
30
|
decodeSubagentCompletion,
|
|
31
31
|
emptySubagentUsage,
|
|
32
32
|
encodeSubagentCompletion,
|
|
33
|
-
} from "../../../tools/subagent
|
|
34
|
-
import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent
|
|
33
|
+
} from "../../../tools/subagent/runtime";
|
|
34
|
+
import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent/authority";
|
|
35
35
|
import { createCloudflareSubAgentTools } from "./tools";
|
|
36
36
|
|
|
37
37
|
type RunStatus =
|
|
@@ -96,25 +96,27 @@ function stringifyJson(value: unknown): string {
|
|
|
96
96
|
function progressFor(
|
|
97
97
|
event: HarnessEvent,
|
|
98
98
|
): Omit<AgentToolProgressSnapshot, "at"> | null {
|
|
99
|
-
if (event.type === "run_start"
|
|
99
|
+
if (event.type === "run_start") {
|
|
100
100
|
return { phase: "running", message: "SubAgent started" };
|
|
101
101
|
}
|
|
102
|
-
if (event.type === "
|
|
102
|
+
if (event.type === "tool_execution_start") {
|
|
103
103
|
return {
|
|
104
104
|
phase: "tool",
|
|
105
105
|
message: `Running ${event.toolName}`,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
if (event.type === "
|
|
108
|
+
if (event.type === "tool_execution_update") {
|
|
109
109
|
return {
|
|
110
110
|
phase: "tool",
|
|
111
111
|
message: `${event.toolName} is still running`,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
if (event.type === "
|
|
114
|
+
if (event.type === "tool_execution_end") {
|
|
115
|
+
const result = event.entry?.model?.[0];
|
|
116
|
+
const isError = result?.role !== "toolResult" || result.isError;
|
|
115
117
|
return {
|
|
116
|
-
phase:
|
|
117
|
-
message:
|
|
118
|
+
phase: isError ? "tool-error" : "tool",
|
|
119
|
+
message: isError
|
|
118
120
|
? `${event.toolName} failed`
|
|
119
121
|
: `${event.toolName} completed`,
|
|
120
122
|
};
|
|
@@ -414,14 +416,24 @@ export abstract class CloudflareSubAgent<
|
|
|
414
416
|
tools,
|
|
415
417
|
signal,
|
|
416
418
|
onEvent: (event) => {
|
|
417
|
-
if (event.type === "message_end" && event.
|
|
418
|
-
addSubagentUsage(usage, event.
|
|
419
|
+
if (event.type === "message_end" && event.entry.model?.[0]?.role === "assistant") {
|
|
420
|
+
addSubagentUsage(usage, event.entry.model[0].usage);
|
|
419
421
|
this.persistUsage(runId, usage);
|
|
420
422
|
}
|
|
421
423
|
publish(projection!.event(event));
|
|
422
424
|
const progress = progressFor(event);
|
|
423
425
|
if (progress) this.persistProgress(runId, progress);
|
|
424
426
|
},
|
|
427
|
+
// Live frames can stop short of the last response; the canonical entries are the billed total.
|
|
428
|
+
onEntries: (entries) => {
|
|
429
|
+
const canonical = emptySubagentUsage();
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const message = entry.model?.[0];
|
|
432
|
+
if (message?.role === "assistant") addSubagentUsage(canonical, message.usage);
|
|
433
|
+
}
|
|
434
|
+
Object.assign(usage, canonical);
|
|
435
|
+
this.persistUsage(runId, usage);
|
|
436
|
+
},
|
|
425
437
|
});
|
|
426
438
|
publish(projection.finish({ status: "completed", endedAt: Date.now() }));
|
|
427
439
|
this.completeRun(runId, output, usage);
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type HarnessEvent,
|
|
4
|
-
type Session,
|
|
5
|
-
type StreamFn,
|
|
6
|
-
} from "@earendil-works/pi-agent-core";
|
|
1
|
+
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { AgentEvent as HarnessEvent, AgentEventStream, EntryRecord, Storage as Session, ToolRegistration } from "@earendil-works/pi-durable";
|
|
7
3
|
import {
|
|
8
4
|
awaitWithContext,
|
|
9
5
|
BACKGROUND_CONTEXT,
|
|
10
6
|
withAbortSignal,
|
|
11
|
-
} from "@earendil-works/
|
|
7
|
+
} from "@earendil-works/chord/context";
|
|
12
8
|
import type { Api, AssistantMessage, Model, Models } from "@earendil-works/pi-ai";
|
|
13
9
|
import { z } from "zod";
|
|
14
10
|
import { assembleSubagentPrompt } from "../../../lib/prompt";
|
|
15
|
-
import type { AgentType } from "../../../tools/subagent
|
|
11
|
+
import type { AgentType } from "../../../tools/subagent/types/contract";
|
|
16
12
|
import { structuredOutputStream } from "../../../models";
|
|
17
13
|
import {
|
|
18
14
|
openSessionHarness,
|
|
@@ -39,11 +35,24 @@ export interface CloudflareSubAgentRun {
|
|
|
39
35
|
openSession(): Session | Promise<Session>;
|
|
40
36
|
operationId?: string;
|
|
41
37
|
/** Tools compiled by the package's single Harness adapter. */
|
|
42
|
-
tools?:
|
|
38
|
+
tools?: ToolRegistration<any, any>[];
|
|
43
39
|
signal?: AbortSignal;
|
|
44
40
|
timeoutMs?: number;
|
|
45
|
-
/** Native operation events for projections
|
|
41
|
+
/** Native operation events for live projections; they may stop short of the final frame. */
|
|
46
42
|
onEvent?: (event: HarnessEvent, signal: AbortSignal) => void | Promise<void>;
|
|
43
|
+
/** This operation's canonical entries after each settle or release, the source of its final usage. */
|
|
44
|
+
onEntries?: (entries: readonly EntryRecord[]) => void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The first failed Tool result among an operation's canonical entries. */
|
|
48
|
+
function toolFailureOf(entries: readonly EntryRecord[]): { name: string; error: string } | undefined {
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const result = entry.model?.[0];
|
|
51
|
+
if (result?.role !== "toolResult" || !result.isError) continue;
|
|
52
|
+
const diagnostics = (entry.data as { diagnostics?: Array<{ severity: string; message: string }> } | undefined)?.diagnostics;
|
|
53
|
+
return { name: result.toolName, error: diagnostics?.find((item) => item.severity === "error")?.message ?? toolErrorText(result) };
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
function textOf(message: AssistantMessage): string {
|
|
@@ -131,9 +140,15 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
131
140
|
streamOptions: { maxRetries: 0 },
|
|
132
141
|
toolExecution: "parallel",
|
|
133
142
|
compaction: { enabled: false, reserveTokens: 1, keepRecentTokens: 1 },
|
|
134
|
-
} satisfies Omit<SessionHarnessOptions
|
|
135
|
-
let runtime: SessionHarness
|
|
143
|
+
} satisfies Omit<SessionHarnessOptions, "storage">;
|
|
144
|
+
let runtime: SessionHarness | undefined;
|
|
145
|
+
let events: AgentEventStream | undefined;
|
|
136
146
|
let toolFailure: { name: string; error: string } | undefined;
|
|
147
|
+
const operationEntries = async (active: SessionHarness): Promise<EntryRecord[]> => {
|
|
148
|
+
const submission = await active.submissionFor(operationId, BACKGROUND_CONTEXT);
|
|
149
|
+
const entries = await active.entries(BACKGROUND_CONTEXT);
|
|
150
|
+
return submission?.entry === undefined ? [] : entries.filter((entry) => entry.id >= submission.entry!);
|
|
151
|
+
};
|
|
137
152
|
|
|
138
153
|
let timedOut = false;
|
|
139
154
|
const timeout = setTimeout(() => {
|
|
@@ -141,61 +156,43 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
141
156
|
controller.abort(new Error("SubAgent run timed out"));
|
|
142
157
|
}, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
143
158
|
try {
|
|
144
|
-
let settled: Awaited<ReturnType<SessionHarness
|
|
159
|
+
let settled: Awaited<ReturnType<SessionHarness["run"]>>;
|
|
145
160
|
while (true) {
|
|
146
161
|
signal.throwIfAborted();
|
|
147
162
|
if (!runtime) {
|
|
148
163
|
runtime = await openSessionHarness({
|
|
149
164
|
...options,
|
|
150
|
-
|
|
165
|
+
storage: await run.openSession(),
|
|
151
166
|
}, context);
|
|
152
167
|
if (runtime.openOperation && runtime.openOperation.operationId !== operationId) {
|
|
153
168
|
throw new Error("SubAgent Session has an unrelated open operation");
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (const type of ["run_start", "run_resume", "run_end", "turn_start", "message_start", "message_update", "message_end", "tool_start", "tool_update", "tool_end"] as const) {
|
|
159
|
-
harness.events.on(type, (event) => onEvent(event, signal));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
harness.events.on("tool_end", (event) => {
|
|
163
|
-
if (event.isError) {
|
|
164
|
-
toolFailure ??= { name: event.toolName, error: toolErrorText(event.result) };
|
|
165
|
-
}
|
|
170
|
+
events = await runtime.watch(context);
|
|
171
|
+
events.start(async (batch) => {
|
|
172
|
+
for (const event of batch) await run.onEvent?.(event, signal);
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
175
|
const result = await runtime.run(
|
|
169
176
|
!runtime.openOperation
|
|
170
177
|
? {
|
|
171
|
-
kind: "prompt",
|
|
172
178
|
operationId,
|
|
173
179
|
prompt: taskPrompt(input.data, structuredStream ? undefined : outputSchema),
|
|
174
180
|
}
|
|
175
181
|
: undefined,
|
|
176
182
|
context,
|
|
177
|
-
{ waitForRetry: true, pollDeferred: true },
|
|
178
183
|
);
|
|
184
|
+
await events?.stop();
|
|
185
|
+
events = undefined;
|
|
179
186
|
if (result.kind === "released") {
|
|
180
187
|
await runtime.close(BACKGROUND_CONTEXT);
|
|
181
188
|
runtime = undefined;
|
|
182
189
|
continue;
|
|
183
190
|
}
|
|
184
|
-
if (result.kind === "waiting") {
|
|
185
|
-
const delay = result.reason === "deferred"
|
|
186
|
-
? result.deferred.pollAfterMs ?? 1_000
|
|
187
|
-
: Math.max(0, result.notBefore - Date.now());
|
|
188
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
189
|
-
try {
|
|
190
|
-
await awaitWithContext(new Promise<void>((resolve) => {
|
|
191
|
-
timer = setTimeout(resolve, delay);
|
|
192
|
-
}), context);
|
|
193
|
-
} finally {
|
|
194
|
-
clearTimeout(timer);
|
|
195
|
-
}
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
191
|
settled = result;
|
|
192
|
+
// Pi's stop may drop the last frames; failures and usage come from the canonical record.
|
|
193
|
+
const owned = await operationEntries(runtime);
|
|
194
|
+
toolFailure = toolFailureOf(owned);
|
|
195
|
+
await run.onEntries?.(owned);
|
|
199
196
|
break;
|
|
200
197
|
}
|
|
201
198
|
if (timedOut) throw new Error("SubAgent run timed out");
|
|
@@ -212,14 +209,9 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
212
209
|
if (toolFailure) {
|
|
213
210
|
throw new Error(`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`);
|
|
214
211
|
}
|
|
215
|
-
const assistant =
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)).find((entry) => entry.type === "message" && entry.message.role === "assistant");
|
|
219
|
-
if (!assistant || assistant.type !== "message" || assistant.message.role !== "assistant") {
|
|
220
|
-
throw new Error("SubAgent produced no structured output");
|
|
221
|
-
}
|
|
222
|
-
const text = textOf(assistant.message);
|
|
212
|
+
const assistant = [...await runtime.entries(context)].reverse().flatMap((entry) => entry.model ?? []).find((message) => message.role === "assistant");
|
|
213
|
+
if (assistant?.role !== "assistant") throw new Error("SubAgent produced no structured output");
|
|
214
|
+
const text = textOf(assistant);
|
|
223
215
|
if (!text) throw new Error("SubAgent produced no structured output");
|
|
224
216
|
let output: unknown;
|
|
225
217
|
try {
|
|
@@ -239,9 +231,10 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
239
231
|
} finally {
|
|
240
232
|
clearTimeout(timeout);
|
|
241
233
|
if (signal.aborted && runtime?.openOperation) {
|
|
242
|
-
await runtime.
|
|
234
|
+
await runtime.abort(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
243
235
|
await runtime.run(undefined, BACKGROUND_CONTEXT).catch(() => undefined);
|
|
244
236
|
}
|
|
237
|
+
await events?.stop();
|
|
245
238
|
await runtime?.close(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
246
239
|
}
|
|
247
240
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolRegistration } from "@earendil-works/pi-durable";
|
|
1
2
|
import {
|
|
2
3
|
DynamicWorkerExecutor,
|
|
3
4
|
resolveProvider,
|
|
@@ -9,19 +10,18 @@ import {
|
|
|
9
10
|
} from "@cloudflare/shell";
|
|
10
11
|
import { stateToolsFromBackend } from "@cloudflare/shell/workers";
|
|
11
12
|
import type {
|
|
12
|
-
AgentHarnessTool,
|
|
13
13
|
AgentTool,
|
|
14
14
|
AgentToolResult,
|
|
15
15
|
} from "@earendil-works/pi-agent-core";
|
|
16
16
|
import { Type } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { WorkspacePort } from "../../../config/ports";
|
|
18
|
-
import type { AgentType } from "../../../tools/subagent
|
|
18
|
+
import type { AgentType } from "../../../tools/subagent/types/contract";
|
|
19
19
|
import {
|
|
20
20
|
compileHarnessTools,
|
|
21
21
|
type CompileHarnessToolsOptions,
|
|
22
22
|
type PiToolCandidate,
|
|
23
23
|
} from "../../../tools";
|
|
24
|
-
import { workspacePiToolCandidates } from "../../../tools/workspace
|
|
24
|
+
import { workspacePiToolCandidates } from "../../../tools/workspace";
|
|
25
25
|
|
|
26
26
|
const READONLY_WORKSPACE_TOOLS = new Set([
|
|
27
27
|
"read",
|
|
@@ -221,7 +221,7 @@ export function createCloudflareSubAgentTools(options: {
|
|
|
221
221
|
loader: WorkerLoader;
|
|
222
222
|
outbound: Fetcher;
|
|
223
223
|
settle: NonNullable<CompileHarnessToolsOptions["onSettlement"]>;
|
|
224
|
-
}):
|
|
224
|
+
}): ToolRegistration<any, any>[] {
|
|
225
225
|
const workspace = workspacePiToolCandidates(options.workspace).filter(
|
|
226
226
|
(candidate) =>
|
|
227
227
|
options.profile === "capable" ||
|
|
@@ -9,16 +9,16 @@ import type {
|
|
|
9
9
|
RuntimeSubagentPort,
|
|
10
10
|
WorkspacePort,
|
|
11
11
|
} from "../../../config/ports";
|
|
12
|
-
import { decodeSubagentCompletion } from "../../../tools/subagent
|
|
12
|
+
import { decodeSubagentCompletion } from "../../../tools/subagent/runtime";
|
|
13
13
|
import type { RuntimeDegradation } from "../../../config/degradation";
|
|
14
14
|
import type { RuntimeMemoryProfile } from "../../../config/profile";
|
|
15
|
-
import { AGENT_TYPES } from "../../../tools/subagent
|
|
15
|
+
import { AGENT_TYPES } from "../../../tools/subagent/types/registry";
|
|
16
16
|
import type { RuntimeAgentConfigContext } from "../../../config/agent-context";
|
|
17
17
|
import {
|
|
18
18
|
createBrowserExecutionFactory,
|
|
19
19
|
createWorkspaceCodeExecutionFactory,
|
|
20
20
|
type RuntimeBrowserBinding,
|
|
21
|
-
} from "../../../tools/
|
|
21
|
+
} from "../../../tools/codemode/adapter";
|
|
22
22
|
import {
|
|
23
23
|
createCloudflareSandboxAdapter,
|
|
24
24
|
type SandboxAdmission,
|
|
@@ -7,15 +7,13 @@ import type {
|
|
|
7
7
|
} from "../../../config/ports";
|
|
8
8
|
import type { RuntimeDegradation } from "../../../config/degradation";
|
|
9
9
|
import type { RuntimeMemoryProfile } from "../../../config/profile";
|
|
10
|
-
import type { WorkspaceRevisionRestorePort } from "../../../tools/workspace
|
|
10
|
+
import type { WorkspaceRevisionRestorePort } from "../../../tools/workspace/versioning";
|
|
11
11
|
import { schedulePiToolCandidates } from "../../../tools/schedule";
|
|
12
|
-
import { workspaceRevisionPiToolCandidate } from "../../../tools/workspace
|
|
13
|
-
import {
|
|
14
|
-
sandboxPiToolCandidates,
|
|
15
|
-
} from "../../../tools/workspace-sandbox";
|
|
12
|
+
import { workspaceRevisionPiToolCandidate } from "../../../tools/workspace/revision";
|
|
13
|
+
import { sandboxPiToolCandidates } from "../../../tools/sandbox";
|
|
16
14
|
import type { RuntimeCodeExecutionFactory } from "../../../config/ports";
|
|
17
15
|
import type { ToolAssemblyResult } from "../../../config/definition";
|
|
18
|
-
import type { PiToolCandidate } from "../../../tools/
|
|
16
|
+
import type { PiToolCandidate } from "../../../tools/runtime/contracts";
|
|
19
17
|
|
|
20
18
|
export function assembleUniversalAgentTools(options: {
|
|
21
19
|
hostTools?: readonly PiToolCandidate[];
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type WorkspaceRevision,
|
|
6
6
|
type WorkspaceRevisionManifest,
|
|
7
7
|
type WorkspaceVersionPort,
|
|
8
|
-
} from "../../../tools/workspace
|
|
8
|
+
} from "../../../tools/workspace/versioning";
|
|
9
9
|
import { createWorkspaceGitFs } from "./git-fs";
|
|
10
10
|
|
|
11
11
|
const PAGE_SIZE = 256;
|
package/src/config/assembler.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { schedulePiToolCandidates } from "../tools/schedule";
|
|
1
2
|
import type { SkillSource } from "agents/skills";
|
|
2
3
|
import type {
|
|
3
4
|
RuntimeGatewayPort,
|
|
@@ -36,10 +37,7 @@ import {
|
|
|
36
37
|
type PiRuntimeAssembly,
|
|
37
38
|
} from "./assembly-snapshot";
|
|
38
39
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
39
|
-
import type {
|
|
40
|
-
PiToolCandidate,
|
|
41
|
-
ToolExposureMode,
|
|
42
|
-
} from "../tools/compiler";
|
|
40
|
+
import type { PiToolCandidate, ToolExposureMode } from "../tools/runtime/contracts";
|
|
43
41
|
import type {
|
|
44
42
|
AssembleRuntimeSnapshotInput,
|
|
45
43
|
RuntimeSettings,
|
|
@@ -50,17 +48,11 @@ import type {
|
|
|
50
48
|
RuntimeToolSurfacePolicy,
|
|
51
49
|
} from "./definition";
|
|
52
50
|
import type { RuntimeCodeExecutionFactory } from "./ports";
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from "../tools/base";
|
|
57
|
-
import {
|
|
58
|
-
BROWSER_EXECUTE_TOOL_NAME,
|
|
59
|
-
browserExecutionPiToolCandidate,
|
|
60
|
-
codeExecutionPiToolCandidate,
|
|
61
|
-
} from "../tools/core";
|
|
51
|
+
import { basePiToolCandidates } from "../tools/assistant";
|
|
52
|
+
import { memoryPiToolCandidate } from "../tools/assistant/memory";
|
|
53
|
+
import { BROWSER_EXECUTE_TOOL_NAME, browserExecutionPiToolCandidate, codeExecutionPiToolCandidate } from "../tools/codemode";
|
|
62
54
|
import { skillPiToolCandidates } from "../tools/skill";
|
|
63
|
-
import { createWebSearch, WEB_SEARCH_MODEL } from "../tools/web
|
|
55
|
+
import { createWebSearch, WEB_SEARCH_MODEL } from "../tools/web/search";
|
|
64
56
|
import { subagentPiToolCandidates } from "../tools/subagent";
|
|
65
57
|
import { resolvePiModel } from "../models";
|
|
66
58
|
import {
|
|
@@ -264,6 +256,16 @@ async function createToolSurface(
|
|
|
264
256
|
if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
|
|
265
257
|
throw new Error(`SpringBrand Tool ${name} execution level is invalid`);
|
|
266
258
|
}
|
|
259
|
+
if (candidate.unattendedExecution) {
|
|
260
|
+
requiredName(
|
|
261
|
+
candidate.unattendedExecution.capability,
|
|
262
|
+
`SpringBrand Tool ${name} unattended capability`,
|
|
263
|
+
);
|
|
264
|
+
requiredName(
|
|
265
|
+
candidate.unattendedExecution.operation,
|
|
266
|
+
`SpringBrand Tool ${name} unattended operation`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
267
269
|
if (tools.has(name)) {
|
|
268
270
|
throw new Error(`Duplicate Runtime SpringBrand Tool: ${name}`);
|
|
269
271
|
}
|
|
@@ -294,17 +296,12 @@ async function createToolSurface(
|
|
|
294
296
|
typeof candidate.tool.execute === "function",
|
|
295
297
|
);
|
|
296
298
|
const codeExecutionCandidates = Object.freeze([...mergeable]);
|
|
297
|
-
const codeExecutionToolNames = new Set(
|
|
298
|
-
codeExecutionCandidates.map(({ tool }) => tool.name),
|
|
299
|
-
);
|
|
300
299
|
const codeExecution = input.codeExecution.create(
|
|
301
300
|
codeExecutionCandidates,
|
|
302
301
|
);
|
|
303
|
-
const
|
|
304
|
-
({ tool }) => !codeExecutionToolNames.has(tool.name),
|
|
305
|
-
);
|
|
302
|
+
const directToolNames = new Set(directVisible.map(({ tool }) => tool.name));
|
|
306
303
|
const routeList = (deferred: boolean) => {
|
|
307
|
-
const matches =
|
|
304
|
+
const matches = directVisible.filter((candidate) =>
|
|
308
305
|
(defersTool?.(candidate.tool.name) === true) === deferred
|
|
309
306
|
);
|
|
310
307
|
return matches.length > 0
|
|
@@ -316,8 +313,9 @@ async function createToolSurface(
|
|
|
316
313
|
};
|
|
317
314
|
const routeGuidance = [
|
|
318
315
|
"## Tool routing",
|
|
319
|
-
"
|
|
316
|
+
"For top-level deferred Tools, use Provider Tool Search with their exact name, then call them directly.",
|
|
320
317
|
"Top-level immediate Tools are already available outside execute; call them directly.",
|
|
318
|
+
"Only Code Mode Tools listed below are callable inside execute through tools.<name>. Tools marked execute-only are not registered as top-level Tools.",
|
|
321
319
|
"",
|
|
322
320
|
"## Top-level deferred Tools",
|
|
323
321
|
routeList(true),
|
|
@@ -325,21 +323,20 @@ async function createToolSurface(
|
|
|
325
323
|
"## Top-level immediate Tools",
|
|
326
324
|
routeList(false),
|
|
327
325
|
"",
|
|
326
|
+
"## Code Mode Tools",
|
|
327
|
+
codeExecutionCandidates.length > 0
|
|
328
|
+
? codeExecutionCandidates.map(({ tool }) =>
|
|
329
|
+
`- \`tools.${tool.name}\` — ${directToolNames.has(tool.name) ? "also available as a top-level Tool" : "execute-only"}`
|
|
330
|
+
).join("\n")
|
|
331
|
+
: "- None.",
|
|
332
|
+
"",
|
|
328
333
|
].join("\n");
|
|
329
|
-
const materializeGuidance = codeExecutionCandidates.some(
|
|
330
|
-
({ tool }) => tool.name === "materialize_skill_resource",
|
|
331
|
-
)
|
|
332
|
-
? "Skill resource copying is available only inside execute through " +
|
|
333
|
-
"`tools.materialize_skill_resource({ name, path, destination })`; " +
|
|
334
|
-
"no Direct Tool is registered for it. " +
|
|
335
|
-
"Copy multiple resources sequentially in one execute so durable replay preserves call order.\n\n"
|
|
336
|
-
: "";
|
|
337
334
|
return Object.freeze({
|
|
338
335
|
candidates: Object.freeze([
|
|
339
336
|
classify(codeExecutionPiToolCandidate({
|
|
340
337
|
...codeExecution,
|
|
341
338
|
description:
|
|
342
|
-
routeGuidance + "\n" +
|
|
339
|
+
routeGuidance + "\n" + codeExecution.description,
|
|
343
340
|
})),
|
|
344
341
|
...directVisible.map(classify),
|
|
345
342
|
]),
|
|
@@ -586,6 +583,9 @@ class RuntimeBuilder {
|
|
|
586
583
|
? { headers: Object.freeze({ ...endpoint.headers }) }
|
|
587
584
|
: {}),
|
|
588
585
|
models: Object.freeze([...endpoint.models]),
|
|
586
|
+
...(endpoint.openRouterProviderPins
|
|
587
|
+
? { openRouterProviderPins: Object.freeze({ ...endpoint.openRouterProviderPins }) }
|
|
588
|
+
: {}),
|
|
589
589
|
...(endpoint.modelInputModalities
|
|
590
590
|
? {
|
|
591
591
|
modelInputModalities: Object.freeze(Object.fromEntries(
|
|
@@ -595,6 +595,22 @@ class RuntimeBuilder {
|
|
|
595
595
|
)),
|
|
596
596
|
}
|
|
597
597
|
: {}),
|
|
598
|
+
...(endpoint.modelMetadata
|
|
599
|
+
? {
|
|
600
|
+
modelMetadata: Object.freeze(Object.fromEntries(
|
|
601
|
+
Object.entries(endpoint.modelMetadata).map(([modelId, metadata]) => [
|
|
602
|
+
modelId,
|
|
603
|
+
Object.freeze({
|
|
604
|
+
...metadata,
|
|
605
|
+
cost: Object.freeze({ ...metadata.cost }),
|
|
606
|
+
...(metadata.thinkingLevelMap
|
|
607
|
+
? { thinkingLevelMap: Object.freeze({ ...metadata.thinkingLevelMap }) }
|
|
608
|
+
: {}),
|
|
609
|
+
}),
|
|
610
|
+
]),
|
|
611
|
+
)),
|
|
612
|
+
}
|
|
613
|
+
: {}),
|
|
598
614
|
}),
|
|
599
615
|
)),
|
|
600
616
|
});
|
|
@@ -632,6 +648,7 @@ class RuntimeBuilder {
|
|
|
632
648
|
: {}),
|
|
633
649
|
hostTools: [
|
|
634
650
|
...this.hostTools,
|
|
651
|
+
...(this.schedule ? schedulePiToolCandidates(this.schedule) : []),
|
|
635
652
|
...subagentPiToolCandidates(
|
|
636
653
|
this.subagents,
|
|
637
654
|
[...this.enabledSubagents],
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
import type { RuntimeProviderPort } from "./ports";
|
|
19
19
|
import { assembleSystemPrompt } from "../lib/prompt";
|
|
20
20
|
import { resolvePiModel } from "../models";
|
|
21
|
-
import type { PiToolCandidate } from "../tools/
|
|
21
|
+
import type { PiToolCandidate } from "../tools/runtime/contracts";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* 收集完动态 Extension/MCP 候选后,一次性生成最终 Tool Surface。
|
package/src/config/definition.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
} from "./profile";
|
|
18
18
|
import type { RuntimeExtensionConfig } from "../context/extension/contracts";
|
|
19
19
|
import type { RuntimeDegradation } from "./degradation";
|
|
20
|
-
import type { PiToolCandidate } from "../tools/
|
|
20
|
+
import type { PiToolCandidate } from "../tools/runtime/contracts";
|
|
21
21
|
import type {
|
|
22
22
|
RuntimeAgentContext,
|
|
23
23
|
RuntimeAgentPlanningContext,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createModels,
|
|
3
|
+
lazyStream,
|
|
3
4
|
type MutableModels,
|
|
4
5
|
} from "@earendil-works/pi-ai";
|
|
5
6
|
import {
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
type PreparePiRuntimeOptions,
|
|
24
25
|
type PreparedPiRuntime,
|
|
25
26
|
} from "./pinning";
|
|
27
|
+
import { refreshModelGuidance } from "./model-guidance";
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
|
|
@@ -89,8 +91,10 @@ export class HarnessRuntimeConfig {
|
|
|
89
91
|
prepared: PreparedPiRuntime,
|
|
90
92
|
pinnedDescriptor: string,
|
|
91
93
|
request: {
|
|
92
|
-
readonly modelSessionId?: string;
|
|
94
|
+
readonly modelSessionId?: string | (() => string);
|
|
93
95
|
readonly onGeneration?: PiGenerationLifecycleObserver;
|
|
96
|
+
readonly assertRequestAllowed?: () => void;
|
|
97
|
+
readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
|
|
94
98
|
} = {},
|
|
95
99
|
) {
|
|
96
100
|
const { state, descriptor } = readPinnedPiRuntime(
|
|
@@ -100,6 +104,19 @@ export class HarnessRuntimeConfig {
|
|
|
100
104
|
);
|
|
101
105
|
const pinnedModel = state.snapshot.pi.model;
|
|
102
106
|
let models!: MutableModels;
|
|
107
|
+
const requestStream = (observer = request.onGeneration) => {
|
|
108
|
+
const stream = withProviderBoundary(
|
|
109
|
+
this.models.streamSimple.bind(this.models), request.modelSessionId, observer,
|
|
110
|
+
);
|
|
111
|
+
return ((model, context, options) => lazyStream(model, async () => {
|
|
112
|
+
options?.signal?.throwIfAborted();
|
|
113
|
+
request.assertRequestAllowed?.();
|
|
114
|
+
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
115
|
+
options?.signal?.throwIfAborted();
|
|
116
|
+
request.assertRequestAllowed?.();
|
|
117
|
+
return stream(model, refreshed, options);
|
|
118
|
+
})) satisfies typeof stream;
|
|
119
|
+
};
|
|
103
120
|
models = new Proxy(this.models, {
|
|
104
121
|
get: (target, property) => {
|
|
105
122
|
if (property === "getModel") {
|
|
@@ -108,15 +125,19 @@ export class HarnessRuntimeConfig {
|
|
|
108
125
|
? pinnedModel
|
|
109
126
|
: target.getModel(provider, modelId);
|
|
110
127
|
}
|
|
111
|
-
if (property === "streamSimple")
|
|
112
|
-
return withProviderBoundary(
|
|
113
|
-
target.streamSimple.bind(target),
|
|
114
|
-
request.modelSessionId,
|
|
115
|
-
request.onGeneration,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
128
|
+
if (property === "streamSimple") return requestStream();
|
|
118
129
|
if (property === "completeSimple") {
|
|
119
|
-
|
|
130
|
+
const compactionModels = new Proxy(models, {
|
|
131
|
+
get: (base, key) => {
|
|
132
|
+
if (key === "streamSimple") return requestStream((event) => {
|
|
133
|
+
request.onGeneration?.(event);
|
|
134
|
+
request.onCompactionGeneration?.(event);
|
|
135
|
+
});
|
|
136
|
+
const value = Reflect.get(base, key, base);
|
|
137
|
+
return typeof value === "function" ? value.bind(base) : value;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
return target.completeSimple.bind(compactionModels);
|
|
120
141
|
}
|
|
121
142
|
const value = Reflect.get(target, property, target);
|
|
122
143
|
return typeof value === "function" ? value.bind(target) : value;
|
|
@@ -132,10 +153,9 @@ export class HarnessRuntimeConfig {
|
|
|
132
153
|
platform: state.snapshot.bindings.platform,
|
|
133
154
|
compaction: {
|
|
134
155
|
enabled: true,
|
|
135
|
-
// Pi compacts above contextWindow - reserveTokens.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
(state.snapshot.pi.model.contextWindow - state.snapshot.pi.model.maxTokens) * 0.85),
|
|
156
|
+
// Pi compacts above contextWindow - reserveTokens.
|
|
157
|
+
reserveTokens: state.snapshot.pi.model.contextWindow -
|
|
158
|
+
compactionThreshold(state.snapshot.pi.model),
|
|
139
159
|
keepRecentTokens: Math.min(
|
|
140
160
|
20_000,
|
|
141
161
|
Math.floor(state.snapshot.pi.model.contextWindow * 0.15),
|
|
@@ -180,3 +200,21 @@ export type {
|
|
|
180
200
|
PreparePiRuntimeOptions,
|
|
181
201
|
PreparedPiRuntime,
|
|
182
202
|
} from "./pinning";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Input tokens above which a Turn compacts its history.
|
|
206
|
+
*
|
|
207
|
+
* OpenRouter rejects some requests well below a model's advertised window
|
|
208
|
+
* (seen from about 393k tokens on DeepSeek V4.1 Flash and 520k-630k on GPT-6
|
|
209
|
+
* Luna), before any provider is chosen. No model waits past this cap, so long
|
|
210
|
+
* Chats compact instead of failing.
|
|
211
|
+
*/
|
|
212
|
+
export const MAX_COMPACTION_THRESHOLD_TOKENS = 300_000;
|
|
213
|
+
|
|
214
|
+
/** 15% headroom within the input budget, capped at {@link MAX_COMPACTION_THRESHOLD_TOKENS}. */
|
|
215
|
+
export function compactionThreshold(model: { contextWindow: number; maxTokens: number }): number {
|
|
216
|
+
return Math.min(
|
|
217
|
+
Math.floor((model.contextWindow - model.maxTokens) * 0.85),
|
|
218
|
+
MAX_COMPACTION_THRESHOLD_TOKENS,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getCurrentTools, normalizeContext, type Context, type Tool, type TranscriptContext } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { PiToolCandidate } from "../tools/runtime/contracts";
|
|
3
|
+
|
|
4
|
+
/** Request-only presentation; Pi remains the source of transcript and active Tool selection. */
|
|
5
|
+
export async function refreshModelGuidance(candidates: readonly PiToolCandidate[], context: Context): Promise<TranscriptContext> {
|
|
6
|
+
const transcript = normalizeContext(context);
|
|
7
|
+
const active = new Set(getCurrentTools(transcript.messages).map((tool) => tool.name));
|
|
8
|
+
const byName = new Map(candidates.map((candidate) => [candidate.tool.name, candidate]));
|
|
9
|
+
const guidance = new Map(await Promise.all(candidates.filter((candidate) => active.has(candidate.tool.name) && candidate.modelGuidance)
|
|
10
|
+
.map(async (candidate) => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
|
|
11
|
+
const messages = transcript.messages.map((message) => message.role !== "system" || !message.toolsAdded ? message : {
|
|
12
|
+
...message,
|
|
13
|
+
toolsAdded: message.toolsAdded.map((tool): Tool => {
|
|
14
|
+
const candidate = byName.get(tool.name), prompt = guidance.get(tool.name);
|
|
15
|
+
return { ...tool, ...(candidate?.deferLoading ? { deferLoading: true as const } : {}), ...(prompt ? { description: prompt.description } : {}) };
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
const sections = [...new Set([...guidance.values()].flatMap((value) => value.systemPrompt ? [value.systemPrompt] : []))];
|
|
19
|
+
return normalizeContext({ messages: sections.length ? [...messages, { role: "system", content: "", sections: { "springbrand.live-services": sections.join("\n\n") }, timestamp: 1 }] : messages });
|
|
20
|
+
}
|