@dynamicagents/core 0.9.0

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 (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. package/scripts/generate-keys.mjs +48 -0
@@ -0,0 +1,133 @@
1
+ const TURN_TAG_RE = /^<turn\b([^>]*)>([\s\S]*)<\/turn>$/;
2
+ const ATTR_RE = /(\w+)="([^"]*)"/g;
3
+ function parseAttrs(raw) {
4
+ const out = {};
5
+ let m;
6
+ ATTR_RE.lastIndex = 0;
7
+ while ((m = ATTR_RE.exec(raw)) !== null)
8
+ out[m[1]] = m[2];
9
+ return out;
10
+ }
11
+ const ATTR_UNESCAPES = {
12
+ amp: "&",
13
+ lt: "<",
14
+ gt: ">",
15
+ quot: '"'
16
+ };
17
+ /** Reverse the gatekeeper's attribute escaping — single pass so `&amp;` round-trips. */
18
+ function unescAttr(value) {
19
+ return value.replace(/&(amp|lt|gt|quot);/g, (_, e) => ATTR_UNESCAPES[e]);
20
+ }
21
+ /**
22
+ * Recover the structured provenance from a gatekeeper-authored turn. Returns null
23
+ * for any text that isn't a `<turn>` wrapper (plain messages, assistant replies),
24
+ * so callers can treat the provenance as optional.
25
+ */
26
+ export function parseTurn(text) {
27
+ const m = TURN_TAG_RE.exec(text);
28
+ if (!m)
29
+ return null;
30
+ const attrs = parseAttrs(m[1]);
31
+ if (!attrs.from || !attrs.id || !attrs.channel || !attrs.at)
32
+ return null;
33
+ return {
34
+ from: unescAttr(attrs.from),
35
+ id: unescAttr(attrs.id),
36
+ channel: unescAttr(attrs.channel),
37
+ at: unescAttr(attrs.at),
38
+ body: m[2]
39
+ };
40
+ }
41
+ /**
42
+ * Deterministic Session-message ids for the task round loop.
43
+ *
44
+ * A round runs inside a durable Workflow step that can re-run after a crash, so
45
+ * its Session appends must be exactly-once. `Session.appendMessage` already
46
+ * dedupes on message id (an id that exists is not re-written), so deriving the id
47
+ * from the task id + round makes the append idempotent for free — no
48
+ * read-then-write race, no duplicate turns in history on replay. Pair with
49
+ * {@link file://./session.ts appendOnce}, which reads the durable text back so a
50
+ * retry that re-inferred still returns the *stored* reply.
51
+ *
52
+ * These ids share the Session id-space with random UUIDs (plain turns) and the
53
+ * SDK's `compaction_`-prefixed summaries; the `task:` prefix cannot collide with
54
+ * either.
55
+ */
56
+ /** Id of the inbound user turn for a task (appended once, by round 0). */
57
+ export function taskUserMessageId(taskId) {
58
+ return `task:${taskId}:user`;
59
+ }
60
+ /**
61
+ * Id of the acknowledgment a **delegating** round publishes — the message the
62
+ * user sees while that round's Subtasks run. Also the anchor a later round finds
63
+ * to reattach the round's `delegate` call to (see
64
+ * {@link file://../round/turn.ts renderTurnMessages}), which is why it is
65
+ * derived from the round rather than stored.
66
+ */
67
+ export function roundAckMessageId(taskId, round) {
68
+ return `task:${taskId}:round:${round}:ack`;
69
+ }
70
+ /** Matches {@link roundAckMessageId}; the trailing suffix is fixed, so the greedy
71
+ * task-id group cannot swallow it. */
72
+ const ROUND_ACK_ID = /^task:(.+):round:(\d+):ack$/;
73
+ /**
74
+ * Recognize an acknowledgment id — **any** Task's, not just the one being
75
+ * rendered. A Session outlives the Task that wrote it and is shared by every Task
76
+ * from the same caller, so its history accumulates acks the current render has no
77
+ * branches for: earlier Tasks' acks, and (in the window between a round's ack
78
+ * append and its Subtask rows) this Task's own. Both are the agent's scaffolding
79
+ * rather than conversation, so both must stay out of the reference catalog; the
80
+ * `branches`-derived anchor map alone cannot see either.
81
+ *
82
+ * Returns the parsed Task id and round so the caller can tell whose ack it is.
83
+ */
84
+ export function parseRoundAckMessageId(id) {
85
+ const match = ROUND_ACK_ID.exec(id);
86
+ return match ? { taskId: match[1], round: Number(match[2]) } : null;
87
+ }
88
+ /** Id of the terminal reply — the round in which the agent answered the user. */
89
+ export function finalReplyMessageId(taskId) {
90
+ return `task:${taskId}:reply:final`;
91
+ }
92
+ /** A Sessions-store message with a caller-chosen (deterministic) id. */
93
+ export function deterministicSessionMessage(id, role, text) {
94
+ return {
95
+ id,
96
+ role,
97
+ createdAt: new Date(),
98
+ parts: [{ type: "text", text }]
99
+ };
100
+ }
101
+ /**
102
+ * A Sessions-store message with a fresh random id.
103
+ *
104
+ * The counterpart to {@link deterministicSessionMessage}, and choosing between
105
+ * them is a real decision rather than a style one. A deterministic id makes an
106
+ * append **idempotent**, which is what a durable-Workflow agent needs: its round
107
+ * can re-run after a crash, and the id is what stops the retry from duplicating a
108
+ * turn or rewriting a reply the user already received.
109
+ *
110
+ * A conversational agent that answers inline has no such replay to defend
111
+ * against — every turn is a new message and there is no step to re-enter — so a
112
+ * random id is correct and a synthesized deterministic one would be a lie about
113
+ * what is being deduplicated.
114
+ */
115
+ export function sessionMessage(role, text) {
116
+ return deterministicSessionMessage(crypto.randomUUID(), role, text);
117
+ }
118
+ /** Concatenate the text parts of a stored session message. */
119
+ export function sessionText(m) {
120
+ return m.parts
121
+ .filter((p) => p.type === "text" && typeof p.text === "string")
122
+ .map((p) => p.text)
123
+ .join("");
124
+ }
125
+ /** Convert stored history to AI-SDK model messages (user/assistant text only). */
126
+ export function toModelMessages(history) {
127
+ return history
128
+ .filter((m) => m.role === "user" || m.role === "assistant")
129
+ .map((m) => ({
130
+ role: m.role,
131
+ content: sessionText(m)
132
+ }));
133
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@dynamicagents/core/agent` — the primitives a loop is built from.
3
+ *
4
+ * The session, the model pair with its fallback, the budget, and the
5
+ * control-tool abstraction that turns "the model called something that ends the
6
+ * round" into a checked value. Everything both predecessor loops needed
7
+ * identically, and nothing about how a round is shaped.
8
+ *
9
+ * This subpath ships **no loop** — that is what makes it importable by one. A
10
+ * loop module needs these primitives without also pulling in a Durable Object
11
+ * base class and drizzle, which is why `DynamicAgent` lives in
12
+ * `@dynamicagents/core/host` and the delegating round loop in
13
+ * `@dynamicagents/core/round`.
14
+ *
15
+ * Core as a whole *does* now ship a loop, opt-in, in `/round`. The rule it
16
+ * still keeps is narrower and better: **core ships no prompt copy and no
17
+ * policy** — see `AGENTS.md`, "The line core does not cross". An agent that
18
+ * wants a different round shape imports none of `/round` and builds it from
19
+ * exactly what is here.
20
+ */
21
+ export { newTurnBudget, stepAllowance, type TurnBudget } from "./budget.js";
22
+ export { type AiGatewayMetadata, type ModelOverrides, type ModelPair, type ModelRuntime, type ModelRuntimeFactory } from "./model.js";
23
+ export { CredentialRejectedError, type CredentialRejectedBy } from "./errors.js";
24
+ export { createWorkersAIModelRuntime, workersAIModels, type WorkersAIRuntimeDeps } from "./workers-ai/index.js";
25
+ export { appendOnce, buildAgentSession, notifyingCompaction, type AgentSessionOptions, type SessionHost, type SessionLike } from "./session.js";
26
+ export { deterministicSessionMessage, finalReplyMessageId, parseRoundAckMessageId, parseTurn, roundAckMessageId, sessionMessage, sessionText, taskUserMessageId, toModelMessages, type ParsedTurn } from "./history.js";
27
+ export { buildIntermediateContentHandler, isTransientAiError, nonRecoverableKind, type NonRecoverableKind, type OnContent, type RoundFailureKind } from "./inference.js";
28
+ export { ControlCallError, controlTools, controlToolSet, type ControlTool, type TurnDecision } from "./control.js";
29
+ export { FINAL_REPLY_TOOL_NAME, finalReplyInputSchema, finalReplyTool } from "./final-reply.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@dynamicagents/core/agent` — the primitives a loop is built from.
3
+ *
4
+ * The session, the model pair with its fallback, the budget, and the
5
+ * control-tool abstraction that turns "the model called something that ends the
6
+ * round" into a checked value. Everything both predecessor loops needed
7
+ * identically, and nothing about how a round is shaped.
8
+ *
9
+ * This subpath ships **no loop** — that is what makes it importable by one. A
10
+ * loop module needs these primitives without also pulling in a Durable Object
11
+ * base class and drizzle, which is why `DynamicAgent` lives in
12
+ * `@dynamicagents/core/host` and the delegating round loop in
13
+ * `@dynamicagents/core/round`.
14
+ *
15
+ * Core as a whole *does* now ship a loop, opt-in, in `/round`. The rule it
16
+ * still keeps is narrower and better: **core ships no prompt copy and no
17
+ * policy** — see `AGENTS.md`, "The line core does not cross". An agent that
18
+ * wants a different round shape imports none of `/round` and builds it from
19
+ * exactly what is here.
20
+ */
21
+ export { newTurnBudget, stepAllowance } from "./budget.js";
22
+ export {} from "./model.js";
23
+ export { CredentialRejectedError } from "./errors.js";
24
+ export { createWorkersAIModelRuntime, workersAIModels } from "./workers-ai/index.js";
25
+ export { appendOnce, buildAgentSession, notifyingCompaction } from "./session.js";
26
+ export { deterministicSessionMessage, finalReplyMessageId, parseRoundAckMessageId, parseTurn, roundAckMessageId, sessionMessage, sessionText, taskUserMessageId, toModelMessages } from "./history.js";
27
+ export { buildIntermediateContentHandler, isTransientAiError, nonRecoverableKind } from "./inference.js";
28
+ export { ControlCallError, controlTools, controlToolSet } from "./control.js";
29
+ export { FINAL_REPLY_TOOL_NAME, finalReplyInputSchema, finalReplyTool } from "./final-reply.js";
@@ -0,0 +1,110 @@
1
+ import type { StepResult, ToolSet } from "ai";
2
+ /**
3
+ * Shared Workers-AI plumbing for the agent's inference operations — the pieces
4
+ * every model call needs regardless of *which* operation it belongs to.
5
+ *
6
+ * The two loops themselves are deliberately separate, not layered on a common
7
+ * one: the main agent's Session-coupled round lives in
8
+ * {@link file://../round/turn.ts turn.ts}, and the Session-less subagent loop in
9
+ * {@link file://../subagent/run.ts run.ts}. They share error classification and
10
+ * progress streaming; their control flow has nothing in common worth abstracting.
11
+ */
12
+ /**
13
+ * Called with each **intermediate** assistant content message — text the model
14
+ * emits in a step that also makes tool calls (`finishReason:"tool-calls"`), i.e.
15
+ * before the final reply. Used to stream those messages out live; the final reply
16
+ * is the operation's return value, not an `onContent` call. `stepIndex` is the
17
+ * 0-based step ordinal (stable enough across a primary→fallback re-run for the
18
+ * gatekeeper to dedupe on). Best-effort — the caller must swallow its own failures.
19
+ */
20
+ export type OnContent = (text: string, stepIndex: number) => void | Promise<void>;
21
+ /**
22
+ * Whether an error is a transient availability condition rather than a
23
+ * deterministic bad-output one.
24
+ *
25
+ * The distinction decides who handles it: transient throws out of the attempt loop
26
+ * so the Workflow step retries the whole round, while everything else burns the
27
+ * model slot and hands over to the fallback. Classifying a capacity blip as
28
+ * deterministic is the expensive mistake — it spends both slots on an outage and
29
+ * fails a Task that would have succeeded a second later.
30
+ *
31
+ * Structured signals first: the SDK's own `APICallError.isRetryable`, then the
32
+ * status code, then `RetryError` (raised once the SDK's internal backoff is
33
+ * exhausted). The message fragments stay as the last resort for the Workers-AI
34
+ * error codes, which arrive as prose on a plain `Error`.
35
+ */
36
+ export declare function isTransientAiError(err: unknown): boolean;
37
+ /**
38
+ * Why a round stopped without a second attempt being worth making.
39
+ *
40
+ * A stable string rather than the error itself, because this value crosses two
41
+ * serialization boundaries — the DO's RPC return and a Workflow step result —
42
+ * and an `Error` survives neither reliably. The host maps it to operator-facing
43
+ * copy; core never owns that wording.
44
+ *
45
+ * The credential kinds are separate strings rather than one, because they have
46
+ * different remedies and the host cannot tell them apart afterwards:
47
+ *
48
+ * - `credential` — the model provider rejected the token. Rotate that one.
49
+ * - `gateway-credential` — the AI Gateway *in front of* the provider rejected the
50
+ * request, which the provider therefore never saw. Rotate the AI Gateway token
51
+ * (`cf-aig-authorization`) instead; the model credential is very likely fine.
52
+ * - `unknown-credential` — a `401`/`403` matching none of the shapes. Says so,
53
+ * rather than picking one and sending an operator to rotate a working secret.
54
+ *
55
+ * A fourth, `proxy-credential`, was removed in 0.8.0 along with
56
+ * {@link file://./errors.ts CredentialRejectedBy}'s `"proxy"` arm. Adding a kind
57
+ * back is a breaking change for every consumer, because the `Record` they map it
58
+ * with is total — which is the property that makes a new kind impossible to
59
+ * ignore, and the reason to remove one rather than leave it unreachable.
60
+ */
61
+ export type NonRecoverableKind = "credential" | "gateway-credential" | "unknown-credential";
62
+ /**
63
+ * Why a round ended with no answer — one terminal status, two situations.
64
+ *
65
+ * `exhausted` is the ladder run to the end: both slots tried, every repair
66
+ * spent, nothing usable produced. Every other member is the ladder stopping
67
+ * early, because nothing further could have cleared the fault — see
68
+ * {@link nonRecoverableKind}.
69
+ *
70
+ * The distinction is a *reason*, not an outcome: both deliver a failed Task with
71
+ * the same shape. What it decides is the words, and only the host has those (see
72
+ * `HandleTaskDeps.failureCopy`) — which is why this is a total union rather than
73
+ * an optional field. A consumer that maps kinds to copy is then a `Record` the
74
+ * compiler checks, and a new kind cannot be silently ignored by any of them.
75
+ */
76
+ export type RoundFailureKind = "exhausted" | NonRecoverableKind;
77
+ /**
78
+ * Whether an error is one that **no** further attempt can clear, and the reason.
79
+ *
80
+ * This is the third classification, and the one the other two cannot express.
81
+ * {@link isTransientAiError} splits failures into "retry the step" (`true`) and
82
+ * "burn this slot, try the fallback" (`false`) — and for a rejected credential
83
+ * *both* are wrong. Retrying spends the Workflow's budget on a request that can
84
+ * never succeed; falling back spends the second slot presenting the *same* dead
85
+ * token. Returning `false` from the transient check only avoids the first.
86
+ *
87
+ * So the attempt ladders check this **before** entering the fallback slot and
88
+ * stop there, and `runHandleTask` ends the Task with copy the host supplies.
89
+ * Nothing is retried and nothing is spent proving the obvious twice.
90
+ *
91
+ * Keyed on {@link file://./errors.ts CredentialRejectedError}, which is neutral
92
+ * and structurally matched — so a provider outside core raises one and gets this
93
+ * handling with nothing here to change.
94
+ */
95
+ export declare function nonRecoverableKind(err: unknown): NonRecoverableKind | undefined;
96
+ /**
97
+ * Returns a fresh `onStepEnd` callback for one `generateText` attempt.
98
+ * Fires `onContent` for each intermediate step (text that accompanies tool
99
+ * calls); the final step is skipped because its text is the operation's return
100
+ * value. A fresh handler per attempt resets the 0-based `stepIndex` counter so a
101
+ * primary→fallback re-run reuses the same indices and the gatekeeper dedupes.
102
+ *
103
+ * `terminalToolNames` are the loop's **halting** control tools (e.g. the main
104
+ * agent's `delegate`): a step that calls one still has `finishReason:"tool-calls"`,
105
+ * but it is the round's *final* step, and its accompanying text is the round's
106
+ * acknowledgment — which the caller publishes separately as a milestone. Streaming
107
+ * it here too would double-post the same text under a second messageId, so those
108
+ * steps are skipped. Default `[]` (the subagent loop has no control tools).
109
+ */
110
+ export declare function buildIntermediateContentHandler(onContent: OnContent, terminalToolNames?: string[]): (step: StepResult<ToolSet>) => Promise<void>;
@@ -0,0 +1,120 @@
1
+ import { APICallError, RetryError } from "ai";
2
+ // Type-only would not work: this is a runtime guard. `errors.ts` is the neutral
3
+ // sibling of `model.ts` and imports nothing, so this reaches no provider.
4
+ import { CredentialRejectedError } from "./errors.js";
5
+ /** Workers-AI error codes and message fragments that mean "try again later". */
6
+ const TRANSIENT_MESSAGE_FRAGMENTS = [
7
+ "3040",
8
+ "3046",
9
+ "capacity temporarily exceeded",
10
+ "request timeout",
11
+ "rate limit",
12
+ "too many requests",
13
+ "overloaded",
14
+ "service unavailable"
15
+ ];
16
+ /** HTTP statuses worth another attempt: timeout, conflict, throttle, any 5xx. */
17
+ function isRetryableStatus(status) {
18
+ if (status === undefined)
19
+ return false;
20
+ return status === 408 || status === 409 || status === 429 || status >= 500;
21
+ }
22
+ /**
23
+ * Whether an error is a transient availability condition rather than a
24
+ * deterministic bad-output one.
25
+ *
26
+ * The distinction decides who handles it: transient throws out of the attempt loop
27
+ * so the Workflow step retries the whole round, while everything else burns the
28
+ * model slot and hands over to the fallback. Classifying a capacity blip as
29
+ * deterministic is the expensive mistake — it spends both slots on an outage and
30
+ * fails a Task that would have succeeded a second later.
31
+ *
32
+ * Structured signals first: the SDK's own `APICallError.isRetryable`, then the
33
+ * status code, then `RetryError` (raised once the SDK's internal backoff is
34
+ * exhausted). The message fragments stay as the last resort for the Workers-AI
35
+ * error codes, which arrive as prose on a plain `Error`.
36
+ */
37
+ export function isTransientAiError(err) {
38
+ // Checked first because a rejected credential's message can carry "rate
39
+ // limit"-adjacent prose the fragment scan below would misread as transient.
40
+ // Note that `false` alone does not protect the fallback slot — see
41
+ // {@link nonRecoverableKind}, which is what actually stops the ladder.
42
+ if (nonRecoverableKind(err) !== undefined)
43
+ return false;
44
+ if (APICallError.isInstance(err)) {
45
+ if (err.isRetryable)
46
+ return true;
47
+ if (isRetryableStatus(err.statusCode))
48
+ return true;
49
+ }
50
+ if (RetryError.isInstance(err))
51
+ return true;
52
+ if (!(err instanceof Error))
53
+ return false;
54
+ const message = err.message.toLowerCase();
55
+ return TRANSIENT_MESSAGE_FRAGMENTS.some((fragment) => message.includes(fragment));
56
+ }
57
+ /**
58
+ * Whether an error is one that **no** further attempt can clear, and the reason.
59
+ *
60
+ * This is the third classification, and the one the other two cannot express.
61
+ * {@link isTransientAiError} splits failures into "retry the step" (`true`) and
62
+ * "burn this slot, try the fallback" (`false`) — and for a rejected credential
63
+ * *both* are wrong. Retrying spends the Workflow's budget on a request that can
64
+ * never succeed; falling back spends the second slot presenting the *same* dead
65
+ * token. Returning `false` from the transient check only avoids the first.
66
+ *
67
+ * So the attempt ladders check this **before** entering the fallback slot and
68
+ * stop there, and `runHandleTask` ends the Task with copy the host supplies.
69
+ * Nothing is retried and nothing is spent proving the obvious twice.
70
+ *
71
+ * Keyed on {@link file://./errors.ts CredentialRejectedError}, which is neutral
72
+ * and structurally matched — so a provider outside core raises one and gets this
73
+ * handling with nothing here to change.
74
+ */
75
+ export function nonRecoverableKind(err) {
76
+ if (!CredentialRejectedError.isInstance(err))
77
+ return undefined;
78
+ switch (err.source) {
79
+ case "provider":
80
+ return "credential";
81
+ case "gateway":
82
+ return "gateway-credential";
83
+ // Includes an error that crossed a realm boundary carrying no `source` at
84
+ // all: `isInstance` is structural, so that is reachable, and "unknown" is
85
+ // the honest reading of it.
86
+ default:
87
+ return "unknown-credential";
88
+ }
89
+ }
90
+ /** A step is "intermediate" when it makes tool calls — more content follows. */
91
+ function isIntermediateStep(step) {
92
+ return step.finishReason === "tool-calls";
93
+ }
94
+ /**
95
+ * Returns a fresh `onStepEnd` callback for one `generateText` attempt.
96
+ * Fires `onContent` for each intermediate step (text that accompanies tool
97
+ * calls); the final step is skipped because its text is the operation's return
98
+ * value. A fresh handler per attempt resets the 0-based `stepIndex` counter so a
99
+ * primary→fallback re-run reuses the same indices and the gatekeeper dedupes.
100
+ *
101
+ * `terminalToolNames` are the loop's **halting** control tools (e.g. the main
102
+ * agent's `delegate`): a step that calls one still has `finishReason:"tool-calls"`,
103
+ * but it is the round's *final* step, and its accompanying text is the round's
104
+ * acknowledgment — which the caller publishes separately as a milestone. Streaming
105
+ * it here too would double-post the same text under a second messageId, so those
106
+ * steps are skipped. Default `[]` (the subagent loop has no control tools).
107
+ */
108
+ export function buildIntermediateContentHandler(onContent, terminalToolNames = []) {
109
+ let stepIndex = 0;
110
+ return async (step) => {
111
+ const i = stepIndex++;
112
+ if (!isIntermediateStep(step))
113
+ return;
114
+ if (step.toolCalls.some((c) => terminalToolNames.includes(c.toolName)))
115
+ return;
116
+ const content = step.text.trim();
117
+ if (content)
118
+ await onContent(content, i);
119
+ };
120
+ }
@@ -0,0 +1,90 @@
1
+ import type { LanguageModel } from "ai";
2
+ import type { ModelConfig } from "../config.js";
3
+ /**
4
+ * The provider contract every loop runs against — and nothing that implements
5
+ * it.
6
+ *
7
+ * {@link ModelRuntime} is the whole seam: an agent that returns one runs every
8
+ * loop in core unchanged, because nothing downstream — the round loop, the
9
+ * control-tool repair ladder, the Session's compaction, the Workflow — ever sees
10
+ * more than a `LanguageModel` from `ai`. It never learns which provider produced
11
+ * it.
12
+ *
13
+ * Core ships exactly one implementation —
14
+ * {@link file://./workers-ai/index.ts `./workers-ai`} — as a sibling directory
15
+ * under `agent/`. A second provider is a second directory exporting one
16
+ * {@link ModelRuntimeFactory}, and nothing here has to change to admit it;
17
+ * a consumer can equally supply one from outside the package, which is the
18
+ * point of the seam.
19
+ *
20
+ * There was a `./anthropic` sibling until 0.8.0 — a hand-written Messages API
21
+ * adapter behind an optional peer dependency. It was removed with the only
22
+ * deployment that used it. Nothing about this contract changed when it went,
23
+ * which is the strongest thing that can be said for the contract.
24
+ *
25
+ * Which is why this file has no runtime imports at all. The Workers AI factory
26
+ * used to live in it, and a contract that ships one implementation inline reads
27
+ * as *the* runtime with an escape hatch, rather than as one of N.
28
+ */
29
+ /**
30
+ * Custom metadata attached to the AI Gateway log for every call a pair makes.
31
+ * AI Gateway's own `metadata` is otherwise `null`, so a model call can only be
32
+ * tied back to its task by timestamp; stamping `{ taskId, round }` (a turn) or
33
+ * `{ taskId, subtaskId }` (a chunk) makes correlation exact. Values are limited
34
+ * to AI Gateway's accepted scalar set.
35
+ */
36
+ export type AiGatewayMetadata = Record<string, number | string | boolean>;
37
+ export interface ModelOverrides {
38
+ /** Test override for the primary slot. */
39
+ model?: LanguageModel;
40
+ /** Test override for the fallback slot. */
41
+ fallbackModel?: LanguageModel;
42
+ /**
43
+ * The provider's model id for the primary slot. Defaults to the configured
44
+ * `chatModelId`.
45
+ *
46
+ * The subagent path passes `ValidatedRecipe.primaryModelId`, which is the
47
+ * host's own configured id — `validateRecipe` copies the pair on, and a recipe
48
+ * has no field to name a model with. So this parameterizes the pair without
49
+ * ever widening which models are reachable.
50
+ */
51
+ primaryModelId?: string;
52
+ /** The provider's model id for the fallback slot. See {@link primaryModelId}. */
53
+ fallbackModelId?: string;
54
+ /** AI Gateway log metadata for correlation — see {@link AiGatewayMetadata}. */
55
+ metadata?: AiGatewayMetadata;
56
+ }
57
+ /** The primary/fallback models (lazily memoized) plus their ids for logging. */
58
+ export interface ModelPair {
59
+ primary: () => LanguageModel;
60
+ fallback: () => LanguageModel;
61
+ primaryId: () => string;
62
+ fallbackId: () => string;
63
+ }
64
+ export interface ModelRuntime {
65
+ /**
66
+ * Lazily build + memoize a primary/fallback model pair (overridable in tests,
67
+ * id-parameterized so a subagent can run the pair its validated recipe
68
+ * carries). Nothing is checked here — the ids reaching this can only be the
69
+ * host's own, which `resolveConfig` has already proven non-empty and distinct.
70
+ */
71
+ createModelPair(overrides?: ModelOverrides): ModelPair;
72
+ }
73
+ /**
74
+ * How a provider is supplied to an agent: given the Worker env and the agent's
75
+ * *resolved* model config, return a runtime.
76
+ *
77
+ * Both base-class seams — `DynamicAgent.modelRuntime` and
78
+ * `RecipeSubagentHost.modelRuntime` — take this shape, which is the point of it.
79
+ * A provider written as one of these is defined once and referenced from the
80
+ * agent and its subagent facet, instead of being spelled out twice in two class
81
+ * bodies that nothing keeps in step. See
82
+ * {@link file://./workers-ai/runtime.ts workersAIModels} for core's own.
83
+ *
84
+ * Config arrives as an argument rather than being read off `this`: the facet
85
+ * resolves its config inside `buildRuntime` and calls the seam from there, so
86
+ * there is no `this.config` to read at that point — and a factory that cannot
87
+ * reach for one cannot disagree with its caller about which AI Gateway the agent
88
+ * is on.
89
+ */
90
+ export type ModelRuntimeFactory<TEnv> = (env: TEnv, config: ModelConfig) => ModelRuntime;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import type { LanguageModel, ToolSet } from "ai";
2
+ import { Session } from "agents/experimental/memory/session";
3
+ import type { SessionMessage } from "agents/experimental/memory/session";
4
+ import { createCompactFunction } from "agents/experimental/memory/utils";
5
+ /**
6
+ * The one continuous {@link Session} an agent Durable Object owns: soul + memory
7
+ * + compaction, one Session per DO.
8
+ *
9
+ * Compaction is the one **lossy** thing this module does, so it is also the one
10
+ * thing it announces: `onMessagesDisplaced` hands over the raw messages a
11
+ * summary is about to replace. Core neither stores them nor knows who wants
12
+ * them — a host wires the seam to whatever does.
13
+ */
14
+ /**
15
+ * The SQLite-backed host the Sessions API needs — satisfied by the Agents SDK
16
+ * `Agent` (`this.sql`).
17
+ */
18
+ export interface SessionHost {
19
+ sql<T = Record<string, string | number | boolean | null>>(strings: TemplateStringsArray, ...values: (string | number | boolean | null)[]): T[];
20
+ }
21
+ /** The subset of `Session` the agent loop drives — lets tests inject a fake. */
22
+ export interface SessionLike {
23
+ appendMessage(message: SessionMessage, parentId?: string | null): Promise<unknown> | unknown;
24
+ getHistory(): Promise<SessionMessage[]>;
25
+ /**
26
+ * Read one message by id, or null. Reads the **raw stored row**, so it is
27
+ * unaffected by compaction overlays — a message folded into a summary is still
28
+ * readable here. That is what makes {@link appendOnce}'s read-back a reliable
29
+ * recovery path for a round whose Workflow step re-ran.
30
+ */
31
+ getMessage(id: string): Promise<SessionMessage | null>;
32
+ refreshSystemPrompt(): Promise<string>;
33
+ tools(): Promise<ToolSet>;
34
+ /** Compaction overlays so far — non-empty ⇒ history has been displaced. */
35
+ getCompactions(): Promise<unknown[]>;
36
+ }
37
+ /**
38
+ * Append a message with a deterministic id exactly once, and return the text that
39
+ * is **durably stored** under that id.
40
+ *
41
+ * `Session.appendMessage` is already idempotent by id: appending an id that
42
+ * exists is a no-op. The read-back is what matters for a re-run step — if it
43
+ * crashed after appending and the retry re-inferred a *different* reply, the
44
+ * append no-ops and this returns the original, durable text. The Session and the
45
+ * value the caller goes on to deliver therefore never disagree.
46
+ *
47
+ * Falls back to the message's own text if the read-back returns null (it cannot,
48
+ * having just been appended) rather than failing a round over a missing echo.
49
+ */
50
+ export declare function appendOnce(session: SessionLike, message: SessionMessage): Promise<string>;
51
+ export interface AgentSessionOptions {
52
+ /** Read-only identity block injected into the system prompt every turn. */
53
+ soul: () => string | Promise<string>;
54
+ /** Description of the writable SQLite `"memory"` scratchpad the model self-edits. */
55
+ memoryDescription: string;
56
+ /** Soft cap (tokens) for the `"memory"` block. */
57
+ memoryMaxTokens: number;
58
+ /** History token threshold that triggers compaction. */
59
+ compactAfterTokens: number;
60
+ /**
61
+ * Tokens of recent history compaction keeps verbatim. Coupled to
62
+ * {@link compactAfterTokens} — see `SessionConfig.compactTailTokens` for the
63
+ * invariant that binds them, which `resolveConfig` enforces.
64
+ */
65
+ compactTailTokens: number;
66
+ /**
67
+ * Output-token ceiling for the summarizer call. An unbounded summary is not
68
+ * the risk; a silently truncated one is — it becomes this caller's memory of
69
+ * everything that scrolled out, with no way to tell it was cut short.
70
+ */
71
+ maxOutputTokens: number;
72
+ /**
73
+ * Hand over the raw messages each compaction displaces, before a summary
74
+ * replaces them. Best-effort: a throw here must never abort compaction.
75
+ *
76
+ * The seam, not a policy — pass `runtime.onMessagesDisplaced` to reach every
77
+ * installed plugin declaring the hook, or any function of your own.
78
+ */
79
+ onMessagesDisplaced?: (messages: SessionMessage[]) => Promise<void>;
80
+ }
81
+ type CompactFn = ReturnType<typeof createCompactFunction>;
82
+ /**
83
+ * Wrap a compaction function so the raw messages it folds into a summary are
84
+ * also handed to `onMessagesDisplaced` before they stop being readable as
85
+ * history. The displaced range is `fromMessageId..toMessageId` of the result,
86
+ * sliced from the `history` the compaction saw.
87
+ *
88
+ * A listener's failure is swallowed — compaction must still shorten history
89
+ * when whatever is listening is briefly unavailable. The alternative is
90
+ * unbounded context because a side concern is down.
91
+ */
92
+ export declare function notifyingCompaction(base: CompactFn, onMessagesDisplaced?: (messages: SessionMessage[]) => Promise<void>): CompactFn;
93
+ /**
94
+ * Build the one continuous `Session` an agent Durable Object owns: a read-only
95
+ * `"soul"` identity block + a writable `"memory"` scratchpad, with history
96
+ * compaction summarized by the same model. All of a caller's turns (any channel
97
+ * or thread) accumulate into this single conversation.
98
+ */
99
+ export declare function buildAgentSession(agent: SessionHost, model: LanguageModel, opts: AgentSessionOptions): Session;
100
+ export {};