@springbrand/agent-runtime 0.3.0-alpha.16 → 0.3.0-alpha.18
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 +6 -4
- package/src/adapter/cloudflare/agent.ts +81 -123
- package/src/adapter/cloudflare/subagent/definition.ts +21 -9
- package/src/adapter/cloudflare/subagent/runner.ts +42 -49
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/config/harness-runtime.ts +28 -15
- package/src/config/model-guidance.ts +16 -17
- package/src/config/ports.ts +2 -0
- package/src/conversation/harness-resume.ts +69 -234
- package/src/conversation/harness-stream.ts +85 -70
- package/src/conversation/history.ts +153 -153
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/session-entry.ts +29 -0
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +251 -607
- package/src/harness/submission-operation.ts +15 -40
- package/src/models/index.ts +14 -3
- package/src/models/openrouter/protocol-response.ts +36 -112
- package/src/session/cloudflare/migration.ts +382 -924
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/submission/finalization.ts +29 -13
- package/src/submission/repository.ts +1 -22
- package/src/submission/steer-delivery.ts +32 -76
- package/src/telemetry/turn-events.ts +34 -24
- package/src/tools/runtime/compiler.ts +32 -50
- package/src/tools/runtime/decision-gate.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
"@cloudflare/sandbox": "0.12.4",
|
|
22
22
|
"@cloudflare/shell": "0.4.3",
|
|
23
23
|
"@cloudflare/think": "0.18.0",
|
|
24
|
-
"@earendil-works/pi-agent-core": "
|
|
25
|
-
"@earendil-works/pi-ai": "
|
|
24
|
+
"@earendil-works/pi-agent-core": "1.1.0",
|
|
25
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
26
26
|
"@types/lodash-es": "^4.17.12",
|
|
27
27
|
"cron-schedule": "6.0.0",
|
|
28
28
|
"isomorphic-git": "1.38.6",
|
|
29
29
|
"lodash-es": "^4.18.1",
|
|
30
|
-
"zod": "^4.4.3"
|
|
30
|
+
"zod": "^4.4.3",
|
|
31
|
+
"@earendil-works/pi-durable": "1.1.0",
|
|
32
|
+
"@earendil-works/chord": "1.1.0"
|
|
31
33
|
},
|
|
32
34
|
"peerDependencies": {
|
|
33
35
|
"agents": "^0.23.0",
|
|
@@ -69,9 +69,9 @@ import type {
|
|
|
69
69
|
AssistantMessage,
|
|
70
70
|
JsonObject,
|
|
71
71
|
} from "@earendil-works/pi-ai";
|
|
72
|
-
import { BACKGROUND_CONTEXT } from "@earendil-works/
|
|
72
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
73
73
|
import { openCloudflareSession } from "../../session";
|
|
74
|
-
import { openSessionHarness, prepareSubmissionOperation } from "../../harness";
|
|
74
|
+
import { ModelRecovery, openSessionHarness, prepareSubmissionOperation } from "../../harness";
|
|
75
75
|
import type { SessionHarness } from "../../harness";
|
|
76
76
|
import {
|
|
77
77
|
compileHarnessTools,
|
|
@@ -185,7 +185,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
185
185
|
|
|
186
186
|
private activeRuntime?: ActiveRuntime;
|
|
187
187
|
private telemetryCoordinator?: RuntimeTelemetryCoordinator;
|
|
188
|
-
private activeHarness?: SessionHarness
|
|
188
|
+
private activeHarness?: SessionHarness;
|
|
189
189
|
private piAdapter?: HarnessRuntimeConfig;
|
|
190
190
|
private readonly transcript: ConversationHistory;
|
|
191
191
|
private conversationBridge?: HarnessConversationBridge;
|
|
@@ -370,14 +370,14 @@ export abstract class AgentRuntimeKernel<
|
|
|
370
370
|
runtime: Awaited<ReturnType<typeof openSessionHarness>>,
|
|
371
371
|
submissionId: string,
|
|
372
372
|
): Promise<void> {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
373
|
+
const native = await runtime.submissionFor(submissionId, BACKGROUND_CONTEXT);
|
|
374
|
+
if (!native || native.entry === undefined) return;
|
|
375
|
+
const entries = await runtime.entries();
|
|
376
|
+
const start = entries.findIndex((entry) => entry.id === native.entry);
|
|
377
|
+
if (start < 0) throw new Error("Native Submission entry is missing from its conversation");
|
|
378
|
+
this.db.transaction(() => {
|
|
379
|
+
for (const entry of entries.slice(start)) this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
|
|
380
|
+
});
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
/**
|
|
@@ -632,11 +632,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
632
632
|
releaseAbortedHarness: async () => {
|
|
633
633
|
const runtime = this.activeHarness;
|
|
634
634
|
if (!runtime) return;
|
|
635
|
-
await runtime.
|
|
636
|
-
|
|
637
|
-
|
|
635
|
+
await runtime.conversation.waitForIdle(BACKGROUND_CONTEXT);
|
|
636
|
+
// The execution owner closes after transport projections settle.
|
|
637
|
+
|
|
638
|
+
},
|
|
639
|
+
cancelQueued: async (id) => {
|
|
640
|
+
const runtime = this.activeHarness;
|
|
641
|
+
if (!runtime) return undefined;
|
|
642
|
+
const result = await runtime.harness.abortSubmission(Number(id) as import("@earendil-works/pi-durable").SubmissionId, BACKGROUND_CONTEXT, runtime.conversation.id);
|
|
643
|
+
return { kind: result === "already_placed" || result === "settled" ? "already_consumed" as const : "cancelled" as const };
|
|
638
644
|
},
|
|
639
|
-
cancelQueued: async (id) => this.activeHarness?.lane.cancelQueued(id, BACKGROUND_CONTEXT),
|
|
640
645
|
submit: (message, options) => this.submitMessage(message, options),
|
|
641
646
|
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
642
647
|
});
|
|
@@ -784,12 +789,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
784
789
|
(key) => this.ensureRuntimeVersion(key),
|
|
785
790
|
);
|
|
786
791
|
const activeRuntime = this.requireRuntime();
|
|
792
|
+
// A fresh provider Session per Submission keeps one failing upstream route from pinning later turns.
|
|
793
|
+
const recovery = new ModelRecovery(submissionId);
|
|
787
794
|
const harnessInput = this.pi.harnessInputs(
|
|
788
795
|
activeRuntime.pi,
|
|
789
796
|
submission.assemblyDescriptor,
|
|
790
797
|
{
|
|
791
|
-
|
|
792
|
-
|
|
798
|
+
modelSessionId: () => recovery.sessionId(),
|
|
799
|
+
assertRequestAllowed: () => {
|
|
800
|
+
const current = this.db.submissions.find(submissionId);
|
|
801
|
+
if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
|
|
802
|
+
},
|
|
803
|
+
onGeneration: (event) => this.telemetry.generation(submissionId, event),
|
|
804
|
+
onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
|
|
793
805
|
},
|
|
794
806
|
);
|
|
795
807
|
const prepareToolInput = (toolName: string, input: unknown): unknown => {
|
|
@@ -848,9 +860,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
848
860
|
unattendedFailure: () => this.db.submissions.find(submissionId)?.unattendedFailure,
|
|
849
861
|
});
|
|
850
862
|
runtime = await openSessionHarness({
|
|
851
|
-
|
|
863
|
+
storage: openCloudflareSession(this.ctx.storage),
|
|
852
864
|
models: harnessInput.models,
|
|
853
|
-
modelSessionId: submissionId,
|
|
854
865
|
model: harnessInput.model,
|
|
855
866
|
thinkingLevel: harnessInput.thinkingLevel,
|
|
856
867
|
systemPrompt: harnessInput.systemPrompt,
|
|
@@ -867,35 +878,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
867
878
|
streamOptions: { maxRetries: 0 },
|
|
868
879
|
compaction: harnessInput.compaction,
|
|
869
880
|
toolExecution: "parallel",
|
|
881
|
+
recovery,
|
|
882
|
+
beforeTool: (call, _api, context) => gate.beforeTool({ toolName: call.name, toolCallId: call.id, args: call.arguments }, context),
|
|
883
|
+
afterResponse: (message) => {
|
|
884
|
+
for (const part of message.content) if (part.type === "toolCall") part.arguments = prepareToolInput(part.name, part.arguments) as JsonObject;
|
|
885
|
+
},
|
|
870
886
|
}, BACKGROUND_CONTEXT);
|
|
871
887
|
this.activeHarness = runtime;
|
|
872
|
-
let stream: ReturnType<typeof bindHarnessStream
|
|
888
|
+
let stream: Awaited<ReturnType<typeof bindHarnessStream>> | undefined;
|
|
873
889
|
let deactivate = () => {};
|
|
890
|
+
let released = false;
|
|
874
891
|
try {
|
|
875
|
-
runtime.harness.hooks.on("after_response", ({ message }) => {
|
|
876
|
-
for (const part of message.content) {
|
|
877
|
-
if (part.type === "toolCall") {
|
|
878
|
-
part.arguments = prepareToolInput(
|
|
879
|
-
part.name,
|
|
880
|
-
part.arguments,
|
|
881
|
-
) as JsonObject;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
return { message };
|
|
885
|
-
});
|
|
886
|
-
runtime.harness.hooks.on("before_tool", gate.beforeTool);
|
|
887
|
-
runtime.harness.hooks.on("before_drive", async (event, context) => {
|
|
888
|
-
const current = this.db.submissions.find(submissionId);
|
|
889
|
-
if (current?.abortReason || current?.unattendedFailure) {
|
|
890
|
-
await runtime.requestAbort(event.runId, context);
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
892
|
|
|
894
893
|
if ((submission.abortReason || submission.unattendedFailure) && runtime.openOperation) {
|
|
895
|
-
await runtime.
|
|
896
|
-
runtime.openOperation.operationId,
|
|
897
|
-
BACKGROUND_CONTEXT,
|
|
898
|
-
);
|
|
894
|
+
await runtime.abort(BACKGROUND_CONTEXT);
|
|
899
895
|
}
|
|
900
896
|
|
|
901
897
|
await this.associateHarnessEntries(runtime, submissionId);
|
|
@@ -906,25 +902,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
906
902
|
(id) => this.transcript.regenerationTarget(id),
|
|
907
903
|
BACKGROUND_CONTEXT,
|
|
908
904
|
);
|
|
909
|
-
if (preparation.kind === "settled") {
|
|
910
|
-
const record = preparation.outcome;
|
|
911
|
-
const unattendedFailure = this.db.submissions.find(submissionId)
|
|
912
|
-
?.unattendedFailure;
|
|
913
|
-
const terminal = await this.submissions.finish(
|
|
914
|
-
submission,
|
|
915
|
-
unattendedFailure
|
|
916
|
-
? "failed"
|
|
917
|
-
: record.status === "completed" || record.status === "declined"
|
|
918
|
-
? "succeeded" : record.status === "aborted" ? "aborted" : "failed",
|
|
919
|
-
unattendedFailure ?? record.error?.message,
|
|
920
|
-
);
|
|
921
|
-
this.broadcast(json({
|
|
922
|
-
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
923
|
-
messages: await this.getMessages(),
|
|
924
|
-
}));
|
|
925
|
-
return terminal;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
905
|
if (submission.status === "pending") {
|
|
929
906
|
this.db.submissions.transition(submissionId, "running", ["pending"]);
|
|
930
907
|
this.telemetry.capture("turnStarted", {
|
|
@@ -938,71 +915,58 @@ export abstract class AgentRuntimeKernel<
|
|
|
938
915
|
steers: this.db.steers,
|
|
939
916
|
messageUi: this.db.messageUi,
|
|
940
917
|
transaction: (run) => this.db.transaction(run),
|
|
941
|
-
enqueue: async (
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
return queued.
|
|
918
|
+
enqueue: async (steer) => {
|
|
919
|
+
const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
|
|
920
|
+
const queued = await runtime.conversation.submit({ type: "input", content: message.content, requestId: `steer:${steer.messageId}`, whenBusy: "steer" }, BACKGROUND_CONTEXT);
|
|
921
|
+
return queued.status(BACKGROUND_CONTEXT);
|
|
945
922
|
},
|
|
923
|
+
find: (messageId) => runtime.submissionFor(`steer:${messageId}`, BACKGROUND_CONTEXT),
|
|
946
924
|
});
|
|
947
925
|
const uiMessageBody = submission.queuedUiMessageJson;
|
|
948
926
|
let initialUserSidecarPersisted = (await this.transcript.storedMessages())
|
|
949
927
|
.some((entry) =>
|
|
950
928
|
entry.submissionId === submissionId && entry.message.role === "user"
|
|
951
929
|
);
|
|
952
|
-
runtime.harness.
|
|
953
|
-
const entry = event.entry;
|
|
930
|
+
const unsubscribeEntries = runtime.harness.subscribeCommits((publication) => {
|
|
954
931
|
this.db.transaction(() => {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
932
|
+
for (const change of publication.changes) {
|
|
933
|
+
if (change.type === "submission") steerDelivery.settleRecord(change.value);
|
|
934
|
+
if (change.type !== "entry" || change.value.conversationId !== runtime.conversation.id) continue;
|
|
935
|
+
const entry = change.value;
|
|
936
|
+
this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
|
|
937
|
+
if (entry.kind === "pi.user" && !initialUserSidecarPersisted && uiMessageBody) {
|
|
938
|
+
this.db.messageUi.upsert(String(entry.id), uiMessageBody);
|
|
959
939
|
initialUserSidecarPersisted = true;
|
|
960
940
|
}
|
|
961
941
|
}
|
|
962
942
|
});
|
|
963
|
-
|
|
964
943
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
await steerDelivery.reconcile(submissionId, pendingWatch.snapshot);
|
|
968
|
-
} finally {
|
|
969
|
-
pendingWatch.unsubscribe();
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
this.runtimeTurnEvents.observe(runtime.harness, submissionId, harnessInput.model);
|
|
973
|
-
stream = bindHarnessStream(runtime.harness.events, {
|
|
944
|
+
await this.runtimeTurnEvents.observe(runtime, submissionId, harnessInput.model);
|
|
945
|
+
stream = await bindHarnessStream(runtime, {
|
|
974
946
|
messageId: submission.assistantMessageId,
|
|
975
947
|
userMessageId: submission.userMessageId ?? undefined,
|
|
976
948
|
startedAt: submission.createdAt,
|
|
977
949
|
turnId: submissionId,
|
|
978
950
|
prepareToolInput,
|
|
979
951
|
publish: async (chunks) => {
|
|
980
|
-
for (const chunk of chunks) {
|
|
981
|
-
this.conversation.broadcastResponse({
|
|
982
|
-
id: submission.requestId,
|
|
983
|
-
body: json(chunk),
|
|
984
|
-
done: false,
|
|
985
|
-
continuation,
|
|
986
|
-
});
|
|
987
|
-
}
|
|
952
|
+
for (const chunk of chunks) this.conversation.broadcastResponse({ id: submission.requestId, body: json(chunk), done: false, continuation });
|
|
988
953
|
},
|
|
989
954
|
});
|
|
955
|
+
deactivate = unsubscribeEntries;
|
|
956
|
+
|
|
990
957
|
publishApprovalRequest = stream.publishApprovalRequest;
|
|
991
|
-
this.conversation.activate(runtime
|
|
958
|
+
this.conversation.activate(runtime, submission);
|
|
992
959
|
|
|
993
960
|
const control = {
|
|
994
961
|
abort: async () => {
|
|
995
962
|
if (runtime.openOperation) {
|
|
996
|
-
await runtime.
|
|
997
|
-
runtime.openOperation.operationId,
|
|
998
|
-
BACKGROUND_CONTEXT,
|
|
999
|
-
);
|
|
963
|
+
await runtime.abort(BACKGROUND_CONTEXT);
|
|
1000
964
|
}
|
|
1001
965
|
},
|
|
1002
966
|
interrupt: () => void runtime.close(BACKGROUND_CONTEXT),
|
|
1003
967
|
steer: async (_message: PiCanonicalUserInput, messageId: string) => {
|
|
1004
968
|
const steer = this.db.steers.findByMessageId(messageId);
|
|
1005
|
-
if (steer) await steerDelivery.queue(steer);
|
|
969
|
+
if (steer && runtime.openOperation) await steerDelivery.queue(steer);
|
|
1006
970
|
},
|
|
1007
971
|
};
|
|
1008
972
|
const turn: ActiveTurn = {
|
|
@@ -1013,32 +977,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
1013
977
|
continuation,
|
|
1014
978
|
agent: control,
|
|
1015
979
|
};
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
980
|
+
const deactivateTurn = this.submissions.activate(submission, turn);
|
|
981
|
+
const unsubscribeEntriesAndTurn = deactivate;
|
|
982
|
+
deactivate = () => { unsubscribeEntriesAndTurn(); deactivateTurn(); };
|
|
983
|
+
await runtime.admit(preparation, BACKGROUND_CONTEXT);
|
|
984
|
+
await steerDelivery.reconcile(submissionId);
|
|
985
|
+
const outcome = await runtime.run(undefined, BACKGROUND_CONTEXT);
|
|
1020
986
|
this.db.submissions.clearQueuedPayload(submissionId);
|
|
1021
987
|
if (outcome.kind === "released") {
|
|
1022
|
-
if (
|
|
1023
|
-
outcome.reason === "slice" ||
|
|
1024
|
-
outcome.reason === "budget" ||
|
|
1025
|
-
outcome.reason === "tool-failure"
|
|
1026
|
-
) {
|
|
988
|
+
if (outcome.reason === "slice") {
|
|
1027
989
|
await this.schedule(0, "_piHarnessContinuation", { submissionId }, {
|
|
1028
990
|
idempotent: true,
|
|
1029
991
|
});
|
|
1030
992
|
}
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
if (outcome.kind === "waiting") {
|
|
1034
|
-
await this.schedule(
|
|
1035
|
-
outcome.reason === "retry"
|
|
1036
|
-
? Math.max(0, Math.ceil((outcome.notBefore - Date.now()) / 1000))
|
|
1037
|
-
: Math.max(0, Math.ceil((outcome.deferred.pollAfterMs ?? 1_000) / 1000)),
|
|
1038
|
-
"_piHarnessContinuation",
|
|
1039
|
-
{ submissionId },
|
|
1040
|
-
{ idempotent: true },
|
|
1041
|
-
);
|
|
993
|
+
released = true;
|
|
1042
994
|
return this.db.submissions.find(submissionId)!;
|
|
1043
995
|
}
|
|
1044
996
|
const record = outcome.outcome;
|
|
@@ -1072,13 +1024,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
1072
1024
|
}));
|
|
1073
1025
|
throw error;
|
|
1074
1026
|
} finally {
|
|
1075
|
-
await this.conversation.release(runtime
|
|
1027
|
+
await this.conversation.release(runtime).catch((error) => {
|
|
1076
1028
|
console.warn("[runtime-stream:degraded]", errorText(error));
|
|
1077
1029
|
});
|
|
1078
|
-
stream?.unsubscribe();
|
|
1030
|
+
await stream?.unsubscribe();
|
|
1079
1031
|
deactivate();
|
|
1080
1032
|
if (this.activeHarness === runtime) this.activeHarness = undefined;
|
|
1081
1033
|
await runtime.close(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
1034
|
+
if (released) {
|
|
1035
|
+
// Pi closes the Harness on release and drops undelivered stream frames; the
|
|
1036
|
+
// canonical transcript gives a paused question or approval its complete input.
|
|
1037
|
+
this.broadcast(json({
|
|
1038
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
1039
|
+
messages: await this.getMessages(),
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1082
1042
|
}
|
|
1083
1043
|
}
|
|
1084
1044
|
|
|
@@ -1209,10 +1169,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
1209
1169
|
|
|
1210
1170
|
if (event.type === "clear") {
|
|
1211
1171
|
if (this.submissions.isBusy()) return;
|
|
1212
|
-
this.
|
|
1213
|
-
|
|
1214
|
-
this.db.clearAll();
|
|
1215
|
-
});
|
|
1172
|
+
await this.transcript.clear();
|
|
1173
|
+
this.db.transaction(() => this.db.clearAll());
|
|
1216
1174
|
await this.broadcastApprovals();
|
|
1217
1175
|
this.broadcast(json({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
|
|
1218
1176
|
return;
|
|
@@ -1354,7 +1312,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1354
1312
|
await this.broadcastApprovals();
|
|
1355
1313
|
return;
|
|
1356
1314
|
}
|
|
1357
|
-
this.transcript.append(crypto.randomUUID(), message);
|
|
1315
|
+
await this.transcript.append(crypto.randomUUID(), message);
|
|
1358
1316
|
}
|
|
1359
1317
|
|
|
1360
1318
|
async _hostGetSessionInfo(): Promise<{ messageCount: number }> {
|
|
@@ -1377,7 +1335,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1377
1335
|
async addCanonicalSnapshot(
|
|
1378
1336
|
snapshot: PiCanonicalTranscriptSnapshot,
|
|
1379
1337
|
): Promise<void> {
|
|
1380
|
-
this.transcript.importSnapshot(snapshot);
|
|
1338
|
+
await this.transcript.importSnapshot(snapshot);
|
|
1381
1339
|
}
|
|
1382
1340
|
|
|
1383
1341
|
/**
|
|
@@ -7,7 +7,7 @@ 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,
|
|
@@ -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,14 +1,10 @@
|
|
|
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";
|
|
@@ -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,7 +10,6 @@ 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";
|
|
@@ -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" ||
|