@springbrand/agent-runtime 0.3.0-alpha.1 → 0.3.0-alpha.11
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 +8 -8
- package/src/adapter/cloudflare/agent.ts +160 -14
- package/src/adapter/cloudflare/define-agent.ts +12 -7
- 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 +4 -4
- package/src/adapter/cloudflare/subagent/runner.ts +1 -1
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- 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 +29 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +9 -1
- package/src/config/model-guidance.ts +21 -0
- package/src/config/pinning.ts +22 -3
- package/src/config/ports.ts +27 -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-stream.ts +2 -1
- package/src/conversation/history.ts +2 -0
- package/src/conversation/message/projection.ts +38 -21
- package/src/conversation/state.ts +23 -2
- package/src/harness/session-harness.ts +160 -6
- package/src/index.ts +8 -7
- 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/models/index.ts +32 -10
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +71 -11
- 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 +1 -1
- package/src/submission/repository.ts +64 -4
- 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 +1 -1
- 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/{compiler.ts → runtime/compiler.ts} +84 -147
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
- 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 +1 -1
- 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 +1320 -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/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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AgentHarness as PiAgentHarness,
|
|
5
5
|
type AgentHarnessOptions,
|
|
6
6
|
type AgentLane,
|
|
7
|
+
type AgentToolResult,
|
|
7
8
|
type DriveOutcome,
|
|
8
9
|
type DriveOptions,
|
|
9
10
|
type OpenOperation,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
isRecoverableAssistantError,
|
|
18
19
|
} from "../models";
|
|
19
20
|
import { projectToolOutputForModel } from "../context/memory/budget/gate";
|
|
21
|
+
import type { ArtifactRef } from "../lib/artifacts";
|
|
20
22
|
|
|
21
23
|
export interface SessionHarnessLimits {
|
|
22
24
|
/** Complete model turns committed before one Worker instance releases. */
|
|
@@ -34,6 +36,22 @@ export interface SessionHarnessOptions<TContext extends object | undefined>
|
|
|
34
36
|
modelToolResultExemptions?: readonly string[];
|
|
35
37
|
/** Base provider Session identity, retained except after a server Tool timeout. */
|
|
36
38
|
modelSessionId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Compiled tools omitted from the initial active set. A later successful
|
|
41
|
+
* result may add them through {@link SessionHarnessOptions.toolsToActivate}.
|
|
42
|
+
*/
|
|
43
|
+
onDemandToolNames?: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* Names to add with `setActiveTools` from `after_tool`. That hook runs
|
|
46
|
+
* before the next generation snapshots `activeToolNames`; Pi then writes
|
|
47
|
+
* the difference as transcript `toolsAdded`. Do not call `setActiveTools`
|
|
48
|
+
* from `tool_end`: event delivery is serialized and would deadlock.
|
|
49
|
+
*/
|
|
50
|
+
toolsToActivate?: (
|
|
51
|
+
toolName: string,
|
|
52
|
+
result: AgentToolResult<unknown>,
|
|
53
|
+
isError: boolean,
|
|
54
|
+
) => readonly string[];
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
export type SessionHarnessRunResult = DriveOutcome | {
|
|
@@ -54,6 +72,7 @@ const TOOL_FAILURE_WRAP_UP_INSTRUCTION =
|
|
|
54
72
|
"This turn reached its tool failure limit. Stop calling tools now. Reply with a final message that explains what failed and the exact next step the user should take. Do not start new work.";
|
|
55
73
|
const MAX_TOOL_RETRIES = 3;
|
|
56
74
|
const TURN_FAILURE_CEILING = 8;
|
|
75
|
+
const ARTIFACT_MANIFEST_MAX_CHARS = 12 * 1024;
|
|
57
76
|
|
|
58
77
|
function canonicalJson(value: unknown): string {
|
|
59
78
|
if (Array.isArray(value)) {
|
|
@@ -73,6 +92,10 @@ function toolCallKey(toolName: string, args: unknown): string {
|
|
|
73
92
|
return `${toolName}\0${canonicalJson(args)}`;
|
|
74
93
|
}
|
|
75
94
|
|
|
95
|
+
function uniqueToolNames(names: readonly string[]): string[] {
|
|
96
|
+
return [...new Set(names)];
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
function projectToolResultsForModel(
|
|
77
100
|
messages: readonly AgentMessage[],
|
|
78
101
|
exemptions: ReadonlySet<string>,
|
|
@@ -92,6 +115,66 @@ function projectToolResultsForModel(
|
|
|
92
115
|
return changed ? projected : [...messages];
|
|
93
116
|
}
|
|
94
117
|
|
|
118
|
+
function artifactRefFromMessage(message: AgentMessage): ArtifactRef | undefined {
|
|
119
|
+
if (message.role !== "toolResult" || message.details === null ||
|
|
120
|
+
typeof message.details !== "object" ||
|
|
121
|
+
(message.details as { kind?: unknown }).kind !== "artifact_ref") {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const ref = message.details as Partial<ArtifactRef>;
|
|
125
|
+
if (typeof ref.path !== "string" || typeof ref.hash !== "string" ||
|
|
126
|
+
typeof ref.bytes !== "number" || typeof ref.preview !== "string" ||
|
|
127
|
+
typeof ref.note !== "string") return undefined;
|
|
128
|
+
return {
|
|
129
|
+
kind: "artifact_ref",
|
|
130
|
+
artifactId: typeof ref.artifactId === "string"
|
|
131
|
+
? ref.artifactId
|
|
132
|
+
: `artifact_${ref.hash.slice(0, 24)}`,
|
|
133
|
+
path: ref.path,
|
|
134
|
+
bytes: ref.bytes,
|
|
135
|
+
hash: ref.hash,
|
|
136
|
+
workspaceRevision: typeof ref.workspaceRevision === "string"
|
|
137
|
+
? ref.workspaceRevision
|
|
138
|
+
: "unknown",
|
|
139
|
+
availability: ref.availability === "unavailable"
|
|
140
|
+
? "unavailable"
|
|
141
|
+
: "available",
|
|
142
|
+
preview: ref.preview,
|
|
143
|
+
note: ref.note,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function artifactManifestMessage(
|
|
148
|
+
refs: ReadonlyMap<string, ArtifactRef>,
|
|
149
|
+
): AgentMessage | undefined {
|
|
150
|
+
if (refs.size === 0) return undefined;
|
|
151
|
+
const lines = [
|
|
152
|
+
"<workspace_artifact_manifest>",
|
|
153
|
+
"These are lightweight references to prior Tool output. Do not treat previews as complete. Read an available artifact with the Workspace readFile/readFilePage tool only when the current request needs its evidence.",
|
|
154
|
+
];
|
|
155
|
+
let omitted = 0;
|
|
156
|
+
for (const ref of refs.values()) {
|
|
157
|
+
const preview = ref.preview.replace(/[\r\n]+/g, " ").slice(0, 160);
|
|
158
|
+
const line = [
|
|
159
|
+
`path=${ref.path}`,
|
|
160
|
+
...(ref.availability === "unavailable" ? ["unavailable"] : []),
|
|
161
|
+
`preview=${preview}`,
|
|
162
|
+
].join(" ");
|
|
163
|
+
if ([...lines, line, "</workspace_artifact_manifest>"].join("\n").length > ARTIFACT_MANIFEST_MAX_CHARS) {
|
|
164
|
+
omitted += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
lines.push(line);
|
|
168
|
+
}
|
|
169
|
+
if (omitted > 0) lines.push(`omitted=${omitted} additional artifact references due to manifest budget`);
|
|
170
|
+
lines.push("</workspace_artifact_manifest>");
|
|
171
|
+
return {
|
|
172
|
+
role: "user",
|
|
173
|
+
content: lines.join("\n"),
|
|
174
|
+
timestamp: 1,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
95
178
|
function deferred<T>(): {
|
|
96
179
|
promise: Promise<T>;
|
|
97
180
|
resolve(value: T): void;
|
|
@@ -126,6 +209,15 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
126
209
|
private readonly modelSessionId: string | undefined;
|
|
127
210
|
private readonly wrapUpInstruction: string;
|
|
128
211
|
private readonly configuredToolNames: string[];
|
|
212
|
+
private readonly compiledToolNames: ReadonlySet<string>;
|
|
213
|
+
private readonly onDemandToolNames: ReadonlySet<string>;
|
|
214
|
+
private readonly toolsToActivate:
|
|
215
|
+
| ((
|
|
216
|
+
toolName: string,
|
|
217
|
+
result: AgentToolResult<unknown>,
|
|
218
|
+
isError: boolean,
|
|
219
|
+
) => readonly string[])
|
|
220
|
+
| undefined;
|
|
129
221
|
private readonly modelToolResultExemptions: ReadonlySet<string>;
|
|
130
222
|
private readonly toolFailures = new Map<string, number>();
|
|
131
223
|
private totalToolFailures = 0;
|
|
@@ -144,6 +236,7 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
144
236
|
private readonly releaseRequest = deferred<"approval" | "interaction">();
|
|
145
237
|
private readonly activeTools = new Set<string>();
|
|
146
238
|
private readonly toolsIdle = new Set<() => void>();
|
|
239
|
+
private readonly artifactRefs = new Map<string, ArtifactRef>();
|
|
147
240
|
|
|
148
241
|
private constructor(
|
|
149
242
|
readonly harness: PiAgentHarness<TContext>,
|
|
@@ -169,18 +262,22 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
169
262
|
DEFAULT_WRAP_UP_INSTRUCTION;
|
|
170
263
|
this.configuredToolNames = options.activeToolNames ??
|
|
171
264
|
(options.tools ?? []).map(({ name }) => name);
|
|
265
|
+
this.compiledToolNames = new Set((options.tools ?? []).map(({ name }) => name));
|
|
266
|
+
this.onDemandToolNames = new Set(options.onDemandToolNames ?? []);
|
|
267
|
+
this.toolsToActivate = options.toolsToActivate;
|
|
172
268
|
this.modelToolResultExemptions = new Set(
|
|
173
269
|
options.modelToolResultExemptions ?? [],
|
|
174
270
|
);
|
|
175
271
|
}
|
|
176
272
|
|
|
177
273
|
private async operationEntries(context: Context): Promise<Entry[]> {
|
|
274
|
+
// Stop boundaries follow traversal order; walk back from the current tip.
|
|
178
275
|
const entries = await this.lane.findEntries({
|
|
179
276
|
...(this.operationStartTip === null ? {} : { stopAtId: this.operationStartTip }),
|
|
180
277
|
type: "message",
|
|
181
|
-
order: "
|
|
278
|
+
order: "newestFirst",
|
|
182
279
|
}, context);
|
|
183
|
-
return entries.filter(({ id }) => id !== this.operationStartTip);
|
|
280
|
+
return entries.filter(({ id }) => id !== this.operationStartTip).reverse();
|
|
184
281
|
}
|
|
185
282
|
|
|
186
283
|
private async restoreToolGovernance(context: Context): Promise<void> {
|
|
@@ -255,6 +352,8 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
255
352
|
limits: _limits,
|
|
256
353
|
modelToolResultExemptions: _modelToolResultExemptions,
|
|
257
354
|
modelSessionId: _modelSessionId,
|
|
355
|
+
onDemandToolNames: _onDemandToolNames,
|
|
356
|
+
toolsToActivate: _toolsToActivate,
|
|
258
357
|
...harnessOptions
|
|
259
358
|
} = options;
|
|
260
359
|
let runtime!: SessionHarness<TContext>;
|
|
@@ -288,6 +387,12 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
288
387
|
}
|
|
289
388
|
|
|
290
389
|
private async install(context: Context): Promise<void> {
|
|
390
|
+
for (const entry of await this.lane.findEntries({ type: "message", order: "oldestFirst" }, context)) {
|
|
391
|
+
if (entry.type === "message") {
|
|
392
|
+
const ref = artifactRefFromMessage(entry.message);
|
|
393
|
+
if (ref) this.artifactRefs.set(ref.artifactId, ref);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
291
396
|
if (this._openOperation) {
|
|
292
397
|
const watch = await this.lane.watch(context);
|
|
293
398
|
this.operationStartTip = watch.snapshot.operation?.fromTipId ?? null;
|
|
@@ -316,19 +421,21 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
316
421
|
event.messages,
|
|
317
422
|
this.modelToolResultExemptions,
|
|
318
423
|
);
|
|
424
|
+
const manifest = artifactManifestMessage(this.artifactRefs);
|
|
319
425
|
const instruction = this.toolFailureWrapUp
|
|
320
426
|
? TOOL_FAILURE_WRAP_UP_INSTRUCTION
|
|
321
427
|
: this.wrappingUp
|
|
322
428
|
? this.wrapUpInstruction
|
|
323
429
|
: undefined;
|
|
430
|
+
const contextMessages = manifest ? [...messages, manifest] : messages;
|
|
324
431
|
return {
|
|
325
432
|
messages: instruction
|
|
326
|
-
? [...
|
|
433
|
+
? [...contextMessages, {
|
|
327
434
|
role: "user" as const,
|
|
328
435
|
content: instruction,
|
|
329
436
|
timestamp: 1,
|
|
330
437
|
}]
|
|
331
|
-
:
|
|
438
|
+
: contextMessages,
|
|
332
439
|
};
|
|
333
440
|
});
|
|
334
441
|
this.harness.hooks.on("before_tool", (event) => {
|
|
@@ -353,7 +460,12 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
353
460
|
};
|
|
354
461
|
});
|
|
355
462
|
this.harness.events.on("entry_added", (event) => {
|
|
356
|
-
if (event.lane
|
|
463
|
+
if (event.lane !== this.lane.name) return;
|
|
464
|
+
this.recordCanonicalToolOutcome(event.entry);
|
|
465
|
+
if (event.entry.type === "message") {
|
|
466
|
+
const ref = artifactRefFromMessage(event.entry.message);
|
|
467
|
+
if (ref) this.artifactRefs.set(ref.artifactId, ref);
|
|
468
|
+
}
|
|
357
469
|
});
|
|
358
470
|
this.harness.events.on("turn_end", async (_event, eventContext) => {
|
|
359
471
|
const total = await this.completedModelTurns(eventContext);
|
|
@@ -371,6 +483,17 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
371
483
|
}
|
|
372
484
|
await this.harness.close(eventContext);
|
|
373
485
|
});
|
|
486
|
+
this.harness.hooks.on("after_tool", async (event, eventContext) => {
|
|
487
|
+
await this.activateToolsFromResult({
|
|
488
|
+
toolName: event.toolName,
|
|
489
|
+
result: {
|
|
490
|
+
content: event.content,
|
|
491
|
+
details: event.details,
|
|
492
|
+
},
|
|
493
|
+
isError: event.isError,
|
|
494
|
+
}, eventContext);
|
|
495
|
+
return undefined;
|
|
496
|
+
});
|
|
374
497
|
this.harness.events.on("tool_start", (event) => {
|
|
375
498
|
this.activeTools.add(event.toolCallId);
|
|
376
499
|
});
|
|
@@ -393,6 +516,34 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
393
516
|
await new Promise<void>((resolve) => this.toolsIdle.add(resolve));
|
|
394
517
|
}
|
|
395
518
|
|
|
519
|
+
private async activeToolsForNewOperation(context: Context): Promise<string[]> {
|
|
520
|
+
const previous = await this.lane.getActiveTools(context);
|
|
521
|
+
const keepOnDemand = previous.filter((name) =>
|
|
522
|
+
this.onDemandToolNames.has(name) && this.compiledToolNames.has(name)
|
|
523
|
+
);
|
|
524
|
+
return uniqueToolNames([...this.configuredToolNames, ...keepOnDemand]);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private async activateToolsFromResult(
|
|
528
|
+
event: {
|
|
529
|
+
toolName: string;
|
|
530
|
+
result: AgentToolResult<unknown>;
|
|
531
|
+
isError: boolean;
|
|
532
|
+
},
|
|
533
|
+
context: Context,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
const requested = this.toolsToActivate?.(
|
|
536
|
+
event.toolName,
|
|
537
|
+
event.result,
|
|
538
|
+
event.isError,
|
|
539
|
+
) ?? [];
|
|
540
|
+
const allowed = requested.filter((name) => this.compiledToolNames.has(name));
|
|
541
|
+
if (allowed.length === 0) return;
|
|
542
|
+
const current = await this.lane.getActiveTools(context);
|
|
543
|
+
if (allowed.every((name) => current.includes(name))) return;
|
|
544
|
+
await this.lane.setActiveTools(uniqueToolNames([...current, ...allowed]), context);
|
|
545
|
+
}
|
|
546
|
+
|
|
396
547
|
private async completedModelTurns(context: Context): Promise<number> {
|
|
397
548
|
return (await this.operationEntries(context)).filter((entry) =>
|
|
398
549
|
entry.type === "message" && entry.message.role === "assistant"
|
|
@@ -417,7 +568,10 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
417
568
|
this.toolFailureWrapUp = false;
|
|
418
569
|
this.toolFailureWrapUpAt = undefined;
|
|
419
570
|
this.releaseReason = undefined;
|
|
420
|
-
await this.lane.setActiveTools(
|
|
571
|
+
await this.lane.setActiveTools(
|
|
572
|
+
await this.activeToolsForNewOperation(context),
|
|
573
|
+
context,
|
|
574
|
+
);
|
|
421
575
|
const admission = await this.lane.accept(request, context);
|
|
422
576
|
if (!admission.ok) throw admission.error;
|
|
423
577
|
operationId = admission.value.operationId;
|
package/src/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { RuntimeAssemblyView } from "./config/assembly-view";
|
|
|
51
51
|
export * from "./adapter/cloudflare/define-agent";
|
|
52
52
|
export type { ToolInteractionResponsePreparation } from "./adapter/cloudflare/agent";
|
|
53
53
|
export * from "./conversation/state";
|
|
54
|
-
export * from "./tools/workspace
|
|
54
|
+
export * from "./tools/workspace/versioning";
|
|
55
55
|
export * from "./lib/execution-level";
|
|
56
56
|
export type {
|
|
57
57
|
RuntimeAssemblyInput,
|
|
@@ -84,16 +84,17 @@ export type {
|
|
|
84
84
|
} from "./lib/host-actions";
|
|
85
85
|
export {
|
|
86
86
|
AGENT_TYPES,
|
|
87
|
-
} from "./tools/subagent
|
|
87
|
+
} from "./tools/subagent/types/registry";
|
|
88
88
|
export type {
|
|
89
89
|
AgentType,
|
|
90
|
-
} from "./tools/subagent
|
|
90
|
+
} from "./tools/subagent/types/contract";
|
|
91
91
|
export type {
|
|
92
92
|
PiToolCandidate,
|
|
93
93
|
ToolExposureMode,
|
|
94
94
|
} from "./tools";
|
|
95
95
|
export { basePiToolCandidates } from "./tools";
|
|
96
96
|
export { createPiDeclaredToolCandidate } from "./tools";
|
|
97
|
+
export { harnessToolActivation } from "./tools";
|
|
97
98
|
export type {
|
|
98
99
|
PiDeclaredTool,
|
|
99
100
|
PiDeclaredToolCallContext,
|
|
@@ -129,15 +130,15 @@ export type {
|
|
|
129
130
|
TemporaryAgentLaunch,
|
|
130
131
|
TemporaryAgentRequest,
|
|
131
132
|
TemporaryAgentRunContext,
|
|
132
|
-
} from "./tools/temporary
|
|
133
|
-
export { TEMPORARY_AGENT_LAUNCH_KEY } from "./tools/temporary
|
|
133
|
+
} from "./tools/subagent/temporary/core";
|
|
134
|
+
export { TEMPORARY_AGENT_LAUNCH_KEY } from "./tools/subagent/temporary/core";
|
|
134
135
|
export {
|
|
135
136
|
temporaryAgentExtensionIsSafe,
|
|
136
137
|
temporaryAgentToolAllowed,
|
|
137
|
-
} from "./tools/temporary
|
|
138
|
+
} from "./tools/subagent/temporary/runner";
|
|
138
139
|
export {
|
|
139
140
|
createTemporaryAgentWorkspace,
|
|
140
|
-
} from "./tools/temporary
|
|
141
|
+
} from "./tools/subagent/temporary/workspace";
|
|
141
142
|
export {
|
|
142
143
|
createPiModels,
|
|
143
144
|
modelRequestUrl,
|
package/src/lib/artifacts.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
/** Stable narrow reference shared by messages, RPC responses and the UI. */
|
|
5
5
|
export interface ArtifactRef {
|
|
6
6
|
kind: "artifact_ref";
|
|
7
|
+
artifactId: string;
|
|
7
8
|
path: string;
|
|
8
9
|
bytes: number;
|
|
9
10
|
hash: string;
|
|
11
|
+
workspaceRevision: string;
|
|
12
|
+
availability: "available" | "unavailable";
|
|
10
13
|
preview: string;
|
|
11
14
|
note: string;
|
|
12
15
|
}
|
|
@@ -14,6 +17,15 @@ export interface ArtifactRef {
|
|
|
14
17
|
export interface SpillWorkspace {
|
|
15
18
|
/** 把溢出的 Artifact 内容写入指定 Workspace 路径。 */
|
|
16
19
|
writeFile(path: string, content: string, mimeType?: string): Promise<void>;
|
|
20
|
+
/** 写入后验证物理文件,防止只持久化了一个不存在或被截断的引用。 */
|
|
21
|
+
stat?(path: string): Promise<{
|
|
22
|
+
type: "file" | "directory" | "symlink";
|
|
23
|
+
size: number;
|
|
24
|
+
updatedAt: number;
|
|
25
|
+
contentVersion?: string;
|
|
26
|
+
} | null>;
|
|
27
|
+
/** 写入后验证内容摘要;Artifact 全部是 UTF-8 文本。 */
|
|
28
|
+
readFileBytes?(path: string): Promise<Uint8Array | null>;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
/**
|
|
@@ -33,6 +45,10 @@ export async function sha256Hex(text: string): Promise<string> {
|
|
|
33
45
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
function artifactId(hash: string): string {
|
|
49
|
+
return `artifact_${hash.slice(0, 24)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
/**
|
|
37
53
|
* 把 Tool 结果转成可存储的文本,并选择 `.txt` 或 `.json` 扩展名。
|
|
38
54
|
*
|
|
@@ -52,3 +68,5 @@ export function serializeOutput(output: unknown): { text: string; ext: "txt" | "
|
|
|
52
68
|
return { text: String(output), ext: "txt" };
|
|
53
69
|
}
|
|
54
70
|
}
|
|
71
|
+
|
|
72
|
+
export { artifactId };
|
|
@@ -25,3 +25,16 @@ export function maxExecutionLevel(
|
|
|
25
25
|
)
|
|
26
26
|
]!;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/** The most permissive level shared by an Agent and a Scheduled Task ceiling. */
|
|
30
|
+
export function minExecutionLevel(
|
|
31
|
+
left: ExecutionLevel,
|
|
32
|
+
right: ExecutionLevel,
|
|
33
|
+
): ExecutionLevel {
|
|
34
|
+
return EXECUTION_LEVELS[
|
|
35
|
+
Math.min(
|
|
36
|
+
EXECUTION_LEVELS.indexOf(left),
|
|
37
|
+
EXECUTION_LEVELS.indexOf(right),
|
|
38
|
+
)
|
|
39
|
+
]!;
|
|
40
|
+
}
|
package/src/lib/prompt.ts
CHANGED
|
@@ -17,9 +17,8 @@ export const PERSONA =
|
|
|
17
17
|
// Names the stable runtime environment.
|
|
18
18
|
export const RUNTIME =
|
|
19
19
|
"Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
|
|
20
|
-
"instrument — code there runs with
|
|
21
|
-
"
|
|
22
|
-
"requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
|
|
20
|
+
"instrument — code there runs with your workspace filesystem (state.*) and the tools.* methods listed in its own description. " +
|
|
21
|
+
"Use it for tool composition, parsing a payload, or computing over a file. Write plain " +
|
|
23
22
|
"JavaScript — the sandbox evaluates " +
|
|
24
23
|
"it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
|
|
25
24
|
"is no Python interpreter or package manager to invoke. Use the JS/Workers equivalent of what you " +
|
|
@@ -58,8 +57,7 @@ export const TOOLS =
|
|
|
58
57
|
"use state.* for workspace file operations, tools.* for other host capabilities, and sequential calls for repeated or durable operations. " +
|
|
59
58
|
"Top-level-only Tools remain Direct even when called repeatedly. " +
|
|
60
59
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
61
|
-
"and public URL analysis
|
|
62
|
-
"API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
|
|
60
|
+
"and public URL analysis; do not use execute for ordinary web " +
|
|
63
61
|
"searches. When execute is absent, use the actually exposed Direct Tools. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
|
|
64
62
|
"package managers, system commands, builds, tests and background processes; its filesystem is temporary, " +
|
|
65
63
|
"persistent inputs are copied in automatically on first use, /userspace is read-only, and only explicitly " +
|
|
@@ -70,7 +68,10 @@ export const TOOLS =
|
|
|
70
68
|
"prioritize verification, saving durable results, and the final response. " +
|
|
71
69
|
"When related Tool calls can run independently, all are available inside execute, and execute is present, run them inside that execute rather than as parallel top-level calls; " +
|
|
72
70
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
73
|
-
"code, cite it as file_path:line_number."
|
|
71
|
+
"code, cite it as file_path:line_number. " +
|
|
72
|
+
"When schedule tools are available, prepare a standalone Requirement (at most 1000 Unicode characters), a name (at most 50), a time rule and an explicit timezone. Ask for missing information. " +
|
|
73
|
+
"Use local once/daily/weekly rules for new scheduled tasks. The Host prepares the authorization scope and the user must confirm before saving; never claim a task was created before the Tool succeeds. " +
|
|
74
|
+
"One source Chat may create multiple tasks. Before editing, list tasks and use the exact Task ID; ask when the target is ambiguous. Never attach source history or hidden context to the Requirement.";
|
|
74
75
|
|
|
75
76
|
// A real browser is the only way to observe what a page actually does, and the
|
|
76
77
|
// shared failure mode is a browser tool that returns nothing and looks clean.
|
|
@@ -117,10 +118,8 @@ export const INTERACTION =
|
|
|
117
118
|
"without that answer; required defaults to false. At most one question may request attachments. Do not add an accept field or tell the user to upload through the normal chat composer; " +
|
|
118
119
|
"continue the same turn when its result arrives. " +
|
|
119
120
|
"Don't use it when a sensible default lets you proceed. " +
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"After it returns, write your complete final answer and make no further Tool calls. Never for small talk or while a " +
|
|
123
|
-
"task is still in progress; it is non-blocking, unlike ask_user.";
|
|
121
|
+
"When a substantive task (report, analysis, or multi-step job) is complete, call suggest_followups once before the final answer. " +
|
|
122
|
+
"Never use it for small talk, quick answers, or while a task is still in progress; it is non-blocking, unlike ask_user.";
|
|
124
123
|
|
|
125
124
|
// Separates short always-on memory from indexed long-tail workspace files.
|
|
126
125
|
export const MEMORY =
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Conservative token estimate for prompt budgeting, not provider billing. */
|
|
2
|
+
export function estimateStringTokens(text: string): number {
|
|
3
|
+
if (!text) return 0;
|
|
4
|
+
return Math.ceil(Math.max(
|
|
5
|
+
text.length / 4,
|
|
6
|
+
text.split(/\s+/u).filter(Boolean).length * 1.3,
|
|
7
|
+
));
|
|
8
|
+
}
|
package/src/models/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import {
|
|
3
3
|
createModels,
|
|
4
4
|
createProvider,
|
|
5
|
+
getCurrentSystemPrompt,
|
|
5
6
|
isRetryableAssistantError,
|
|
6
7
|
lazyStream,
|
|
7
8
|
type Api,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
type Model,
|
|
12
13
|
type MutableModels,
|
|
13
14
|
type ProviderStreams,
|
|
15
|
+
type SimpleStreamOptions,
|
|
16
|
+
type TranscriptContext,
|
|
14
17
|
} from "@earendil-works/pi-ai";
|
|
15
18
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
16
19
|
import {
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
openaiCodexProvider,
|
|
39
42
|
} from "@earendil-works/pi-ai/providers/openai-codex";
|
|
40
43
|
import {
|
|
41
|
-
|
|
44
|
+
resolveCloudflareModel,
|
|
42
45
|
} from "@earendil-works/pi-ai/providers/cloudflare-stream";
|
|
43
46
|
import {
|
|
44
47
|
ChatStreamStalledError,
|
|
@@ -51,6 +54,10 @@ import type {
|
|
|
51
54
|
} from "../config/ports";
|
|
52
55
|
import { modelInputMessages } from "../conversation/message/model-input";
|
|
53
56
|
import { openRouterMessagesApi } from "./openrouter/messages";
|
|
57
|
+
import {
|
|
58
|
+
overlayProviderStreams,
|
|
59
|
+
remapProviderStreamModels,
|
|
60
|
+
} from "./provider-streams";
|
|
54
61
|
|
|
55
62
|
const CATALOGS = {
|
|
56
63
|
"openai-chat": openaiProvider().getModels(),
|
|
@@ -130,6 +137,12 @@ export type PiGenerationLifecycleObserver = (
|
|
|
130
137
|
event: PiGenerationLifecycleEvent,
|
|
131
138
|
) => void;
|
|
132
139
|
|
|
140
|
+
type ProviderBoundStream = (
|
|
141
|
+
model: Model<Api>,
|
|
142
|
+
context: Context | TranscriptContext,
|
|
143
|
+
options?: SimpleStreamOptions,
|
|
144
|
+
) => ReturnType<StreamFn>;
|
|
145
|
+
|
|
133
146
|
export function structuredOutputStream(
|
|
134
147
|
streamFn: StreamFn,
|
|
135
148
|
model: Model<Api>,
|
|
@@ -490,16 +503,19 @@ export function modelRequestUrl(model: Model<Api>): string {
|
|
|
490
503
|
* observer 的异常被隔离,不能改变模型执行结果。
|
|
491
504
|
*/
|
|
492
505
|
export function withProviderBoundary(
|
|
493
|
-
streamFn:
|
|
506
|
+
streamFn: ProviderBoundStream,
|
|
494
507
|
defaultSessionId?: string,
|
|
495
508
|
onGeneration?: PiGenerationLifecycleObserver,
|
|
496
|
-
):
|
|
509
|
+
): ProviderBoundStream {
|
|
497
510
|
return (model, context, options) =>
|
|
498
511
|
lazyStream(model, async () => {
|
|
499
512
|
const requestId = crypto.randomUUID();
|
|
500
513
|
const startedAt = Date.now();
|
|
501
514
|
const sessionId = options?.sessionId ?? defaultSessionId;
|
|
502
515
|
const url = modelRequestUrl(model);
|
|
516
|
+
const systemPrompt = "systemPrompt" in context && typeof context.systemPrompt === "string"
|
|
517
|
+
? context.systemPrompt
|
|
518
|
+
: getCurrentSystemPrompt(context.messages);
|
|
503
519
|
const notify = (event: PiGenerationLifecycleEvent) => {
|
|
504
520
|
try {
|
|
505
521
|
onGeneration?.(event);
|
|
@@ -516,7 +532,7 @@ export function withProviderBoundary(
|
|
|
516
532
|
model: model.id,
|
|
517
533
|
url,
|
|
518
534
|
input: {
|
|
519
|
-
...(
|
|
535
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
520
536
|
messages: context.messages,
|
|
521
537
|
},
|
|
522
538
|
});
|
|
@@ -708,7 +724,10 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
|
|
|
708
724
|
return openAICodexResponsesApi();
|
|
709
725
|
}
|
|
710
726
|
})();
|
|
711
|
-
const contextFor = (
|
|
727
|
+
const contextFor = (
|
|
728
|
+
model: Model<Api>,
|
|
729
|
+
context: TranscriptContext,
|
|
730
|
+
): TranscriptContext => ({
|
|
712
731
|
...context,
|
|
713
732
|
messages: modelInputMessages(
|
|
714
733
|
context.messages,
|
|
@@ -716,12 +735,12 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
|
|
|
716
735
|
adapterHostedInputModalities(protocol),
|
|
717
736
|
),
|
|
718
737
|
});
|
|
719
|
-
return {
|
|
738
|
+
return overlayProviderStreams(api, {
|
|
720
739
|
stream: (model, context, options) =>
|
|
721
740
|
api.stream(model, contextFor(model, context), options),
|
|
722
741
|
streamSimple: (model, context, options) =>
|
|
723
742
|
api.streamSimple(model, contextFor(model, context), options),
|
|
724
|
-
};
|
|
743
|
+
});
|
|
725
744
|
}
|
|
726
745
|
|
|
727
746
|
function anthropicMessagesModelForOpenRouter(
|
|
@@ -790,6 +809,7 @@ function configuredModel(
|
|
|
790
809
|
? {
|
|
791
810
|
compat: {
|
|
792
811
|
...anthropicMessagesCompat,
|
|
812
|
+
// OpenRouter rejects Anthropic's mid-conversation effort messages and block binding.
|
|
793
813
|
supportsMidConvoEffort: false,
|
|
794
814
|
supportsEagerToolInputStreaming: false,
|
|
795
815
|
supportsStrictTools:
|
|
@@ -865,9 +885,11 @@ export function configurePiModels(
|
|
|
865
885
|
models: endpoint.models.map((modelId) =>
|
|
866
886
|
configuredModel(endpoint, index, modelId),
|
|
867
887
|
),
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
888
|
+
// remapProviderStreamModels 在运行时把 baseURL 里的
|
|
889
|
+
// {CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID} 占位符替换成真实值;
|
|
890
|
+
// 如果 baseURL 不含占位符则直接透传。Pi 自带的 cloudflareStreams 只保留
|
|
891
|
+
// stream / streamSimple,这里 spread 其余方法以免丢掉 deferred 入口。
|
|
892
|
+
api: remapProviderStreamModels(piApiFor(endpoint), resolveCloudflareModel),
|
|
871
893
|
})
|
|
872
894
|
);
|
|
873
895
|
models.clearProviders();
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
Context,
|
|
3
2
|
Model,
|
|
4
3
|
ProviderStreams,
|
|
5
4
|
StreamOptions,
|
|
5
|
+
TranscriptContext,
|
|
6
6
|
} from "@earendil-works/pi-ai";
|
|
7
|
+
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
7
8
|
import {
|
|
8
9
|
anthropicMessagesApi,
|
|
9
10
|
} from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
11
|
+
import { overlayProviderStreams } from "../provider-streams";
|
|
10
12
|
|
|
11
13
|
type Payload = Record<string, unknown>;
|
|
12
14
|
|
|
@@ -21,14 +23,16 @@ function record(value: unknown): Payload | undefined {
|
|
|
21
23
|
function decoratePayload(
|
|
22
24
|
payload: unknown,
|
|
23
25
|
model: Model<"anthropic-messages">,
|
|
24
|
-
context:
|
|
26
|
+
context: TranscriptContext,
|
|
25
27
|
sessionId?: string,
|
|
26
28
|
): unknown {
|
|
27
29
|
const body = record(payload);
|
|
28
30
|
if (!body) throw new Error("OpenRouter Messages payload must be an object");
|
|
29
31
|
|
|
30
32
|
const deferredNames = new Set(
|
|
31
|
-
context.
|
|
33
|
+
getCurrentTools(context.messages)
|
|
34
|
+
.filter((tool) => tool.deferLoading)
|
|
35
|
+
.map((tool) => tool.name),
|
|
32
36
|
);
|
|
33
37
|
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
34
38
|
const immediate: unknown[] = [];
|
|
@@ -80,7 +84,7 @@ function decoratePayload(
|
|
|
80
84
|
|
|
81
85
|
function openRouterOptions(
|
|
82
86
|
model: Model<"anthropic-messages">,
|
|
83
|
-
context:
|
|
87
|
+
context: TranscriptContext,
|
|
84
88
|
options: StreamOptions | undefined,
|
|
85
89
|
): StreamOptions {
|
|
86
90
|
const apiKey = options?.apiKey;
|
|
@@ -107,9 +111,10 @@ function openRouterOptions(
|
|
|
107
111
|
};
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
export function openRouterMessagesApi(
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
export function openRouterMessagesApi(
|
|
115
|
+
anthropic: ProviderStreams = anthropicMessagesApi(),
|
|
116
|
+
): ProviderStreams {
|
|
117
|
+
return overlayProviderStreams(anthropic, {
|
|
113
118
|
stream: (model, context, options) =>
|
|
114
119
|
anthropic.stream(
|
|
115
120
|
model,
|
|
@@ -130,5 +135,5 @@ export function openRouterMessagesApi(): ProviderStreams {
|
|
|
130
135
|
options,
|
|
131
136
|
),
|
|
132
137
|
),
|
|
133
|
-
};
|
|
138
|
+
});
|
|
134
139
|
}
|