@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.15",
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": "0.86.1",
25
- "@earendil-works/pi-ai": "0.86.1",
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/pi-agent-core/harness/context";
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<object | undefined>;
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 watch = await runtime.lane.watch(BACKGROUND_CONTEXT);
374
- try {
375
- this.db.transaction(() =>
376
- this.db.submissions.associateSessionEntries(submissionId, watch.snapshot)
377
- );
378
- } finally {
379
- watch.unsubscribe();
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.lane.waitForIdle(BACKGROUND_CONTEXT);
636
- if (this.activeHarness === runtime) this.activeHarness = undefined;
637
- await runtime.close(BACKGROUND_CONTEXT);
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
- modelSessionId: submissionId,
792
- onGeneration: (event) => this.telemetry.generation(submissionId, event),
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
- session: openCloudflareSession(this.ctx.storage),
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> | undefined;
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.requestAbort(
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 (message) => {
942
- const queued = await runtime.lane.steer(message, undefined, BACKGROUND_CONTEXT);
943
- if (!queued.ok) throw queued.error;
944
- return queued.value.entryId;
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.events.on("entry_added", (event) => {
953
- const entry = event.entry;
925
+ const unsubscribeEntries = runtime.harness.subscribeCommits((publication) => {
954
926
  this.db.transaction(() => {
955
- this.db.submissions.linkSessionEntry(entry.id, submissionId);
956
- if (entry.type === "message" && entry.message.role === "user") {
957
- if (!steerDelivery.settle(entry.id) && !initialUserSidecarPersisted && uiMessageBody) {
958
- this.db.messageUi.upsert(entry.id, uiMessageBody);
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
- const pendingWatch = await runtime.lane.watch(BACKGROUND_CONTEXT);
966
- try {
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.lane, submission);
953
+ this.conversation.activate(runtime, submission);
992
954
 
993
955
  const control = {
994
956
  abort: async () => {
995
957
  if (runtime.openOperation) {
996
- await runtime.requestAbort(
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
- deactivate = this.submissions.activate(submission, turn);
1017
- const outcome = await runtime.run(preparation.request, BACKGROUND_CONTEXT, {
1018
- pollDeferred: !preparation.request,
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.lane).catch((error) => {
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.db.transaction(() => {
1213
- this.transcript.clear();
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-agent-core";
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" || event.type === "run_resume") {
99
+ if (event.type === "run_start") {
100
100
  return { phase: "running", message: "SubAgent started" };
101
101
  }
102
- if (event.type === "tool_start") {
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 === "tool_update") {
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 === "tool_end") {
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: event.isError ? "tool-error" : "tool",
117
- message: event.isError
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.message.role === "assistant") {
418
- addSubagentUsage(usage, event.message.usage);
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
- type AgentHarnessTool,
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/pi-agent-core/harness/context";
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?: AgentHarnessTool<object | undefined>[];
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<object | undefined>, "session">;
135
- let runtime: SessionHarness<object | undefined> | undefined;
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<object | undefined>["run"]>>;
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
- session: await run.openSession(),
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 harness = runtime.harness;
156
- const onEvent = run.onEvent;
157
- if (onEvent) {
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) };
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 = (await runtime.lane.findEntries(
216
- { type: "message", order: "newestFirst" },
217
- context,
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.requestAbort(operationId, BACKGROUND_CONTEXT).catch(() => undefined);
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
- }): AgentHarnessTool<object | undefined>[] {
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
- return target.completeSimple.bind(models);
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;