effect-agent 0.1.0-beta.143 → 0.1.0-beta.148

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.
Files changed (81) hide show
  1. package/dist/{Admin-CQMppCcy.d.mts → Admin-BzPfXvzC.d.mts} +37 -22
  2. package/dist/Agent-1DMSO_Fl.mjs.map +1 -1
  3. package/dist/{Agent-BWhD4Dvu.d.mts → Agent-Cjd_ZveM.d.mts} +7 -3
  4. package/dist/{AgentRuntime-xeCUXutN.mjs → AgentRuntime-D8U9gl9Q.mjs} +23 -18
  5. package/dist/AgentRuntime-D8U9gl9Q.mjs.map +1 -0
  6. package/dist/{AgentRuntime-B4kIzmSF.d.mts → AgentRuntime-DZdgNcBk.d.mts} +5 -5
  7. package/dist/{DurableAgentRuntime-D8GI66mP.mjs → DurableAgentRuntime-N5Meflyt.mjs} +70 -42
  8. package/dist/DurableAgentRuntime-N5Meflyt.mjs.map +1 -0
  9. package/dist/{EventSource-w9PonN9X.d.mts → EventSource-DJW_1kPY.d.mts} +2 -2
  10. package/dist/{Messaging-Dnmk31yq.d.mts → Messaging-ccQURKCf.d.mts} +3 -3
  11. package/dist/{MessagingHost-CajpPDPG.d.mts → MessagingHost-D04wE-7W.d.mts} +2 -2
  12. package/dist/{Output-DhtWbBcw.d.mts → Output-CFD1CPfU.d.mts} +2 -2
  13. package/dist/{PreparedInputAdmission-BfcLy_k4.d.mts → PreparedInputAdmission-CNRSDoYu.d.mts} +3 -3
  14. package/dist/{RunOptions-IlEXLG5F.d.mts → RunOptions-Cy7CjrjS.d.mts} +2 -2
  15. package/dist/{Subagent-8RzJkqJ9.mjs → Subagent-hG9lJ7tJ.mjs} +2 -2
  16. package/dist/{Subagent-8RzJkqJ9.mjs.map → Subagent-hG9lJ7tJ.mjs.map} +1 -1
  17. package/dist/{Subagent-Cl8D1mFM.d.mts → Subagent-zS6prSK6.d.mts} +5 -5
  18. package/dist/{SubagentHost-B6f4PBC2.d.mts → SubagentHost-C-4A7tA2.d.mts} +2 -2
  19. package/dist/{Subscription-ClkC4_T0.d.mts → Subscription-C279AjM6.d.mts} +2 -2
  20. package/dist/{SubscriptionInput-CMjEKL1z.d.mts → SubscriptionInput-7R-Xrpxd.d.mts} +2 -2
  21. package/dist/capabilities/Commands.d.mts +1 -1
  22. package/dist/capabilities/Messaging.d.mts +1 -1
  23. package/dist/capabilities/RunHooks.d.mts +1 -1
  24. package/dist/capabilities/Subagent.d.mts +1 -1
  25. package/dist/capabilities/Subagent.mjs +1 -1
  26. package/dist/core/Agent.d.mts +1 -1
  27. package/dist/durable/Admin.d.mts +1 -1
  28. package/dist/durable/AgentRegistration.d.mts +1 -1
  29. package/dist/durable/Certification.d.mts +1 -1
  30. package/dist/durable/CommittedActivity.d.mts +1 -1
  31. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  32. package/dist/durable/DurableAgentRuntime.mjs +1 -1
  33. package/dist/durable/EventSource.d.mts +1 -1
  34. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  35. package/dist/durable/LifecyclePublication.d.mts +2 -2
  36. package/dist/durable/LifecyclePublication.mjs +32 -12
  37. package/dist/durable/LifecyclePublication.mjs.map +1 -1
  38. package/dist/durable/MessageDelivery.d.mts +41 -41
  39. package/dist/durable/MessageDeliveryStoreConformance.d.mts +39 -39
  40. package/dist/durable/PersistentHistory.d.mts +1 -1
  41. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  42. package/dist/durable/Recovery.d.mts +1 -1
  43. package/dist/durable/RunJournal.d.mts +1 -1
  44. package/dist/durable/Scheduling.d.mts +1 -1
  45. package/dist/durable/SubmissionLedger.d.mts +1 -1
  46. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  47. package/dist/durable/SubmissionStatus.d.mts +1 -1
  48. package/dist/durable/Subscription.d.mts +1 -1
  49. package/dist/durable/SubscriptionInput.d.mts +1 -1
  50. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  51. package/dist/durable/SubscriptionTools.d.mts +2 -2
  52. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  53. package/dist/durable/Subscriptions.d.mts +5 -5
  54. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  55. package/dist/durable/ThreadInvariants.d.mts +1 -1
  56. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  57. package/dist/durable/ThreadStore.d.mts +1 -1
  58. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  59. package/dist/durable/WorkerHost.d.mts +25 -5
  60. package/dist/durable/WorkerHost.mjs.map +1 -1
  61. package/dist/engine/AgentRuntime.d.mts +1 -1
  62. package/dist/engine/AgentRuntime.mjs +1 -1
  63. package/dist/engine/MessagingHost.d.mts +1 -1
  64. package/dist/engine/Output.d.mts +1 -1
  65. package/dist/engine/Output.mjs +1 -1
  66. package/dist/engine/RunOptions.d.mts +1 -1
  67. package/dist/engine/SubagentHost.d.mts +1 -1
  68. package/dist/index.d.mts +13 -13
  69. package/dist/index.mjs +3 -3
  70. package/dist/{output-contract-CANJJGA5.mjs → output-contract-DS-S8mTv.mjs} +46 -10
  71. package/dist/{output-contract-CANJJGA5.mjs.map → output-contract-DS-S8mTv.mjs.map} +1 -1
  72. package/package.json +1 -1
  73. package/src/core/Agent.ts +6 -2
  74. package/src/durable/DurableAgentRuntime.ts +50 -58
  75. package/src/durable/LifecyclePublication.ts +79 -25
  76. package/src/durable/WorkerHost.ts +23 -1
  77. package/src/durable/internal/worker-host.ts +169 -17
  78. package/src/engine/internal/agent-runtime.ts +80 -18
  79. package/src/engine/internal/output-contract.ts +67 -8
  80. package/dist/AgentRuntime-xeCUXutN.mjs.map +0 -1
  81. package/dist/DurableAgentRuntime-D8GI66mP.mjs.map +0 -1
