@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21
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 +233 -179
- package/src/adapter/cloudflare/define-agent.ts +252 -104
- 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 +21 -2
- 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/activation.ts +13 -15
- 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 +77 -49
- package/src/config/index.ts +1 -1
- package/src/config/model-guidance.ts +20 -0
- package/src/config/ports.ts +57 -7
- package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
- package/src/config/public-contracts.ts +2 -2
- package/src/config/runtime-config.ts +2 -1
- 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 +28 -4
- package/src/conversation/message/conversion.ts +46 -5
- 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 +269 -445
- 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 +20 -30
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +97 -49
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- 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 +319 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SubmissionRecord } from "@earendil-works/pi-durable";
|
|
2
|
+
export type CancelQueuedResult = { kind: "cancelled" | "already_consumed" | "not_found" };
|
|
2
3
|
import type { UIMessage } from "ai";
|
|
3
4
|
import type { ConversationHistory } from "../conversation/history";
|
|
4
5
|
import type { SubmitMessageOptions } from "./admission";
|
|
@@ -12,7 +13,8 @@ interface PendingSteerDeliveryOptions {
|
|
|
12
13
|
steers: SteerRepository;
|
|
13
14
|
messageUi: MessageUiRepository;
|
|
14
15
|
transaction<T>(run: () => T): T;
|
|
15
|
-
enqueue(
|
|
16
|
+
enqueue(steer: PendingSteer): Promise<SubmissionRecord>;
|
|
17
|
+
find(messageId: string): Promise<SubmissionRecord | undefined>;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** Reconciles Host steer receipts with Pi queue identities, without owning execution. */
|
|
@@ -20,66 +22,27 @@ export class PendingSteerDelivery {
|
|
|
20
22
|
constructor(private readonly options: PendingSteerDeliveryOptions) {}
|
|
21
23
|
|
|
22
24
|
async queue(steer: PendingSteer): Promise<void> {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
this.link(steer, entryId);
|
|
25
|
+
const record = await this.options.enqueue(steer);
|
|
26
|
+
this.options.transaction(() => this.options.steers.linkHarnessEntry(steer.messageId, String(record.id)));
|
|
27
|
+
this.settleRecord(record);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const steer = this.options.steers.findByHarnessEntryId(entryId);
|
|
29
|
+
settleRecord(record: SubmissionRecord): boolean {
|
|
30
|
+
if (!record.requestId?.startsWith("steer:") || record.entry === undefined) return false;
|
|
31
|
+
const steer = this.options.steers.findByMessageId(record.requestId.slice(6));
|
|
32
32
|
if (!steer) return false;
|
|
33
33
|
this.options.transaction(() => {
|
|
34
|
-
this.
|
|
34
|
+
if (steer.uiMessageJson) this.options.messageUi.upsert(String(record.entry), steer.uiMessageJson);
|
|
35
35
|
this.options.steers.deleteByMessageId(steer.messageId);
|
|
36
36
|
});
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const transcriptIds = new Set(snapshot.transcript.map(({ id }) => id));
|
|
47
|
-
const linkedIds = new Set(pending.flatMap(({ harnessEntryId }) =>
|
|
48
|
-
harnessEntryId ? [harnessEntryId] : []
|
|
49
|
-
));
|
|
50
|
-
const unlinked = snapshot.queues.filter((entry) =>
|
|
51
|
-
entry.kind === "steer" && entry.type === "message" &&
|
|
52
|
-
!linkedIds.has(entry.entryId)
|
|
53
|
-
);
|
|
54
|
-
// Both durable queues preserve acceptance order; identical text is not identity.
|
|
55
|
-
for (const steer of pending) {
|
|
56
|
-
if (steer.harnessEntryId && transcriptIds.has(steer.harnessEntryId)) {
|
|
57
|
-
this.settle(steer.harnessEntryId);
|
|
58
|
-
} else if (steer.harnessEntryId && queuedIds.has(steer.harnessEntryId)) {
|
|
59
|
-
this.options.transaction(() =>
|
|
60
|
-
this.persistSidecar(steer, steer.harnessEntryId!)
|
|
61
|
-
);
|
|
62
|
-
} else {
|
|
63
|
-
const queued = !steer.harnessEntryId ? unlinked.shift() : undefined;
|
|
64
|
-
if (queued) this.link(steer, queued.entryId);
|
|
65
|
-
else await this.queue(steer);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private link(steer: PendingSteer, entryId: string): void {
|
|
71
|
-
this.options.transaction(() => {
|
|
72
|
-
if (steer.harnessEntryId && steer.harnessEntryId !== entryId) {
|
|
73
|
-
this.options.messageUi.delete(steer.harnessEntryId);
|
|
74
|
-
}
|
|
75
|
-
this.options.steers.linkHarnessEntry(steer.messageId, entryId);
|
|
76
|
-
this.persistSidecar(steer, entryId);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private persistSidecar(steer: PendingSteer, entryId: string): void {
|
|
81
|
-
if (steer.uiMessageJson) {
|
|
82
|
-
this.options.messageUi.upsert(entryId, steer.uiMessageJson);
|
|
39
|
+
async reconcile(submissionId: string): Promise<void> {
|
|
40
|
+
for (const steer of this.options.steers.listForSubmission(submissionId)) {
|
|
41
|
+
const existing = await this.options.find(steer.messageId);
|
|
42
|
+
if (existing) {
|
|
43
|
+
this.options.transaction(() => this.options.steers.linkHarnessEntry(steer.messageId, String(existing.id)));
|
|
44
|
+
this.settleRecord(existing);
|
|
45
|
+
} else await this.queue(steer);
|
|
83
46
|
}
|
|
84
47
|
}
|
|
85
48
|
}
|
|
@@ -109,20 +72,18 @@ export async function finishPendingSteers(options: {
|
|
|
109
72
|
: undefined;
|
|
110
73
|
const requestId = `${options.submissionId}:steer:${steer.steerId}`;
|
|
111
74
|
if (options.aborted) {
|
|
112
|
-
options.transaction(() => {
|
|
75
|
+
const owner = options.transaction(() => {
|
|
113
76
|
let handoff = options.submissions.findByRequestId(requestId);
|
|
114
77
|
if (handoff?.status === "pending") {
|
|
115
78
|
const reason = submissionAbortReason(options.submissions.find(options.submissionId)) ?? "Cancelled";
|
|
116
79
|
options.submissions.updateAbortReason(handoff.submissionId, reason);
|
|
117
80
|
handoff = options.cancelHandoff(handoff.submissionId, reason);
|
|
118
81
|
}
|
|
82
|
+
return handoff?.status === "aborted" || handoff?.status === "skipped" ? handoff.submissionId : options.submissionId;
|
|
83
|
+
});
|
|
84
|
+
await options.conversation.append(steer.messageId, message, { submissionId: owner, ...(userMessage ? { userMessage } : {}) });
|
|
85
|
+
options.transaction(() => {
|
|
119
86
|
if (steer.harnessEntryId) options.messageUi.delete(steer.harnessEntryId);
|
|
120
|
-
options.conversation.append(steer.messageId, message, {
|
|
121
|
-
submissionId: handoff?.status === "aborted" || handoff?.status === "skipped"
|
|
122
|
-
? handoff.submissionId
|
|
123
|
-
: options.submissionId,
|
|
124
|
-
...(userMessage ? { userMessage } : {}),
|
|
125
|
-
});
|
|
126
87
|
options.steers.deleteByMessageId(steer.messageId);
|
|
127
88
|
});
|
|
128
89
|
continue;
|
|
@@ -130,8 +91,7 @@ export async function finishPendingSteers(options: {
|
|
|
130
91
|
if (steer.harnessEntryId) {
|
|
131
92
|
const cancelled = await options.cancelQueued(steer.harnessEntryId);
|
|
132
93
|
if (cancelled) {
|
|
133
|
-
if (
|
|
134
|
-
if (cancelled.value.kind === "already_consumed") {
|
|
94
|
+
if (cancelled.kind === "already_consumed") {
|
|
135
95
|
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
136
96
|
continue;
|
|
137
97
|
}
|
|
@@ -148,20 +108,16 @@ export async function finishPendingSteers(options: {
|
|
|
148
108
|
...(userMessage ? { userMessage } : {}),
|
|
149
109
|
});
|
|
150
110
|
options.waitUntil(submitted.completion);
|
|
151
|
-
options.transaction(() => {
|
|
152
|
-
let
|
|
111
|
+
const handoff = options.transaction(() => {
|
|
112
|
+
let current = options.submissions.find(submitted.receipt.submissionId) ?? submitted.receipt;
|
|
153
113
|
const abortReason = submissionAbortReason(options.submissions.find(options.submissionId));
|
|
154
|
-
if (abortReason &&
|
|
155
|
-
options.submissions.updateAbortReason(
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
if (handoff.status === "skipped" || handoff.status === "aborted") {
|
|
159
|
-
options.conversation.append(steer.messageId, message, {
|
|
160
|
-
submissionId: handoff.submissionId,
|
|
161
|
-
...(userMessage ? { userMessage } : {}),
|
|
162
|
-
});
|
|
114
|
+
if (abortReason && current.status === "pending" && current.requestId === requestId) {
|
|
115
|
+
options.submissions.updateAbortReason(current.submissionId, abortReason);
|
|
116
|
+
current = options.cancelHandoff(current.submissionId, abortReason);
|
|
163
117
|
}
|
|
164
|
-
|
|
118
|
+
return current;
|
|
165
119
|
});
|
|
120
|
+
if (handoff.status === "skipped" || handoff.status === "aborted") await options.conversation.append(steer.messageId, message, { submissionId: handoff.submissionId, ...(userMessage ? { userMessage } : {}) });
|
|
121
|
+
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
166
122
|
}
|
|
167
123
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Host-confirmed authority pinned to one scheduled task Requirement version.
|
|
2
|
+
* Credentials never belong in scope; only trusted Host ports consume it.
|
|
3
|
+
*/
|
|
4
|
+
export interface RuntimeUnattendedExecutionGrant {
|
|
5
|
+
readonly taskId: string;
|
|
6
|
+
readonly requirementVersion: string;
|
|
7
|
+
readonly authorizationVersion: string;
|
|
8
|
+
readonly scope: Readonly<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function pinnedUnattendedExecutionGrant(
|
|
12
|
+
value: RuntimeUnattendedExecutionGrant,
|
|
13
|
+
): RuntimeUnattendedExecutionGrant {
|
|
14
|
+
for (const [name, version] of [
|
|
15
|
+
["task ID", value.taskId],
|
|
16
|
+
["Requirement", value.requirementVersion],
|
|
17
|
+
["authorization", value.authorizationVersion],
|
|
18
|
+
] as const) {
|
|
19
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
20
|
+
throw new Error(`Unattended execution ${name} is required`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!value.scope || typeof value.scope !== "object" || Array.isArray(value.scope)) {
|
|
24
|
+
throw new Error("Unattended execution scope must be an object");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const scope = JSON.parse(JSON.stringify(value.scope)) as unknown;
|
|
28
|
+
if (!scope || typeof scope !== "object" || Array.isArray(scope)) {
|
|
29
|
+
throw new Error("scope changed shape during serialization");
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
taskId: value.taskId,
|
|
33
|
+
requirementVersion: value.requirementVersion,
|
|
34
|
+
authorizationVersion: value.authorizationVersion,
|
|
35
|
+
scope: scope as Readonly<Record<string, unknown>>,
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error("Unattended execution scope must be JSON-serializable", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -5,7 +5,7 @@ import { TelemetryRecorder } from "./recorder";
|
|
|
5
5
|
import { RuntimeTelemetry } from "./runtime-telemetry";
|
|
6
6
|
import type { AgentTelemetryBinding } from "./contract";
|
|
7
7
|
import type { PiGenerationLifecycleEvent } from "../models";
|
|
8
|
-
import type { TemporaryAgentLifecycleEvent } from "../tools/temporary
|
|
8
|
+
import type { TemporaryAgentLifecycleEvent } from "../tools/subagent/temporary/coordinator";
|
|
9
9
|
|
|
10
10
|
const TELEMETRY_COLD_BUFFER_LIMIT = 1_000;
|
|
11
11
|
|
|
@@ -71,7 +71,6 @@ export class RuntimeTelemetry {
|
|
|
71
71
|
|
|
72
72
|
turnStarted(input: Readonly<{
|
|
73
73
|
submissionId: string;
|
|
74
|
-
assemblyRevision?: string;
|
|
75
74
|
input?: unknown;
|
|
76
75
|
occurredAt?: number;
|
|
77
76
|
}>): TelemetryRecordResult {
|
|
@@ -80,9 +79,6 @@ export class RuntimeTelemetry {
|
|
|
80
79
|
input.submissionId,
|
|
81
80
|
);
|
|
82
81
|
return this.record("turn.started", operationId, {
|
|
83
|
-
...(input.assemblyRevision
|
|
84
|
-
? { assemblyRevision: input.assemblyRevision }
|
|
85
|
-
: {}),
|
|
86
82
|
...(input.input === undefined ? {} : { input: input.input as never }),
|
|
87
83
|
}, input);
|
|
88
84
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionHarness } from "../harness/session-harness";
|
|
2
|
+
import { projectSessionEntry, type ProjectedSessionEntry as Entry } from "../conversation/session-entry";
|
|
3
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
4
|
import type {
|
|
3
5
|
RuntimeLifecycleFact,
|
|
4
6
|
RuntimeModelUsageEvent,
|
|
@@ -7,7 +9,7 @@ import type {
|
|
|
7
9
|
RuntimeTurnEventsPort,
|
|
8
10
|
} from "../config/ports";
|
|
9
11
|
import type { StoredSubmission } from "../submission/repository";
|
|
10
|
-
import { decodeSubagentCompletion, isZeroSubagentUsage, subagentTerminalStatus, type RuntimeSubagentRunMetadata } from "../tools/subagent
|
|
12
|
+
import { decodeSubagentCompletion, isZeroSubagentUsage, subagentTerminalStatus, type RuntimeSubagentRunMetadata } from "../tools/subagent/runtime";
|
|
11
13
|
import type { RuntimeTelemetryCoordinator } from "./coordinator";
|
|
12
14
|
import { summarizeModelUsage } from "./model-usage";
|
|
13
15
|
import type { RuntimeEventOutboxRepository } from "./runtime-event-repository";
|
|
@@ -87,10 +89,22 @@ export class RuntimeTurnEvents {
|
|
|
87
89
|
} }, submission.completedAt ?? Date.now());
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/** Pi summarizes through completeSimple; account only its successful, measured response. */
|
|
93
|
+
compactionGeneration(submissionId: string, event: import("../models").PiGenerationLifecycleEvent): void {
|
|
94
|
+
if (event.type !== "finished" || event.outcome !== "success" || !event.output || event.output.stopReason === "error" || event.output.stopReason === "aborted") return;
|
|
95
|
+
const response = event.output;
|
|
96
|
+
this.save({ type: "model-usage", event: {
|
|
97
|
+
eventId: `${submissionId}:compaction:${event.generationId}`, submissionId, runId: submissionId,
|
|
98
|
+
kind: "compaction", api: response.api, provider: response.provider, model: response.model,
|
|
99
|
+
...(response.responseModel ? { responseModel: response.responseModel } : {}), usage: response.usage,
|
|
100
|
+
} }, event.timestamp);
|
|
101
|
+
this.options.waitUntil(this.drain());
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
private recordEntry(submissionId: string, entry: Entry, model: RequestedModel): void {
|
|
91
105
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
92
106
|
this.save({ type: "model-usage", event: {
|
|
93
|
-
eventId: `${submissionId}:model:${entry.
|
|
107
|
+
eventId: `${submissionId}:model:${entry.identity}`,
|
|
94
108
|
submissionId,
|
|
95
109
|
runId: submissionId,
|
|
96
110
|
kind: "assistant",
|
|
@@ -107,7 +121,7 @@ export class RuntimeTurnEvents {
|
|
|
107
121
|
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
108
122
|
if (entry.usage) {
|
|
109
123
|
this.save({ type: "model-usage", event: {
|
|
110
|
-
eventId: `${submissionId}:compaction:${entry.
|
|
124
|
+
eventId: `${submissionId}:compaction:${entry.identity}`,
|
|
111
125
|
submissionId,
|
|
112
126
|
runId: submissionId,
|
|
113
127
|
kind: "compaction",
|
|
@@ -131,28 +145,24 @@ export class RuntimeTurnEvents {
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/** Live observation and recovery use exactly the same committed-entry projection. */
|
|
134
|
-
observe(
|
|
135
|
-
harness.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
toolCallId: event.toolCallId,
|
|
143
|
-
toolName: event.toolName,
|
|
144
|
-
input: event.args,
|
|
145
|
-
});
|
|
148
|
+
async observe(runtime: SessionHarness, submissionId: string, model: RequestedModel): Promise<void> {
|
|
149
|
+
runtime.harness.subscribeCommits((publication) => {
|
|
150
|
+
for (const change of publication.changes) {
|
|
151
|
+
if (change.type === "entry" && change.value.conversationId === runtime.conversation.id) {
|
|
152
|
+
this.recordEntry(submissionId, projectSessionEntry(change.value), model);
|
|
153
|
+
this.options.waitUntil(this.drain());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
146
156
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
toolCallId: event.toolCallId,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
157
|
+
const watch = await runtime.watch(BACKGROUND_CONTEXT);
|
|
158
|
+
watch.start(async (events) => {
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.type === "tool_execution_start") this.options.telemetry.capture("toolStarted", { submissionId, toolCallId: event.toolCallId, toolName: event.toolName, input: event.args });
|
|
161
|
+
if (event.type === "tool_execution_end") {
|
|
162
|
+
const result = event.entry?.model?.[0];
|
|
163
|
+
this.options.telemetry.capture("toolFinished", { submissionId, toolCallId: event.toolCallId, toolName: event.toolName, outcome: result?.role === "toolResult" && !result.isError ? "completed" : "failed", output: result, outputBytes: JSON.stringify(result ?? {}).length });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
156
166
|
});
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Runtime Tools
|
|
2
|
+
|
|
3
|
+
工具按功能归组。根 `index.ts` 只维护包内统一导出;功能目录的 `index.ts` 定义或组合该功能的 Pi Tool。
|
|
4
|
+
|
|
5
|
+
| 目录 | 职责 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `runtime/` | 公共工具契约、Harness 编译、执行决策、审批与客户端交互结算 |
|
|
8
|
+
| `adapters/` | AI SDK ↔ Pi 转换,以及外部工具声明到 Pi Candidate 的适配 |
|
|
9
|
+
| `codemode/` | `execute` 和 `browser_execute` 的 Pi Tool 定义、Cloudflare 执行适配、Workspace Connector |
|
|
10
|
+
| `assistant/` | 用户问答、计划、后续建议、记忆与时间工具 |
|
|
11
|
+
| `workspace/` | 持久 Workspace 文件操作、写入串行化与版本操作工具 |
|
|
12
|
+
| `sandbox/` | 临时 Linux Sandbox 的命令、进程和显式文件发布工具 |
|
|
13
|
+
| `skill/` | Skill 工具组合、资源读取与复制、脚本运行与输出投影 |
|
|
14
|
+
| `subagent/` | 子 Agent 工具、权限、运行记录、类型和临时 Agent 协调 |
|
|
15
|
+
| `schedule/` | 定时任务的 Pi Tool 定义 |
|
|
16
|
+
| `web/` | 网页获取与 Provider-native 搜索 |
|
|
17
|
+
| `mcp/` | MCP Tool 接入及使用 MCP Session 的 Connector Gateway |
|
|
18
|
+
| `extension/` | 已加载 Extension 的发现工具;Extension 装载仍归 `context/extension/` |
|
|
19
|
+
|
|
20
|
+
## Code Mode
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
codemode/index.ts Pi Tool 定义、执行级别与调用截止
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
RuntimeCodeExecutionPort 内部执行接口
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
codemode/adapter.ts Cloudflare 执行器组装、描述与结果适配
|
|
30
|
+
|
|
|
31
|
+
+-- workspace-connector.ts state.* 方法、发现与校验
|
|
32
|
+
+-- ../adapters/pi-to-ai.ts 现有 Pi 工具到 Cloudflare ToolSet 的转换
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`pi-to-ai.ts` 也被 Skill 脚本执行使用,因此放在共用 `adapters/` 中。工具声明仍以 Pi 为唯一事实源。
|
|
36
|
+
|
|
37
|
+
## 修改位置
|
|
38
|
+
|
|
39
|
+
- 修改某个工具:进入对应功能目录,测试与实现放在一起。
|
|
40
|
+
- 修改所有工具共用的执行策略:进入 `runtime/`,不要复制到每个工具。
|
|
41
|
+
- 修改外部协议转换:进入 `adapters/`,不新增另一份工具定义。
|
|
42
|
+
- 小功能可以只有 `index.ts`,不强制拆分 Adapter、Service 或 Repository。
|
|
43
|
+
- `runtime/result.ts` 只共享结果包装与进度报告;`assistant/candidate.ts` 只共享助手工具已有的元数据默认值。
|
|
44
|
+
- 包的公共导出名及 `@springbrand/agent-runtime/declared-tool` 子路径保持不变。内部路径变更需要同步 Vitest 测试池配置。
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// 本文件把 ai-sdk Tool 转成 Pi AgentTool。
|
|
2
2
|
// `@cloudflare/think`、Agents Skills 和 Workspace 等上游工厂生成 ai-sdk Tool 时调用这个边界。
|
|
3
|
-
// 转换只保留在这里,才能让后续治理和结算始终看到同一种 AgentTool 结果;核心术语见
|
|
3
|
+
// 转换只保留在这里,才能让后续治理和结算始终看到同一种 AgentTool 结果;核心术语见 `src/index.ts` 入口说明。
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
AgentTool,
|
|
7
|
-
AgentToolResult,
|
|
8
|
-
} from "@earendil-works/pi-agent-core";
|
|
5
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
9
6
|
import { asSchema, type Tool as AiTool } from "ai";
|
|
10
|
-
import { serializeOutput } from "
|
|
7
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
11
8
|
|
|
12
9
|
/** 控制一个 ai-sdk Tool 如何投影到 Pi AgentTool。 */
|
|
13
10
|
export interface AiToolAdapterOptions {
|
|
@@ -141,9 +138,9 @@ export function aiToolToPi(
|
|
|
141
138
|
.toModelOutput;
|
|
142
139
|
const content = typeof toModelOutput === "function"
|
|
143
140
|
? modelContent(await (toModelOutput as (input: {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
input: unknown;
|
|
142
|
+
output: unknown;
|
|
143
|
+
}) => unknown)({ input: params, output: raw }))
|
|
147
144
|
: null;
|
|
148
145
|
return {
|
|
149
146
|
content: content ?? [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { PiToolCandidate } from "
|
|
2
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 一个由 Runtime 之外声明、由 Runtime 之外执行的工具。
|
|
@@ -15,7 +15,7 @@ import type { PiToolCandidate } from "./compiler";
|
|
|
15
15
|
* 本模块只引入类型,因此可经 `@springbrand/agent-runtime/declared-tool` 单独导入:
|
|
16
16
|
* 声明方在 Node 下也能装配自己的工具,不必为此加载 Workers 运行时。
|
|
17
17
|
*
|
|
18
|
-
* `Pi`、`Tool Candidate` 与 `Runtime` 等核心术语见
|
|
18
|
+
* `Pi`、`Tool Candidate` 与 `Runtime` 等核心术语见 `src/index.ts`。
|
|
19
19
|
*/
|
|
20
20
|
export interface PiDeclaredTool {
|
|
21
21
|
/** 声明方自己的工具名,用于结果详情与诊断,不一定是模型看到的名字。 */
|
|
@@ -49,6 +49,7 @@ export interface PiDeclaredToolPolicy<Reply = unknown> {
|
|
|
49
49
|
readonly modelName: string;
|
|
50
50
|
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
51
51
|
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
52
|
+
readonly unattendedExecution?: PiToolCandidate["unattendedExecution"];
|
|
52
53
|
readonly exposureMode?: PiToolCandidate["exposureMode"];
|
|
53
54
|
readonly retry?: PiToolCandidate["retry"];
|
|
54
55
|
readonly source?: PiToolCandidate["source"];
|
|
@@ -117,6 +118,9 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
|
117
118
|
...(policy.requiredExecutionLevelForInput
|
|
118
119
|
? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
|
|
119
120
|
: {}),
|
|
121
|
+
...(policy.unattendedExecution
|
|
122
|
+
? { unattendedExecution: policy.unattendedExecution }
|
|
123
|
+
: {}),
|
|
120
124
|
...(policy.exposureMode ? { exposureMode: policy.exposureMode } : {}),
|
|
121
125
|
...(policy.retry ? { retry: policy.retry } : {}),
|
|
122
126
|
...(policy.source ? { source: policy.source } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { validateToolArguments } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
import type { PiToolCandidate } from "
|
|
2
|
+
import { validateToolArguments, type JsonObject } from "@earendil-works/pi-ai";
|
|
3
|
+
import { tool as aiTool, jsonSchema, type ToolSet } from "ai";
|
|
4
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
5
|
|
|
6
6
|
interface NestedToolOptions {
|
|
7
7
|
readonly fallbackToolCallId?: (name: string) => string;
|
|
@@ -27,7 +27,7 @@ export function piCandidatesToAiTools(
|
|
|
27
27
|
type: "toolCall",
|
|
28
28
|
id: toolCallId,
|
|
29
29
|
name: candidate.tool.name,
|
|
30
|
-
arguments: prepared as
|
|
30
|
+
arguments: prepared as JsonObject,
|
|
31
31
|
});
|
|
32
32
|
const result = await candidate.tool.execute(
|
|
33
33
|
toolCallId,
|