@springbrand/agent-runtime 0.3.0-alpha.15 → 0.3.0-alpha.17
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 +66 -122
- package/src/adapter/cloudflare/subagent/definition.ts +11 -9
- package/src/adapter/cloudflare/subagent/runner.ts +20 -47
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/config/harness-runtime.ts +27 -14
- 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 +69 -70
- package/src/conversation/history.ts +118 -153
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/session-entry.ts +29 -0
- package/src/harness/index.ts +2 -0
- package/src/harness/session-harness.ts +178 -606
- package/src/harness/submission-operation.ts +15 -40
- package/src/models/index.ts +3 -1
- package/src/models/openrouter/protocol-response.ts +36 -112
- package/src/session/cloudflare/migration.ts +221 -939
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/submission/finalization.ts +2 -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 +25 -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.17",
|
|
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,7 +69,7 @@ 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
74
|
import { openSessionHarness, prepareSubmissionOperation } from "../../harness";
|
|
75
75
|
import type { SessionHarness } from "../../harness";
|
|
@@ -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
|
});
|
|
@@ -788,8 +793,12 @@ export abstract class AgentRuntimeKernel<
|
|
|
788
793
|
activeRuntime.pi,
|
|
789
794
|
submission.assemblyDescriptor,
|
|
790
795
|
{
|
|
791
|
-
|
|
792
|
-
|
|
796
|
+
assertRequestAllowed: () => {
|
|
797
|
+
const current = this.db.submissions.find(submissionId);
|
|
798
|
+
if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
|
|
799
|
+
},
|
|
800
|
+
onGeneration: (event) => this.telemetry.generation(submissionId, event),
|
|
801
|
+
onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
|
|
793
802
|
},
|
|
794
803
|
);
|
|
795
804
|
const prepareToolInput = (toolName: string, input: unknown): unknown => {
|
|
@@ -848,9 +857,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
848
857
|
unattendedFailure: () => this.db.submissions.find(submissionId)?.unattendedFailure,
|
|
849
858
|
});
|
|
850
859
|
runtime = await openSessionHarness({
|
|
851
|
-
|
|
860
|
+
storage: openCloudflareSession(this.ctx.storage),
|
|
852
861
|
models: harnessInput.models,
|
|
853
|
-
modelSessionId: submissionId,
|
|
854
862
|
model: harnessInput.model,
|
|
855
863
|
thinkingLevel: harnessInput.thinkingLevel,
|
|
856
864
|
systemPrompt: harnessInput.systemPrompt,
|
|
@@ -867,35 +875,18 @@ export abstract class AgentRuntimeKernel<
|
|
|
867
875
|
streamOptions: { maxRetries: 0 },
|
|
868
876
|
compaction: harnessInput.compaction,
|
|
869
877
|
toolExecution: "parallel",
|
|
878
|
+
beforeTool: (call, _api, context) => gate.beforeTool({ toolName: call.name, toolCallId: call.id, args: call.arguments }, context),
|
|
879
|
+
afterResponse: (message) => {
|
|
880
|
+
for (const part of message.content) if (part.type === "toolCall") part.arguments = prepareToolInput(part.name, part.arguments) as JsonObject;
|
|
881
|
+
},
|
|
870
882
|
}, BACKGROUND_CONTEXT);
|
|
871
883
|
this.activeHarness = runtime;
|
|
872
|
-
let stream: ReturnType<typeof bindHarnessStream
|
|
884
|
+
let stream: Awaited<ReturnType<typeof bindHarnessStream>> | undefined;
|
|
873
885
|
let deactivate = () => {};
|
|
874
886
|
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
887
|
|
|
894
888
|
if ((submission.abortReason || submission.unattendedFailure) && runtime.openOperation) {
|
|
895
|
-
await runtime.
|
|
896
|
-
runtime.openOperation.operationId,
|
|
897
|
-
BACKGROUND_CONTEXT,
|
|
898
|
-
);
|
|
889
|
+
await runtime.abort(BACKGROUND_CONTEXT);
|
|
899
890
|
}
|
|
900
891
|
|
|
901
892
|
await this.associateHarnessEntries(runtime, submissionId);
|
|
@@ -906,25 +897,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
906
897
|
(id) => this.transcript.regenerationTarget(id),
|
|
907
898
|
BACKGROUND_CONTEXT,
|
|
908
899
|
);
|
|
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
900
|
if (submission.status === "pending") {
|
|
929
901
|
this.db.submissions.transition(submissionId, "running", ["pending"]);
|
|
930
902
|
this.telemetry.capture("turnStarted", {
|
|
@@ -938,71 +910,58 @@ export abstract class AgentRuntimeKernel<
|
|
|
938
910
|
steers: this.db.steers,
|
|
939
911
|
messageUi: this.db.messageUi,
|
|
940
912
|
transaction: (run) => this.db.transaction(run),
|
|
941
|
-
enqueue: async (
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
return queued.
|
|
913
|
+
enqueue: async (steer) => {
|
|
914
|
+
const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
|
|
915
|
+
const queued = await runtime.conversation.submit({ type: "input", content: message.content, requestId: `steer:${steer.messageId}`, whenBusy: "steer" }, BACKGROUND_CONTEXT);
|
|
916
|
+
return queued.status(BACKGROUND_CONTEXT);
|
|
945
917
|
},
|
|
918
|
+
find: (messageId) => runtime.submissionFor(`steer:${messageId}`, BACKGROUND_CONTEXT),
|
|
946
919
|
});
|
|
947
920
|
const uiMessageBody = submission.queuedUiMessageJson;
|
|
948
921
|
let initialUserSidecarPersisted = (await this.transcript.storedMessages())
|
|
949
922
|
.some((entry) =>
|
|
950
923
|
entry.submissionId === submissionId && entry.message.role === "user"
|
|
951
924
|
);
|
|
952
|
-
runtime.harness.
|
|
953
|
-
const entry = event.entry;
|
|
925
|
+
const unsubscribeEntries = runtime.harness.subscribeCommits((publication) => {
|
|
954
926
|
this.db.transaction(() => {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
927
|
+
for (const change of publication.changes) {
|
|
928
|
+
if (change.type === "submission") steerDelivery.settleRecord(change.value);
|
|
929
|
+
if (change.type !== "entry" || change.value.conversationId !== runtime.conversation.id) continue;
|
|
930
|
+
const entry = change.value;
|
|
931
|
+
this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
|
|
932
|
+
if (entry.kind === "pi.user" && !initialUserSidecarPersisted && uiMessageBody) {
|
|
933
|
+
this.db.messageUi.upsert(String(entry.id), uiMessageBody);
|
|
959
934
|
initialUserSidecarPersisted = true;
|
|
960
935
|
}
|
|
961
936
|
}
|
|
962
937
|
});
|
|
963
|
-
|
|
964
938
|
});
|
|
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, {
|
|
939
|
+
await this.runtimeTurnEvents.observe(runtime, submissionId, harnessInput.model);
|
|
940
|
+
stream = await bindHarnessStream(runtime, {
|
|
974
941
|
messageId: submission.assistantMessageId,
|
|
975
942
|
userMessageId: submission.userMessageId ?? undefined,
|
|
976
943
|
startedAt: submission.createdAt,
|
|
977
944
|
turnId: submissionId,
|
|
978
945
|
prepareToolInput,
|
|
979
946
|
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
|
-
}
|
|
947
|
+
for (const chunk of chunks) this.conversation.broadcastResponse({ id: submission.requestId, body: json(chunk), done: false, continuation });
|
|
988
948
|
},
|
|
989
949
|
});
|
|
950
|
+
deactivate = unsubscribeEntries;
|
|
951
|
+
|
|
990
952
|
publishApprovalRequest = stream.publishApprovalRequest;
|
|
991
|
-
this.conversation.activate(runtime
|
|
953
|
+
this.conversation.activate(runtime, submission);
|
|
992
954
|
|
|
993
955
|
const control = {
|
|
994
956
|
abort: async () => {
|
|
995
957
|
if (runtime.openOperation) {
|
|
996
|
-
await runtime.
|
|
997
|
-
runtime.openOperation.operationId,
|
|
998
|
-
BACKGROUND_CONTEXT,
|
|
999
|
-
);
|
|
958
|
+
await runtime.abort(BACKGROUND_CONTEXT);
|
|
1000
959
|
}
|
|
1001
960
|
},
|
|
1002
961
|
interrupt: () => void runtime.close(BACKGROUND_CONTEXT),
|
|
1003
962
|
steer: async (_message: PiCanonicalUserInput, messageId: string) => {
|
|
1004
963
|
const steer = this.db.steers.findByMessageId(messageId);
|
|
1005
|
-
if (steer) await steerDelivery.queue(steer);
|
|
964
|
+
if (steer && runtime.openOperation) await steerDelivery.queue(steer);
|
|
1006
965
|
},
|
|
1007
966
|
};
|
|
1008
967
|
const turn: ActiveTurn = {
|
|
@@ -1013,34 +972,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
1013
972
|
continuation,
|
|
1014
973
|
agent: control,
|
|
1015
974
|
};
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
975
|
+
const deactivateTurn = this.submissions.activate(submission, turn);
|
|
976
|
+
const unsubscribeEntriesAndTurn = deactivate;
|
|
977
|
+
deactivate = () => { unsubscribeEntriesAndTurn(); deactivateTurn(); };
|
|
978
|
+
await runtime.admit(preparation, BACKGROUND_CONTEXT);
|
|
979
|
+
await steerDelivery.reconcile(submissionId);
|
|
980
|
+
const outcome = await runtime.run(undefined, BACKGROUND_CONTEXT);
|
|
1020
981
|
this.db.submissions.clearQueuedPayload(submissionId);
|
|
1021
982
|
if (outcome.kind === "released") {
|
|
1022
|
-
if (
|
|
1023
|
-
outcome.reason === "slice" ||
|
|
1024
|
-
outcome.reason === "budget" ||
|
|
1025
|
-
outcome.reason === "tool-failure"
|
|
1026
|
-
) {
|
|
983
|
+
if (outcome.reason === "slice") {
|
|
1027
984
|
await this.schedule(0, "_piHarnessContinuation", { submissionId }, {
|
|
1028
985
|
idempotent: true,
|
|
1029
986
|
});
|
|
1030
987
|
}
|
|
1031
988
|
return this.db.submissions.find(submissionId)!;
|
|
1032
989
|
}
|
|
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
|
-
);
|
|
1042
|
-
return this.db.submissions.find(submissionId)!;
|
|
1043
|
-
}
|
|
1044
990
|
const record = outcome.outcome;
|
|
1045
991
|
const unattendedFailure = this.db.submissions.find(submissionId)
|
|
1046
992
|
?.unattendedFailure;
|
|
@@ -1072,10 +1018,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
1072
1018
|
}));
|
|
1073
1019
|
throw error;
|
|
1074
1020
|
} finally {
|
|
1075
|
-
await this.conversation.release(runtime
|
|
1021
|
+
await this.conversation.release(runtime).catch((error) => {
|
|
1076
1022
|
console.warn("[runtime-stream:degraded]", errorText(error));
|
|
1077
1023
|
});
|
|
1078
|
-
stream?.unsubscribe();
|
|
1024
|
+
await stream?.unsubscribe();
|
|
1079
1025
|
deactivate();
|
|
1080
1026
|
if (this.activeHarness === runtime) this.activeHarness = undefined;
|
|
1081
1027
|
await runtime.close(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
@@ -1209,10 +1155,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
1209
1155
|
|
|
1210
1156
|
if (event.type === "clear") {
|
|
1211
1157
|
if (this.submissions.isBusy()) return;
|
|
1212
|
-
this.
|
|
1213
|
-
|
|
1214
|
-
this.db.clearAll();
|
|
1215
|
-
});
|
|
1158
|
+
await this.transcript.clear();
|
|
1159
|
+
this.db.transaction(() => this.db.clearAll());
|
|
1216
1160
|
await this.broadcastApprovals();
|
|
1217
1161
|
this.broadcast(json({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
|
|
1218
1162
|
return;
|
|
@@ -1354,7 +1298,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1354
1298
|
await this.broadcastApprovals();
|
|
1355
1299
|
return;
|
|
1356
1300
|
}
|
|
1357
|
-
this.transcript.append(crypto.randomUUID(), message);
|
|
1301
|
+
await this.transcript.append(crypto.randomUUID(), message);
|
|
1358
1302
|
}
|
|
1359
1303
|
|
|
1360
1304
|
async _hostGetSessionInfo(): Promise<{ messageCount: number }> {
|
|
@@ -1377,7 +1321,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1377
1321
|
async addCanonicalSnapshot(
|
|
1378
1322
|
snapshot: PiCanonicalTranscriptSnapshot,
|
|
1379
1323
|
): Promise<void> {
|
|
1380
|
-
this.transcript.importSnapshot(snapshot);
|
|
1324
|
+
await this.transcript.importSnapshot(snapshot);
|
|
1381
1325
|
}
|
|
1382
1326
|
|
|
1383
1327
|
/**
|
|
@@ -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,8 +416,8 @@ 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));
|
|
@@ -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, 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,7 +35,7 @@ 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
41
|
/** Native operation events for projections that must survive model retries. */
|
|
@@ -131,8 +127,8 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
131
127
|
streamOptions: { maxRetries: 0 },
|
|
132
128
|
toolExecution: "parallel",
|
|
133
129
|
compaction: { enabled: false, reserveTokens: 1, keepRecentTokens: 1 },
|
|
134
|
-
} satisfies Omit<SessionHarnessOptions
|
|
135
|
-
let runtime: SessionHarness
|
|
130
|
+
} satisfies Omit<SessionHarnessOptions, "storage">;
|
|
131
|
+
let runtime: SessionHarness | undefined;
|
|
136
132
|
let toolFailure: { name: string; error: string } | undefined;
|
|
137
133
|
|
|
138
134
|
let timedOut = false;
|
|
@@ -141,60 +137,42 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
141
137
|
controller.abort(new Error("SubAgent run timed out"));
|
|
142
138
|
}, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
143
139
|
try {
|
|
144
|
-
let settled: Awaited<ReturnType<SessionHarness
|
|
140
|
+
let settled: Awaited<ReturnType<SessionHarness["run"]>>;
|
|
145
141
|
while (true) {
|
|
146
142
|
signal.throwIfAborted();
|
|
147
143
|
if (!runtime) {
|
|
148
144
|
runtime = await openSessionHarness({
|
|
149
145
|
...options,
|
|
150
|
-
|
|
146
|
+
storage: await run.openSession(),
|
|
151
147
|
}, context);
|
|
152
148
|
if (runtime.openOperation && runtime.openOperation.operationId !== operationId) {
|
|
153
149
|
throw new Error("SubAgent Session has an unrelated open operation");
|
|
154
150
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (event.isError) {
|
|
164
|
-
toolFailure ??= { name: event.toolName, error: toolErrorText(event.result) };
|
|
151
|
+
const watch = await runtime.watch(context);
|
|
152
|
+
watch.start(async (events) => {
|
|
153
|
+
for (const event of events) {
|
|
154
|
+
await run.onEvent?.(event, signal);
|
|
155
|
+
if (event.type === "tool_execution_end") {
|
|
156
|
+
const result = event.entry?.model?.[0];
|
|
157
|
+
if (result?.role === "toolResult" && result.isError) toolFailure ??= { name: event.toolName, error: ((event.entry?.data as { diagnostics?: Array<{ severity: string; message: string }> } | undefined)?.diagnostics?.find((item) => item.severity === "error")?.message ?? toolErrorText(result)) };
|
|
158
|
+
}
|
|
165
159
|
}
|
|
166
160
|
});
|
|
167
161
|
}
|
|
168
162
|
const result = await runtime.run(
|
|
169
163
|
!runtime.openOperation
|
|
170
164
|
? {
|
|
171
|
-
kind: "prompt",
|
|
172
165
|
operationId,
|
|
173
166
|
prompt: taskPrompt(input.data, structuredStream ? undefined : outputSchema),
|
|
174
167
|
}
|
|
175
168
|
: undefined,
|
|
176
169
|
context,
|
|
177
|
-
{ waitForRetry: true, pollDeferred: true },
|
|
178
170
|
);
|
|
179
171
|
if (result.kind === "released") {
|
|
180
172
|
await runtime.close(BACKGROUND_CONTEXT);
|
|
181
173
|
runtime = undefined;
|
|
182
174
|
continue;
|
|
183
175
|
}
|
|
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
176
|
settled = result;
|
|
199
177
|
break;
|
|
200
178
|
}
|
|
@@ -212,14 +190,9 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
212
190
|
if (toolFailure) {
|
|
213
191
|
throw new Error(`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`);
|
|
214
192
|
}
|
|
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);
|
|
193
|
+
const assistant = [...await runtime.entries(context)].reverse().flatMap((entry) => entry.model ?? []).find((message) => message.role === "assistant");
|
|
194
|
+
if (assistant?.role !== "assistant") throw new Error("SubAgent produced no structured output");
|
|
195
|
+
const text = textOf(assistant);
|
|
223
196
|
if (!text) throw new Error("SubAgent produced no structured output");
|
|
224
197
|
let output: unknown;
|
|
225
198
|
try {
|
|
@@ -239,7 +212,7 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
|
|
|
239
212
|
} finally {
|
|
240
213
|
clearTimeout(timeout);
|
|
241
214
|
if (signal.aborted && runtime?.openOperation) {
|
|
242
|
-
await runtime.
|
|
215
|
+
await runtime.abort(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
243
216
|
await runtime.run(undefined, BACKGROUND_CONTEXT).catch(() => undefined);
|
|
244
217
|
}
|
|
245
218
|
await runtime?.close(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
@@ -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" ||
|
|
@@ -93,6 +93,8 @@ export class HarnessRuntimeConfig {
|
|
|
93
93
|
request: {
|
|
94
94
|
readonly modelSessionId?: string;
|
|
95
95
|
readonly onGeneration?: PiGenerationLifecycleObserver;
|
|
96
|
+
readonly assertRequestAllowed?: () => void;
|
|
97
|
+
readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
|
|
96
98
|
} = {},
|
|
97
99
|
) {
|
|
98
100
|
const { state, descriptor } = readPinnedPiRuntime(
|
|
@@ -102,6 +104,19 @@ export class HarnessRuntimeConfig {
|
|
|
102
104
|
);
|
|
103
105
|
const pinnedModel = state.snapshot.pi.model;
|
|
104
106
|
let models!: MutableModels;
|
|
107
|
+
const requestStream = (observer = request.onGeneration) => {
|
|
108
|
+
const stream = withProviderBoundary(
|
|
109
|
+
this.models.streamSimple.bind(this.models), request.modelSessionId, observer,
|
|
110
|
+
);
|
|
111
|
+
return ((model, context, options) => lazyStream(model, async () => {
|
|
112
|
+
options?.signal?.throwIfAborted();
|
|
113
|
+
request.assertRequestAllowed?.();
|
|
114
|
+
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
115
|
+
options?.signal?.throwIfAborted();
|
|
116
|
+
request.assertRequestAllowed?.();
|
|
117
|
+
return stream(model, refreshed, options);
|
|
118
|
+
})) satisfies typeof stream;
|
|
119
|
+
};
|
|
105
120
|
models = new Proxy(this.models, {
|
|
106
121
|
get: (target, property) => {
|
|
107
122
|
if (property === "getModel") {
|
|
@@ -110,21 +125,19 @@ export class HarnessRuntimeConfig {
|
|
|
110
125
|
? pinnedModel
|
|
111
126
|
: target.getModel(provider, modelId);
|
|
112
127
|
}
|
|
113
|
-
if (property === "streamSimple")
|
|
114
|
-
const stream = withProviderBoundary(
|
|
115
|
-
target.streamSimple.bind(target),
|
|
116
|
-
request.modelSessionId,
|
|
117
|
-
request.onGeneration,
|
|
118
|
-
);
|
|
119
|
-
return ((model, context, options) => lazyStream(model, async () => {
|
|
120
|
-
options?.signal?.throwIfAborted();
|
|
121
|
-
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
122
|
-
options?.signal?.throwIfAborted();
|
|
123
|
-
return stream(model, refreshed, options);
|
|
124
|
-
})) satisfies typeof stream;
|
|
125
|
-
}
|
|
128
|
+
if (property === "streamSimple") return requestStream();
|
|
126
129
|
if (property === "completeSimple") {
|
|
127
|
-
|
|
130
|
+
const compactionModels = new Proxy(models, {
|
|
131
|
+
get: (base, key) => {
|
|
132
|
+
if (key === "streamSimple") return requestStream((event) => {
|
|
133
|
+
request.onGeneration?.(event);
|
|
134
|
+
request.onCompactionGeneration?.(event);
|
|
135
|
+
});
|
|
136
|
+
const value = Reflect.get(base, key, base);
|
|
137
|
+
return typeof value === "function" ? value.bind(base) : value;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
return target.completeSimple.bind(compactionModels);
|
|
128
141
|
}
|
|
129
142
|
const value = Reflect.get(target, property, target);
|
|
130
143
|
return typeof value === "function" ? value.bind(target) : value;
|