@@ -128,6 +128,7 @@ import {
128
128
  WorkerHostConfig,
129
129
  WorkerPolicyResolver,
130
130
  type WorkerPolicyTarget,
131
+ type WorkerStartAdmission,
131
132
  } from "../WorkerHost.ts";
132
133
  import {
133
134
  definitionDigestsEqual,
@@ -263,6 +264,20 @@ export class WorkerInputControl extends Context.Service<
263
264
  }
264
265
  >()("@effect-agent/thread/internal/WorkerInputControl") {}
265
266
 
267
+ const CurrentStartPreparation = Context.Reference<WorkerStartAdmission | undefined>(
268
+ "@effect-agent/thread/internal/CurrentStartPreparation",
269
+ { defaultValue: () => undefined },
270
+ );
271
+
272
+ const CurrentStartDelivery = Context.Reference<
273
+ | {
274
+ readonly authorization: WorkerStartAdmission;
275
+ readonly admission: WorkerAdmission;
276
+ readonly inputDigest: Digest;
277
+ }
278
+ | undefined
279
+ >("@effect-agent/thread/internal/CurrentStartDelivery", { defaultValue: () => undefined });
280
+
266
281
  /** No in-memory ownership: every mutation is reserved in source canonical history with CAS. */
267
282
  export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
268
283
  options: WorkerRuntimeOptions,
@@ -290,9 +305,40 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
290
305
  const withCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
291
306
  Effect.provideService(effect, Crypto.Crypto, deps.crypto);
292
307
 
308
+ const currentStart = Effect.fnUntraced(function* (
309
+ admission: WorkerStartAdmission | undefined,
310
+ sourceThreadId: ThreadId,
311
+ sourceSubmissionId: SubmissionId | undefined,
312
+ principal: Principal,
313
+ targetAgentId: AgentId,
314
+ continuationOf: WorkerContinuation | undefined,
315
+ ) {
316
+ if (
317
+ admission === undefined ||
318
+ admission.sourceThreadId !== sourceThreadId ||
319
+ admission.sourceSubmissionId !== sourceSubmissionId ||
320
+ (admission.principal !== principal && admission.requestedPrincipal !== principal) ||
321
+ admission.targetAgentId !== targetAgentId ||
322
+ !Schema.toEquivalence(Schema.UndefinedOr(WorkerContinuation))(
323
+ admission.continuationOf,
324
+ continuationOf,
325
+ )
326
+ )
327
+ return undefined;
328
+
329
+ const tail = yield* deps.store
330
+ .inspectTail(ThreadTailRequest.make({ threadId: sourceThreadId }))
331
+ .pipe(Effect.mapError(storageFailure("start")));
332
+
333
+ return tail.tailSequence === admission.tailSequence && tail.tailDigest === admission.tailDigest
334
+ ? admission
335
+ : undefined;
336
+ });
337
+
293
338
  const authorizeBudget = Effect.fn("WorkerHost.authorizeBudget")(function* (
294
339
  origin: WorkerOrigin,
295
340
  principal: Principal,
341
+ admission?: WorkerStartAdmission,
296
342
  ) {
297
343
  yield* verifyContinuation(origin);
298
344
  if (origin.budgetScope !== "worker-run") return;
@@ -300,6 +346,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
300
346
  if (origin.depth !== 1) return yield* failure("start", "denied");
301
347
 
302
348
  yield* deps.budgetAuthorizer.authorize({
349
+ ...(admission === undefined ? {} : { admission }),
303
350
  source: origin.source,
304
351
  principal,
305
352
  worker: origin.worker,
@@ -1049,6 +1096,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1049
1096
  input: PersistedJson,
1050
1097
  principal: Principal,
1051
1098
  retainedFrameworkInput: boolean,
1099
+ authorization?: WorkerStartAdmission,
1052
1100
  ): Effect.fn.Return<void, WorkerError> {
1053
1101
  const origin = admission.origin;
1054
1102
 
@@ -1133,6 +1181,11 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1133
1181
  ? {
1134
1182
  ...targetRequest,
1135
1183
  _tag: "InitialInput",
1184
+ ...(authorization === undefined ||
1185
+ authorization.tailSequence !== current.tailSequence ||
1186
+ authorization.tailDigest !== current.tailDigest
1187
+ ? {}
1188
+ : { admission: authorization }),
1136
1189
  ...(origin.continuationOf === undefined
1137
1190
  ? {}
1138
1191
  : { continuationOf: origin.continuationOf }),
@@ -1351,7 +1404,24 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1351
1404
  }
1352
1405
 
1353
1406
  yield* verifyContinuation(admission.origin);
1407
+ const delivery = yield* CurrentStartDelivery;
1408
+
1409
+ const authorization =
1410
+ delivery !== undefined &&
1411
+ sameAdmission(delivery.admission, admission) &&
1412
+ delivery.inputDigest === inputDigest
1413
+ ? yield* currentStart(
1414
+ delivery.authorization,
1415
+ admission.origin.source.threadId,
1416
+ admission.sourceSubmissionId,
1417
+ options.principal,
1418
+ agentId,
1419
+ admission.origin.continuationOf,
1420
+ )
1421
+ : undefined;
1422
+
1354
1423
  yield* deps.authorizer.authorize({
1424
+ ...(authorization === undefined ? {} : { admission: authorization }),
1355
1425
  sourceThreadId: admission.origin.source.threadId,
1356
1426
  ...(admission.sourceSubmissionId === undefined
1357
1427
  ? {}
@@ -1364,7 +1434,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1364
1434
  ? {}
1365
1435
  : { continuationOf: admission.origin.continuationOf }),
1366
1436
  });
1367
- yield* authorizeBudget(admission.origin, options.principal);
1437
+ yield* authorizeBudget(admission.origin, options.principal, authorization);
1368
1438
  if (
1369
1439
  admission.origin.worker.threadId !== options.threadId ||
1370
1440
  admission.origin.worker.targetAgentId !== agentId ||
@@ -1378,7 +1448,14 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1378
1448
 
1379
1449
  if (retainedFrameworkInput && admission.reportKind !== "update")
1380
1450
  yield* validateCompletion(options.messageAdmission, options, agentId, inputDigest);
1381
- yield* reservation(admission, inputDigest, input, options.principal, retainedFrameworkInput);
1451
+ yield* reservation(
1452
+ admission,
1453
+ inputDigest,
1454
+ input,
1455
+ options.principal,
1456
+ retainedFrameworkInput,
1457
+ authorization,
1458
+ );
1382
1459
 
1383
1460
  return admission;
1384
1461
  });
@@ -1866,13 +1943,17 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1866
1943
  principal: Principal,
1867
1944
  sourceSubmissionId?: SubmissionId,
1868
1945
  ): SubagentHost["Service"] => {
1946
+ const requestedPrincipal = principal;
1947
+
1869
1948
  const authorize = (
1870
1949
  operation: WorkerError["operation"],
1871
1950
  access: "context" | "read" | "send" | "control",
1872
1951
  worker?: WorkerRef,
1873
1952
  continuationOf?: WorkerContinuation,
1953
+ admission?: WorkerStartAdmission,
1874
1954
  ) =>
1875
1955
  deps.authorizer.authorize({
1956
+ ...(admission === undefined ? {} : { admission }),
1876
1957
  sourceThreadId: context.source.threadId,
1877
1958
  ...(sourceSubmissionId === undefined ? {} : { sourceSubmissionId }),
1878
1959
  principal,
@@ -1894,6 +1975,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1894
1975
  target: Agent.AnyDefinition,
1895
1976
  encodedInput: unknown,
1896
1977
  continuationOf?: WorkerContinuation,
1978
+ authorization?: WorkerStartAdmission,
1897
1979
  ) {
1898
1980
  const resolved = yield* binding(target, "start");
1899
1981
 
@@ -1908,8 +1990,18 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1908
1990
 
1909
1991
  const source = yield* sourceAuthority(context.source.threadId, sourceSubmissionId);
1910
1992
 
1993
+ const admission = yield* currentStart(
1994
+ authorization,
1995
+ context.source.threadId,
1996
+ sourceSubmissionId,
1997
+ principal,
1998
+ target.id,
1999
+ continuationOf,
2000
+ );
2001
+
1911
2002
  const policy = yield* resolveTargetPolicy({
1912
2003
  _tag: "InitialInput",
2004
+ ...(admission === undefined ? {} : { admission }),
1913
2005
  ...(continuationOf === undefined ? {} : { continuationOf }),
1914
2006
  definition: resolved.definition,
1915
2007
  definitions: resolved.digests,
@@ -2064,6 +2156,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2064
2156
  encodedParameters: unknown,
2065
2157
  principal: Principal,
2066
2158
  operation: "start" | "followUp",
2159
+ authorization?: WorkerStartAdmission,
2067
2160
  ) {
2068
2161
  if (Option.isNone(deps.deliveries)) return yield* failure(operation, "unavailable");
2069
2162
  const deliveries = deps.deliveries.value;
@@ -2146,17 +2239,25 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2146
2239
  Effect.provideService(MessageDeliveryStore, deliveries),
2147
2240
  Effect.provideService(PreparedInputAdmission, {
2148
2241
  submit: (prepared) =>
2149
- deps
2150
- .submit(prepared)
2151
- .pipe(
2152
- Effect.mapError((error) =>
2153
- error._tag === "AdmissionConflict" || error._tag === "AgentInputError"
2154
- ? ScheduledInputRefused.make({ code: error._tag, cause: error })
2155
- : error._tag === "AdmissionPolicyError" && error.reason === "refused"
2156
- ? ScheduledInputRefused.make({ code: error.code, cause: error })
2157
- : ScheduledInputRetryable.make({ reason: "storage", cause: error }),
2158
- ),
2242
+ deps.submit(prepared).pipe(
2243
+ Effect.provideService(
2244
+ CurrentStartDelivery,
2245
+ authorization !== undefined &&
2246
+ prepared.workerAdmission !== undefined &&
2247
+ prepared.admissionKey === messageId &&
2248
+ prepared.inputDigest === inputDigest &&
2249
+ sameOrigin(prepared.workerAdmission.origin, origin)
2250
+ ? { authorization, admission: prepared.workerAdmission, inputDigest }
2251
+ : undefined,
2159
2252
  ),
2253
+ Effect.mapError((error) =>
2254
+ error._tag === "AdmissionConflict" || error._tag === "AgentInputError"
2255
+ ? ScheduledInputRefused.make({ code: error._tag, cause: error })
2256
+ : error._tag === "AdmissionPolicyError" && error.reason === "refused"
2257
+ ? ScheduledInputRefused.make({ code: error.code, cause: error })
2258
+ : ScheduledInputRetryable.make({ reason: "storage", cause: error }),
2259
+ ),
2260
+ ),
2160
2261
  submissionStatus: (receipt) =>
2161
2262
  deps
2162
2263
  .status(receipt)
@@ -2393,10 +2494,23 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2393
2494
  request.continuationOf.delegationId,
2394
2495
  );
2395
2496
 
2396
- yield* authorize("start", "send", undefined, previous?.evidence);
2497
+ const admission = yield* currentStart(
2498
+ yield* CurrentStartPreparation,
2499
+ context.source.threadId,
2500
+ sourceSubmissionId,
2501
+ principal,
2502
+ request.target.id,
2503
+ previous?.evidence,
2504
+ );
2505
+
2506
+ yield* authorize("start", "send", undefined, previous?.evidence, admission);
2397
2507
 
2398
- return (yield* preparedTarget(request.target, request.encodedInput, previous?.evidence))
2399
- .policy;
2508
+ return (yield* preparedTarget(
2509
+ request.target,
2510
+ request.encodedInput,
2511
+ previous?.evidence,
2512
+ admission,
2513
+ )).policy;
2400
2514
  }),
2401
2515
  start: Effect.fn("WorkerHost.start")(function* <E = never, R = never>(
2402
2516
  command: StartWorkerRequest | DeferredStartWorkerRequest<E, R>,
@@ -2411,8 +2525,26 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2411
2525
  command.delegationId,
2412
2526
  );
2413
2527
 
2528
+ // Bind the decision to the tail before authorization. If it changes while the
2529
+ // authorizer reads current state, currentStart rejects reuse of this decision.
2530
+ const tail = yield* deps.store
2531
+ .inspectTail(ThreadTailRequest.make({ threadId: context.source.threadId }))
2532
+ .pipe(Effect.mapError(storageFailure("start")));
2533
+
2414
2534
  const principal = yield* authorize("start", "send", undefined, previous?.evidence);
2415
2535
 
2536
+ const startAdmission: WorkerStartAdmission = {
2537
+ sourceThreadId: context.source.threadId,
2538
+ ...(sourceSubmissionId === undefined ? {} : { sourceSubmissionId }),
2539
+ requestedPrincipal,
2540
+ principal,
2541
+ targetAgentId: command.target.id,
2542
+ ...(previous === undefined ? {} : { continuationOf: previous.evidence }),
2543
+ access: "send",
2544
+ tailSequence: tail.tailSequence,
2545
+ tailDigest: tail.tailDigest,
2546
+ };
2547
+
2416
2548
  if (context.depth !== 0 && sourceSubmissionId === undefined)
2417
2549
  return yield* failure("start", "denied");
2418
2550
 
@@ -2507,7 +2639,14 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2507
2639
 
2508
2640
  return yield* Effect.gen(function* () {
2509
2641
  const request =
2510
- "prepare" in command ? { ...(yield* command.prepare), ...command } : command;
2642
+ "prepare" in command
2643
+ ? {
2644
+ ...(yield* command.prepare.pipe(
2645
+ Effect.provideService(CurrentStartPreparation, startAdmission),
2646
+ )),
2647
+ ...command,
2648
+ }
2649
+ : command;
2511
2650
 
2512
2651
  const currentPrevious =
2513
2652
  command.continuationOf === undefined
@@ -2535,6 +2674,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2535
2674
  request.target,
2536
2675
  request.encodedInput,
2537
2676
  currentPrevious?.evidence,
2677
+ startAdmission,
2538
2678
  );
2539
2679
 
2540
2680
  const resolved = prepared.resolved;
@@ -2596,7 +2736,18 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2596
2736
  "start",
2597
2737
  );
2598
2738
 
2599
- yield* authorizeBudget(origin, principal);
2739
+ yield* authorizeBudget(
2740
+ origin,
2741
+ principal,
2742
+ yield* currentStart(
2743
+ startAdmission,
2744
+ context.source.threadId,
2745
+ sourceSubmissionId,
2746
+ principal,
2747
+ request.target.id,
2748
+ currentPrevious?.evidence,
2749
+ ),
2750
+ );
2600
2751
 
2601
2752
  const targetPolicy = Option.isSome(prepared.policy)
2602
2753
  ? prepared.policy.value
@@ -2623,6 +2774,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2623
2774
  request.encodedParameters,
2624
2775
  principal,
2625
2776
  "start",
2777
+ startAdmission,
2626
2778
  );
2627
2779
 
2628
2780
  return { worker: origin.worker, delivery };
@@ -3248,8 +3248,8 @@ export const formatRunStatus = (view: RunStatusView): string => {
3248
3248
  /**
3249
3249
  * The run-status message is derived per request and appended only to the
3250
3250
  * OUTGOING prompt: official history and durable commits never carry it, so it
3251
- * can never accumulate or replay. OpenAI receives it as trailing system guidance,
3252
- * leaving the last user/tool result as the implicit cache-write boundary.
3251
+ * can never accumulate or replay. Capable adapters receive trailing system guidance,
3252
+ * leaving the preceding user/tool result available as a cache-write boundary.
3253
3253
  */
3254
3254
  const outgoingModelPrompt = (
3255
3255
  policy: AgentPolicy,
@@ -3257,13 +3257,13 @@ const outgoingModelPrompt = (
3257
3257
  prepared: Prompt.Prompt,
3258
3258
  turn: number,
3259
3259
  declaredToolCalls: number,
3260
- ): Effect.Effect<Prompt.Prompt, never, Model.ProviderName> =>
3260
+ systemMessagesInHistory: boolean,
3261
+ ): Effect.Effect<Prompt.Prompt> =>
3261
3262
  Effect.gen(function* () {
3262
3263
  if (policy.runStatus !== "appended") {
3263
3264
  return prepared;
3264
3265
  }
3265
3266
  const now = yield* Clock.currentTimeMillis;
3266
- const provider = yield* Model.ProviderName;
3267
3267
 
3268
3268
  const status = formatRunStatus({
3269
3269
  turn,
@@ -3280,7 +3280,7 @@ const outgoingModelPrompt = (
3280
3280
 
3281
3281
  return Prompt.fromMessages([
3282
3282
  ...prepared.content,
3283
- provider === "openai"
3283
+ systemMessagesInHistory
3284
3284
  ? Prompt.systemMessage({ content: status })
3285
3285
  : Prompt.userMessage({ content: [Prompt.textPart({ text: status })] }),
3286
3286
  ]);
@@ -3880,6 +3880,8 @@ const estimateContextTokens = Effect.fn("AgentRuntime.estimateContextTokens")(fu
3880
3880
  const nextContextEstimate = Effect.fn("AgentRuntime.nextContextEstimate")(function* (
3881
3881
  context: RunContext,
3882
3882
  view: ReadonlyArray<Prompt.Message>,
3883
+ systemMessagesInHistory: boolean,
3884
+ staticInstructions: Prompt.RawInput | undefined,
3883
3885
  ) {
3884
3886
  const state = context.compaction;
3885
3887
 
@@ -3896,7 +3898,12 @@ const nextContextEstimate = Effect.fn("AgentRuntime.nextContextEstimate")(functi
3896
3898
  }
3897
3899
 
3898
3900
  return yield* estimateContextTokens(
3899
- prepareModelPrompt(Prompt.fromMessages(view), undefined).content,
3901
+ prepareModelPrompt(
3902
+ Prompt.fromMessages(view),
3903
+ undefined,
3904
+ systemMessagesInHistory,
3905
+ staticInstructions,
3906
+ ).content,
3900
3907
  );
3901
3908
  });
3902
3909
 
@@ -5551,6 +5558,25 @@ const makeTurn = <
5551
5558
  const withCallModel = <A, E, R>(operation: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
5552
5559
  modelServices === undefined ? operation : Effect.provide(operation, modelServices);
5553
5560
 
5561
+ const provider = yield* withCallModel(Model.ProviderName);
5562
+
5563
+ // Older adapters do not expose this native capability. Keep their safe
5564
+ // projection; only the established OpenAI and xAI transports default to it.
5565
+ // Evaluate against this call's services so scoped model overrides are honored.
5566
+ const callModel: LanguageModel.LanguageModel & {
5567
+ readonly supportsSystemMessagesInHistory?: Effect.Effect<boolean> | undefined;
5568
+ } = yield* withCallModel(LanguageModel.LanguageModel);
5569
+
5570
+ const systemMessagesInHistory =
5571
+ callModel.supportsSystemMessagesInHistory === undefined
5572
+ ? provider === "openai" || provider === "xai"
5573
+ : yield* withCallModel(callModel.supportsSystemMessagesInHistory);
5574
+
5575
+ const staticInstructions =
5576
+ typeof agent.definition.instructions === "function"
5577
+ ? undefined
5578
+ : agent.definition.instructions;
5579
+
5554
5580
  const priorCompactionPrefix = context.preparedCompactionSource?.prefix;
5555
5581
 
5556
5582
  if (priorCompactionPrefix !== undefined && priorCompactionPrefix.length > 0) {
@@ -5690,9 +5716,14 @@ const makeTurn = <
5690
5716
 
5691
5717
  const canonicalDecoration = !admissionRequired
5692
5718
  ? Prompt.empty
5693
- : yield* outgoingModelPrompt(policy, context, Prompt.empty, turn, priorToolCalls).pipe(
5694
- withCallModel,
5695
- );
5719
+ : yield* outgoingModelPrompt(
5720
+ policy,
5721
+ context,
5722
+ Prompt.empty,
5723
+ turn,
5724
+ priorToolCalls,
5725
+ systemMessagesInHistory,
5726
+ ).pipe(withCallModel);
5696
5727
 
5697
5728
  const estimateToolSchemaTokens = Effect.suspend(() => {
5698
5729
  const choice = modelToolChoice();
@@ -5752,8 +5783,15 @@ const makeTurn = <
5752
5783
  // canonical indices still locate appended content and compaction coverage.
5753
5784
  const estimateSourceContext = (view: ReadonlyArray<Prompt.Message>) =>
5754
5785
  options.context === undefined && options.transientContext === undefined
5755
- ? nextContextEstimate(context, view)
5756
- : estimateCallTokens(prepareModelPrompt(Prompt.fromMessages(view), undefined).content);
5786
+ ? nextContextEstimate(context, view, systemMessagesInHistory, staticInstructions)
5787
+ : estimateCallTokens(
5788
+ prepareModelPrompt(
5789
+ Prompt.fromMessages(view),
5790
+ undefined,
5791
+ systemMessagesInHistory,
5792
+ staticInstructions,
5793
+ ).content,
5794
+ );
5757
5795
 
5758
5796
  let prepared = buildCompactedView(modelContext.prompt.content, context.compaction);
5759
5797
  let sourceTokens: number | undefined;
@@ -6005,6 +6043,7 @@ const makeTurn = <
6005
6043
  transientContext,
6006
6044
  turn,
6007
6045
  priorToolCalls,
6046
+ systemMessagesInHistory,
6008
6047
  ).pipe(withCallModel);
6009
6048
 
6010
6049
  const derivedPromptContentTokens = !admissionRequired
@@ -6186,9 +6225,12 @@ const makeTurn = <
6186
6225
  prepareModelPrompt(
6187
6226
  Prompt.fromMessages([...basis.content, ...transientContext.content]),
6188
6227
  outputContract._tag === "rendered" ? outputContract.part : undefined,
6228
+ systemMessagesInHistory,
6229
+ staticInstructions,
6189
6230
  ),
6190
6231
  turn,
6191
6232
  priorToolCalls,
6233
+ systemMessagesInHistory,
6192
6234
  ).pipe(
6193
6235
  withCallModel,
6194
6236
  Effect.flatMap((providerPrompt) =>
@@ -6759,7 +6801,16 @@ const makeTurn = <
6759
6801
  const settleOrFollowUp = (history: Prompt.Prompt) =>
6760
6802
  Effect.gen(function* () {
6761
6803
  yield* advanceHistory(context, history, options);
6762
- const steering = yield* drainInputs(context, options);
6804
+
6805
+ // Do not consume durable receipts that this completed Turn cannot cover.
6806
+ // Final-answer mode permits one grace Turn, but never a second finalization.
6807
+ const turnsBlocked =
6808
+ policy.onExhaustion === "fail" ? turn >= bounds.maxTurns : turn > bounds.maxTurns;
6809
+
6810
+ const steering =
6811
+ turnsBlocked || context.finalizationUsed
6812
+ ? []
6813
+ : yield* drainInputs(context, options);
6763
6814
 
6764
6815
  const queued =
6765
6816
  steering.length > 0
@@ -6771,9 +6822,6 @@ const makeTurn = <
6771
6822
  // `maxTurns` (RUN-019): `turn > maxTurns` can only be
6772
6823
  // `maxTurns + 1`, so a second grace is structurally
6773
6824
  // impossible.
6774
- const turnsBlocked =
6775
- policy.onExhaustion === "fail" ? turn >= bounds.maxTurns : turn > bounds.maxTurns;
6776
-
6777
6825
  if (turnsBlocked) {
6778
6826
  return failRunEventStream(
6779
6827
  AgentPolicyError.make({
@@ -7187,9 +7235,24 @@ const toolBatchContinuation = <
7187
7235
  }
7188
7236
  }
7189
7237
 
7190
- if (Option.isSome(selectedOutput)) {
7238
+ // A completion Tool can admit steering while its handler runs. Only claim it
7239
+ // when another ordinary Turn can cover it; otherwise keep it queued for a new Run.
7240
+ const bounds = effectiveRunBounds(agent.definition.policy, options);
7241
+
7242
+ const canContinue =
7243
+ turn < bounds.maxTurns &&
7244
+ (completion?.required === true
7245
+ ? toolCalls + context.programmaticToolCalls < bounds.maxToolCalls
7246
+ : toolCalls + context.programmaticToolCalls <= bounds.maxToolCalls) &&
7247
+ !context.tokenExhausted &&
7248
+ !context.finalizationUsed &&
7249
+ (yield* Clock.currentTimeMillis) < context.durationDeadlineMillis;
7250
+
7251
+ const steering =
7252
+ Option.isSome(selectedOutput) && !canContinue ? [] : yield* drainInputs(context, options);
7253
+
7254
+ if (Option.isSome(selectedOutput) && steering.length === 0) {
7191
7255
  const output = selectedOutput.value;
7192
- const bounds = effectiveRunBounds(agent.definition.policy, options);
7193
7256
 
7194
7257
  const exhausted = context.tokenExhausted
7195
7258
  ? "tokens"
@@ -7225,7 +7288,6 @@ const toolBatchContinuation = <
7225
7288
  ),
7226
7289
  );
7227
7290
  }
7228
- const steering = yield* drainInputs(context, options);
7229
7291
  const nextPrompt = yield* appendInputs(context, history, steering, options);
7230
7292
 
7231
7293
  return nextTurn(nextPrompt, turn + 1, toolCalls);
@@ -147,22 +147,81 @@ export const outputSchemaContract = (definition: Agent.AnyDefinition): OutputCon
147
147
  const sameSystemMessage = Schema.toEquivalence(Prompt.SystemMessage);
148
148
 
149
149
  /**
150
- * Project system instructions and the output contract into one leading block.
151
- * Repeated Runs append instructions to canonical history; letting those copies
152
- * move the contract breaks the preceding user/tool prefix used by provider caches.
153
- * Keep the last occurrence of each equivalent system message, preserving the
154
- * precedence of distinct instructions and their native provider options.
150
+ * Keep supported system instructions in conversation order so changing late guidance
151
+ * cannot invalidate the preceding user/tool cache prefix. Omit an exact repeat
152
+ * only when no distinct system instruction intervened; returning to an earlier
153
+ * instruction after a different one must preserve the new directive's precedence.
154
+ * Anchor the immutable output contract after the initial system block.
155
+ * Restore leading static instructions for caller-supplied conversation-only history.
155
156
  *
156
157
  * This runs after preparation and compaction. Canonical messages, protected
157
158
  * instruction/input spans and compaction coverage retain their original positions.
158
- * Conversation order and message identities are unchanged. A single system block
159
- * also prevents Anthropic's last-system-group conversion from discarding earlier
160
- * application instructions or the output contract.
159
+ * Adapters without this capability retain the grouped-system projection: keep the
160
+ * last equivalent instruction with its native options, then the contract and
161
+ * conversation. This prevents older Anthropic adapters from discarding system groups.
161
162
  */
162
163
  export const prepareModelPrompt = (
163
164
  prompt: Prompt.Prompt,
164
165
  contract: Prompt.SystemMessage | undefined,
166
+ systemMessagesInHistory: boolean,
167
+ staticInstructions: Prompt.RawInput | undefined,
165
168
  ): Prompt.Prompt => {
169
+ if (systemMessagesInHistory) {
170
+ const content: Array<Prompt.Message> = [];
171
+ let lastSystem: Prompt.SystemMessage | undefined;
172
+ let ordered = prompt.content;
173
+
174
+ if (staticInstructions !== undefined) {
175
+ const firstConversation = prompt.content.findIndex((message) => message.role !== "system");
176
+
177
+ const leading = prompt.content.slice(
178
+ 0,
179
+ firstConversation === -1 ? prompt.content.length : firstConversation,
180
+ );
181
+
182
+ const restored: Array<Prompt.Message> = [];
183
+
184
+ const instructions =
185
+ typeof staticInstructions === "string"
186
+ ? [Prompt.systemMessage({ content: staticInstructions })]
187
+ : Prompt.make(staticInstructions).content;
188
+
189
+ for (const message of instructions) {
190
+ if (message.role !== "system") break;
191
+ if (
192
+ leading.some(
193
+ (candidate) => candidate.role === "system" && sameSystemMessage(candidate, message),
194
+ )
195
+ )
196
+ break;
197
+
198
+ const original = prompt.content.find(
199
+ (candidate) => candidate.role === "system" && sameSystemMessage(candidate, message),
200
+ );
201
+
202
+ if (original === undefined) break;
203
+ restored.push(original);
204
+ }
205
+ if (restored.length > 0)
206
+ ordered = [...leading, ...restored, ...prompt.content.slice(leading.length)];
207
+ }
208
+
209
+ for (const message of ordered) {
210
+ if (message.role === "system") {
211
+ if (lastSystem !== undefined && sameSystemMessage(lastSystem, message)) continue;
212
+ lastSystem = message;
213
+ }
214
+ content.push(message);
215
+ }
216
+ if (contract !== undefined) {
217
+ const firstConversation = content.findIndex((message) => message.role !== "system");
218
+
219
+ content.splice(firstConversation === -1 ? content.length : firstConversation, 0, contract);
220
+ }
221
+
222
+ return Prompt.fromMessages(content);
223
+ }
224
+
166
225
  const systems: Array<Prompt.SystemMessage> = [];
167
226
  const conversation: Array<Prompt.Message> = [];
168
227
  const seen = new Map<string, Array<Prompt.SystemMessage>>();