@loopingai/core 0.5.1 → 0.7.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 (99) hide show
  1. package/README.md +6 -4
  2. package/dist/a2a/caller-token.d.ts +38 -0
  3. package/dist/a2a/caller-token.js +61 -0
  4. package/dist/a2a/deliver.d.ts +49 -0
  5. package/dist/a2a/deliver.js +49 -0
  6. package/dist/a2a/index.d.ts +2 -0
  7. package/dist/a2a/index.js +2 -0
  8. package/dist/a2a/notify.d.ts +4 -3
  9. package/dist/a2a/notify.js +4 -3
  10. package/dist/agent/anthropic/index.d.ts +15 -0
  11. package/dist/agent/anthropic/index.js +19 -0
  12. package/dist/agent/anthropic/language-model.d.ts +59 -0
  13. package/dist/agent/anthropic/language-model.js +442 -0
  14. package/dist/agent/anthropic/prompt.d.ts +84 -0
  15. package/dist/agent/anthropic/prompt.js +541 -0
  16. package/dist/agent/anthropic/runtime.d.ts +79 -0
  17. package/dist/agent/anthropic/runtime.js +130 -0
  18. package/dist/agent/control.js +10 -9
  19. package/dist/agent/errors.d.ts +85 -0
  20. package/dist/agent/errors.js +64 -0
  21. package/dist/agent/final-reply.d.ts +14 -13
  22. package/dist/agent/final-reply.js +28 -11
  23. package/dist/agent/history.d.ts +3 -3
  24. package/dist/agent/history.js +2 -2
  25. package/dist/agent/index.d.ts +4 -2
  26. package/dist/agent/index.js +4 -2
  27. package/dist/agent/inference.d.ts +58 -1
  28. package/dist/agent/inference.js +44 -0
  29. package/dist/agent/model.d.ts +42 -25
  30. package/dist/agent/model.js +1 -48
  31. package/dist/agent/session.d.ts +6 -7
  32. package/dist/agent/session.js +3 -3
  33. package/dist/agent/workers-ai/index.d.ts +23 -0
  34. package/dist/agent/workers-ai/index.js +23 -0
  35. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  36. package/dist/agent/workers-ai/runtime.js +63 -0
  37. package/dist/alarm/index.d.ts +77 -0
  38. package/dist/alarm/index.js +116 -0
  39. package/dist/config.d.ts +49 -15
  40. package/dist/config.js +30 -1
  41. package/dist/contract/plugin.d.ts +63 -3
  42. package/dist/contract/plugin.js +76 -0
  43. package/dist/contract/recipe.d.ts +16 -17
  44. package/dist/db/db.d.ts +0 -1
  45. package/dist/db/migrations/index.js +8 -1
  46. package/dist/db/models/subtasks.d.ts +24 -25
  47. package/dist/db/models/subtasks.js +33 -76
  48. package/dist/db/schema.d.ts +2 -21
  49. package/dist/db/schema.js +2 -4
  50. package/dist/host/agent.d.ts +58 -4
  51. package/dist/host/agent.js +63 -9
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +2 -2
  54. package/dist/platform.d.ts +103 -11
  55. package/dist/platform.js +107 -13
  56. package/dist/round/agent.d.ts +36 -31
  57. package/dist/round/agent.js +61 -89
  58. package/dist/round/index.d.ts +3 -2
  59. package/dist/round/index.js +2 -2
  60. package/dist/round/policy.d.ts +2 -2
  61. package/dist/round/subagent.d.ts +19 -1
  62. package/dist/round/subagent.js +22 -5
  63. package/dist/round/turn.d.ts +32 -13
  64. package/dist/round/turn.js +83 -16
  65. package/dist/round/workflow.d.ts +23 -7
  66. package/dist/round/workflow.js +122 -110
  67. package/dist/runtime/index.d.ts +4 -2
  68. package/dist/runtime/index.js +6 -0
  69. package/dist/subagent/fingerprint.d.ts +2 -2
  70. package/dist/subagent/fingerprint.js +8 -17
  71. package/dist/subagent/index.d.ts +6 -4
  72. package/dist/subagent/index.js +8 -6
  73. package/dist/subagent/prompt.d.ts +4 -5
  74. package/dist/subagent/prompt.js +0 -8
  75. package/dist/subagent/run.d.ts +8 -1
  76. package/dist/subagent/run.js +59 -9
  77. package/dist/subtasks/catalog.d.ts +1 -1
  78. package/dist/subtasks/catalog.js +1 -1
  79. package/dist/subtasks/decomposition.d.ts +16 -20
  80. package/dist/subtasks/decomposition.js +27 -75
  81. package/dist/subtasks/delegate.d.ts +20 -1
  82. package/dist/subtasks/delegate.js +21 -16
  83. package/dist/subtasks/index.d.ts +1 -2
  84. package/dist/subtasks/index.js +1 -2
  85. package/dist/subtasks/subtask-types.d.ts +0 -8
  86. package/dist/subtasks/subtask-types.js +0 -7
  87. package/dist/subtasks/types.d.ts +45 -70
  88. package/dist/testing/do.d.ts +4 -4
  89. package/dist/testing/index.d.ts +1 -1
  90. package/dist/testing/index.js +5 -1
  91. package/dist/testing/mock-model.d.ts +35 -0
  92. package/dist/testing/mock-model.js +75 -0
  93. package/dist/testing/vcr-global-setup.d.ts +1 -3
  94. package/dist/testing/vcr-global-setup.js +1 -3
  95. package/dist/worker/index.d.ts +5 -12
  96. package/dist/worker/index.js +5 -12
  97. package/package.json +23 -1
  98. package/dist/subtasks/scheduler.d.ts +0 -48
  99. package/dist/subtasks/scheduler.js +0 -47
@@ -1,12 +1,24 @@
1
1
  import type { LanguageModel } from "ai";
2
2
  import type { ModelConfig } from "../config.js";
3
3
  /**
4
- * The Workers-AI model pair every loop runs on, built per agent instance.
4
+ * The provider contract every loop runs against and nothing that implements
5
+ * it.
5
6
  *
6
- * The predecessor read `env.AI` and five config constants as module-level
7
- * imports. Neither survives packaging: `env` does not exist at module scope on
8
- * Workers, and a module constant cannot be overridden by a consumer. So this is
9
- * a factory over an injected binding and an injected {@link ModelConfig}.
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
+ * The implementations are siblings under `agent/`, one directory each:
14
+ * {@link file://./workers-ai/index.ts `./workers-ai`} is core's default, and
15
+ * {@link file://./anthropic/index.ts `./anthropic`} is Claude behind an optional
16
+ * peer dependency. A third provider is a third directory exporting one
17
+ * {@link ModelRuntimeFactory}; nothing here has to change to admit it.
18
+ *
19
+ * Which is why this file has no runtime imports at all. The Workers AI factory
20
+ * used to live in it, and a contract that ships one implementation inline reads
21
+ * as *the* runtime with an escape hatch, rather than as one of N.
10
22
  */
11
23
  /**
12
24
  * Custom metadata attached to the AI Gateway log for every call a pair makes.
@@ -22,15 +34,16 @@ export interface ModelOverrides {
22
34
  /** Test override for the fallback slot. */
23
35
  fallbackModel?: LanguageModel;
24
36
  /**
25
- * Workers-AI id for the primary slot (a recipe's, already code-validated
26
- * against the allowlist by `validateRecipe`). Defaults to the configured
37
+ * The provider's model id for the primary slot. Defaults to the configured
27
38
  * `chatModelId`.
39
+ *
40
+ * The subagent path passes `ValidatedRecipe.primaryModelId`, which is the
41
+ * host's own configured id — `validateRecipe` copies the pair on, and a recipe
42
+ * has no field to name a model with. So this parameterizes the pair without
43
+ * ever widening which models are reachable.
28
44
  */
29
45
  primaryModelId?: string;
30
- /**
31
- * Workers-AI id for the fallback slot (already code-validated). Defaults to
32
- * the configured `fallbackChatModelId`.
33
- */
46
+ /** The provider's model id for the fallback slot. See {@link primaryModelId}. */
34
47
  fallbackModelId?: string;
35
48
  /** AI Gateway log metadata for correlation — see {@link GatewayMetadata}. */
36
49
  metadata?: GatewayMetadata;
@@ -45,23 +58,27 @@ export interface ModelPair {
45
58
  export interface ModelRuntime {
46
59
  /**
47
60
  * Lazily build + memoize a primary/fallback model pair (overridable in tests,
48
- * id-parameterized for recipes). No allowlisting happens here
49
- * `validateRecipe` is the single validation owner for recipe-supplied ids.
61
+ * id-parameterized so a subagent can run the pair its validated recipe
62
+ * carries). Nothing is checked here — the ids reaching this can only be the
63
+ * host's own, which `resolveConfig` has already proven non-empty and distinct.
50
64
  */
51
65
  createModelPair(overrides?: ModelOverrides): ModelPair;
52
66
  }
53
- export interface ModelRuntimeDeps {
54
- /** The `AI` binding. Read lazily — see {@link createModelRuntime}. */
55
- ai: Ai;
56
- config: ModelConfig;
57
- }
58
67
  /**
59
- * Build the model runtime for one agent instance.
68
+ * How a provider is supplied to an agent: given the Worker env and the agent's
69
+ * *resolved* model config, return a runtime.
70
+ *
71
+ * Both base-class seams — `LoopingAgent.modelRuntime` and
72
+ * `RecipeSubagentHost.modelRuntime` — take this shape, which is the point of it.
73
+ * A provider written as one of these is defined once and referenced from the
74
+ * agent and its subagent facet, instead of being spelled out twice in two class
75
+ * bodies that nothing keeps in step. See
76
+ * {@link file://./workers-ai/runtime.ts workersAIModels} for core's own.
60
77
  *
61
- * The provider is constructed on first *use*, not here. During `wrangler deploy`
62
- * Cloudflare evaluates module scope to validate the new version, and bindings
63
- * are not populated at that point — constructing eagerly makes `createWorkersAI`
64
- * throw "you must provide either a binding or credentials". The same laziness
65
- * protects a consumer who builds their runtime early.
78
+ * Config arrives as an argument rather than being read off `this`: the facet
79
+ * resolves its config inside `buildRuntime` and calls the seam from there, so
80
+ * there is no `this.config` to read at that point — and a factory that cannot
81
+ * reach for one cannot disagree with its caller about which gateway the agent
82
+ * is on.
66
83
  */
67
- export declare function createModelRuntime(deps: ModelRuntimeDeps): ModelRuntime;
84
+ export type ModelRuntimeFactory<TEnv> = (env: TEnv, config: ModelConfig) => ModelRuntime;
@@ -1,48 +1 @@
1
- import { createWorkersAI } from "workers-ai-provider";
2
- /**
3
- * Build the model runtime for one agent instance.
4
- *
5
- * The provider is constructed on first *use*, not here. During `wrangler deploy`
6
- * Cloudflare evaluates module scope to validate the new version, and bindings
7
- * are not populated at that point — constructing eagerly makes `createWorkersAI`
8
- * throw "you must provide either a binding or credentials". The same laziness
9
- * protects a consumer who builds their runtime early.
10
- */
11
- export function createModelRuntime(deps) {
12
- const { config } = deps;
13
- let provider;
14
- const workersai = () => (provider ??= createWorkersAI({
15
- binding: deps.ai,
16
- gateway: { id: config.aiGatewayId }
17
- }));
18
- /**
19
- * Per-model Workers-AI settings: pin the gateway id (so per-call metadata does
20
- * not drop the gateway route), attach correlation metadata when supplied, and
21
- * set the reasoning budget.
22
- *
23
- * Always returns a settings object, even with no metadata: `reasoning_effort`
24
- * has to reach the binding on every call, and an `undefined` return drops it.
25
- */
26
- const chatSettings = (metadata) => ({
27
- gateway: { id: config.aiGatewayId, ...(metadata ? { metadata } : {}) },
28
- reasoning_effort: config.reasoningEffort
29
- });
30
- return {
31
- createModelPair(overrides = {}) {
32
- const primaryId = overrides.primaryModelId ?? config.chatModelId;
33
- const fallbackId = overrides.fallbackModelId ?? config.fallbackChatModelId;
34
- let primary;
35
- let fallback;
36
- const settings = chatSettings(overrides.metadata);
37
- return {
38
- primary: () => (primary ??= overrides.model ?? workersai()(primaryId, settings)),
39
- fallback: () => (fallback ??=
40
- overrides.fallbackModel ??
41
- overrides.model ??
42
- workersai()(fallbackId, settings)),
43
- primaryId: () => primaryId,
44
- fallbackId: () => fallbackId
45
- };
46
- }
47
- };
48
- }
1
+ export {};
@@ -3,9 +3,8 @@ import { Session } from "agents/experimental/memory/session";
3
3
  import type { SessionMessage } from "agents/experimental/memory/session";
4
4
  import { createCompactFunction } from "agents/experimental/memory/utils";
5
5
  /**
6
- * The one continuous {@link Session} an agent Durable Object owns. Ported from
7
- * the looping-gateway admin agent's `shared/session.ts` (single Session per DO
8
- * soul + memory + compaction).
6
+ * The one continuous {@link Session} an agent Durable Object owns: soul + memory
7
+ * + compaction, one Session per DO.
9
8
  *
10
9
  * Compaction is the one **lossy** thing this module does, so it is also the one
11
10
  * thing it announces: `onMessagesDisplaced` hands over the raw messages a
@@ -27,7 +26,7 @@ export interface SessionLike {
27
26
  * Read one message by id, or null. Reads the **raw stored row**, so it is
28
27
  * unaffected by compaction overlays — a message folded into a summary is still
29
28
  * readable here. That is what makes {@link appendOnce}'s read-back a reliable
30
- * recovery path for a phase whose Workflow step re-ran.
29
+ * recovery path for a round whose Workflow step re-ran.
31
30
  */
32
31
  getMessage(id: string): Promise<SessionMessage | null>;
33
32
  refreshSystemPrompt(): Promise<string>;
@@ -40,13 +39,13 @@ export interface SessionLike {
40
39
  * is **durably stored** under that id.
41
40
  *
42
41
  * `Session.appendMessage` is already idempotent by id: appending an id that
43
- * exists is a no-op. The read-back is what matters for a re-run phase — if the
44
- * step crashed after appending and the retry re-inferred a *different* reply, the
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
45
44
  * append no-ops and this returns the original, durable text. The Session and the
46
45
  * value the caller goes on to deliver therefore never disagree.
47
46
  *
48
47
  * Falls back to the message's own text if the read-back returns null (it cannot,
49
- * having just been appended) rather than failing a phase over a missing echo.
48
+ * having just been appended) rather than failing a round over a missing echo.
50
49
  */
51
50
  export declare function appendOnce(session: SessionLike, message: SessionMessage): Promise<string>;
52
51
  export interface AgentSessionOptions {
@@ -7,13 +7,13 @@ import { sessionText } from "./history.js";
7
7
  * is **durably stored** under that id.
8
8
  *
9
9
  * `Session.appendMessage` is already idempotent by id: appending an id that
10
- * exists is a no-op. The read-back is what matters for a re-run phase — if the
11
- * step crashed after appending and the retry re-inferred a *different* reply, the
10
+ * exists is a no-op. The read-back is what matters for a re-run step — if it
11
+ * crashed after appending and the retry re-inferred a *different* reply, the
12
12
  * append no-ops and this returns the original, durable text. The Session and the
13
13
  * value the caller goes on to deliver therefore never disagree.
14
14
  *
15
15
  * Falls back to the message's own text if the read-back returns null (it cannot,
16
- * having just been appended) rather than failing a phase over a missing echo.
16
+ * having just been appended) rather than failing a round over a missing echo.
17
17
  */
18
18
  export async function appendOnce(session, message) {
19
19
  await session.appendMessage(message);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Workers AI — core's default model provider.
3
+ *
4
+ * A sibling of {@link file://../anthropic/index.ts `./anthropic`}, and like it a
5
+ * provider rather than a capability: it ships no tools, no prompt copy and no
6
+ * policy; it satisfies {@link file://../model.ts ModelRuntime} and stops.
7
+ *
8
+ * It differs from that sibling in two ways, both deliberate:
9
+ *
10
+ * **No `LanguageModel` adapter.** `workers-ai-provider` ships one, so this
11
+ * directory is a factory and nothing else. The Anthropic side hand-builds the
12
+ * adapter — the prompt mapping, the cache control, the error taxonomy — which is
13
+ * why it is five files and this is one.
14
+ *
15
+ * **No package subpath.** `/anthropic` has one because `@anthropic-ai/sdk` is an
16
+ * *optional* peer and an agent on Workers AI must not pay for a provider it
17
+ * never calls. That argument does not apply here: `workers-ai-provider` is a
18
+ * required peer, and this is the default every un-overridden seam reaches
19
+ * through `LoopingAgent` itself, so it is in every consumer's module graph
20
+ * already. It is exported from `@loopingai/core/agent` — one symbol, one import
21
+ * path.
22
+ */
23
+ export { createWorkersAIModelRuntime, workersAIModels, type WorkersAIRuntimeDeps } from "./runtime.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Workers AI — core's default model provider.
3
+ *
4
+ * A sibling of {@link file://../anthropic/index.ts `./anthropic`}, and like it a
5
+ * provider rather than a capability: it ships no tools, no prompt copy and no
6
+ * policy; it satisfies {@link file://../model.ts ModelRuntime} and stops.
7
+ *
8
+ * It differs from that sibling in two ways, both deliberate:
9
+ *
10
+ * **No `LanguageModel` adapter.** `workers-ai-provider` ships one, so this
11
+ * directory is a factory and nothing else. The Anthropic side hand-builds the
12
+ * adapter — the prompt mapping, the cache control, the error taxonomy — which is
13
+ * why it is five files and this is one.
14
+ *
15
+ * **No package subpath.** `/anthropic` has one because `@anthropic-ai/sdk` is an
16
+ * *optional* peer and an agent on Workers AI must not pay for a provider it
17
+ * never calls. That argument does not apply here: `workers-ai-provider` is a
18
+ * required peer, and this is the default every un-overridden seam reaches
19
+ * through `LoopingAgent` itself, so it is in every consumer's module graph
20
+ * already. It is exported from `@loopingai/core/agent` — one symbol, one import
21
+ * path.
22
+ */
23
+ export { createWorkersAIModelRuntime, workersAIModels } from "./runtime.js";
@@ -0,0 +1,42 @@
1
+ import type { ModelConfig } from "../../config.js";
2
+ import type { AiEnv } from "../../env.js";
3
+ import type { ModelRuntime, ModelRuntimeFactory } from "../model.js";
4
+ /**
5
+ * The Workers-AI model pair every loop runs on by default, built per agent
6
+ * instance.
7
+ *
8
+ * The predecessor read `env.AI` and five config constants as module-level
9
+ * imports. Neither survives packaging: `env` does not exist at module scope on
10
+ * Workers, and a module constant cannot be overridden by a consumer. So this is
11
+ * a factory over an injected binding and an injected {@link ModelConfig}.
12
+ */
13
+ export interface WorkersAIRuntimeDeps {
14
+ /** The `AI` binding. Read lazily — see {@link createWorkersAIModelRuntime}. */
15
+ ai: Ai;
16
+ config: ModelConfig;
17
+ }
18
+ /**
19
+ * Build the Workers AI model runtime for one agent instance.
20
+ *
21
+ * The provider is constructed on first *use*, not here. During `wrangler deploy`
22
+ * Cloudflare evaluates module scope to validate the new version, and bindings
23
+ * are not populated at that point — constructing eagerly makes `createWorkersAI`
24
+ * throw "you must provide either a binding or credentials". The same laziness
25
+ * protects a consumer who builds their runtime early.
26
+ */
27
+ export declare function createWorkersAIModelRuntime(deps: WorkersAIRuntimeDeps): ModelRuntime;
28
+ /**
29
+ * Core's default provider — what every {@link ModelRuntimeFactory} seam lands on
30
+ * when an agent does not override it.
31
+ *
32
+ * One definition rather than one per base class. The body used to be written
33
+ * twice, once in {@link file://../../host/agent.ts LoopingAgent.modelRuntime}
34
+ * and once in
35
+ * {@link file://../../round/subagent.ts RecipeSubagentHost.modelRuntime}, which
36
+ * is the same duplication the seam exists to let a *consumer* avoid.
37
+ *
38
+ * Typed on {@link AiEnv} rather than a caller's full `Env`: a factory that
39
+ * accepts the narrow shape is callable with any env that satisfies it, so both
40
+ * base classes pass `this.env` straight through.
41
+ */
42
+ export declare const workersAIModels: ModelRuntimeFactory<AiEnv>;
@@ -0,0 +1,63 @@
1
+ import { createWorkersAI } from "workers-ai-provider";
2
+ /**
3
+ * Build the Workers AI model runtime for one agent instance.
4
+ *
5
+ * The provider is constructed on first *use*, not here. During `wrangler deploy`
6
+ * Cloudflare evaluates module scope to validate the new version, and bindings
7
+ * are not populated at that point — constructing eagerly makes `createWorkersAI`
8
+ * throw "you must provide either a binding or credentials". The same laziness
9
+ * protects a consumer who builds their runtime early.
10
+ */
11
+ export function createWorkersAIModelRuntime(deps) {
12
+ const { config } = deps;
13
+ let provider;
14
+ const workersai = () => (provider ??= createWorkersAI({
15
+ binding: deps.ai,
16
+ gateway: { id: config.aiGatewayId }
17
+ }));
18
+ /**
19
+ * Per-model Workers-AI settings: pin the gateway id (so per-call metadata does
20
+ * not drop the gateway route), attach correlation metadata when supplied, and
21
+ * set the reasoning budget.
22
+ *
23
+ * Always returns a settings object, even with no metadata: `reasoning_effort`
24
+ * has to reach the binding on every call, and an `undefined` return drops it.
25
+ */
26
+ const chatSettings = (metadata) => ({
27
+ gateway: { id: config.aiGatewayId, ...(metadata ? { metadata } : {}) },
28
+ reasoning_effort: config.reasoningEffort
29
+ });
30
+ return {
31
+ createModelPair(overrides = {}) {
32
+ const primaryId = overrides.primaryModelId ?? config.chatModelId;
33
+ const fallbackId = overrides.fallbackModelId ?? config.fallbackChatModelId;
34
+ let primary;
35
+ let fallback;
36
+ const settings = chatSettings(overrides.metadata);
37
+ return {
38
+ primary: () => (primary ??= overrides.model ?? workersai()(primaryId, settings)),
39
+ fallback: () => (fallback ??=
40
+ overrides.fallbackModel ??
41
+ overrides.model ??
42
+ workersai()(fallbackId, settings)),
43
+ primaryId: () => primaryId,
44
+ fallbackId: () => fallbackId
45
+ };
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Core's default provider — what every {@link ModelRuntimeFactory} seam lands on
51
+ * when an agent does not override it.
52
+ *
53
+ * One definition rather than one per base class. The body used to be written
54
+ * twice, once in {@link file://../../host/agent.ts LoopingAgent.modelRuntime}
55
+ * and once in
56
+ * {@link file://../../round/subagent.ts RecipeSubagentHost.modelRuntime}, which
57
+ * is the same duplication the seam exists to let a *consumer* avoid.
58
+ *
59
+ * Typed on {@link AiEnv} rather than a caller's full `Env`: a factory that
60
+ * accepts the narrow shape is callable with any env that satisfies it, so both
61
+ * base classes pass `this.env` straight through.
62
+ */
63
+ export const workersAIModels = (env, config) => createWorkersAIModelRuntime({ ai: env.AI, config });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `@loopingai/core/alarm` — many deadlines over a Durable Object's one alarm.
3
+ *
4
+ * A Durable Object has exactly **one** alarm, and an object that needs to wake
5
+ * for more than one reason cannot simply call `setAlarm` from each of them: the
6
+ * last writer silently wins, and whatever the loser was waiting on never
7
+ * happens. {@link WakeMap} is the fix — one storage row holding every pending
8
+ * intent, and the only thing in a DO that calls `setAlarm`.
9
+ *
10
+ * **Its own subpath, deliberately.** This is useful to a plain `DurableObject`,
11
+ * not only to a {@link LoopingAgent}, so it must be importable without pulling
12
+ * the agent machinery into a bundle.
13
+ *
14
+ * **Why not `Agent.schedule()`.** The `agents` SDK has the same mechanism, but
15
+ * sells it only as a method on `Agent`: adopting it means the object becomes an
16
+ * `Agent`, whose constructor creates `cf_agents_state`, `cf_agents_mcp_servers`
17
+ * and `cf_agents_queues` in that object's SQLite, builds an `MCPClientManager`,
18
+ * and prototype-patches every public method for tracing. For an object whose
19
+ * SQLite is something else already — a container's filesystem, say — that is a
20
+ * large import for a small one. `agents/schedule` is not an alternative: it is a
21
+ * prompt and a zod schema for parsing natural-language dates, not alarm
22
+ * machinery.
23
+ *
24
+ * This owns *when* an object wakes. What it owes on waking is the object's own:
25
+ * `alarm()` reads {@link WakeMap.due} and dispatches.
26
+ */
27
+ /** One scheduled wake-up. */
28
+ export interface WakeIntent {
29
+ /** Why we are waking. Namespace it, e.g. `sync-retry:container-shell`. */
30
+ key: string;
31
+ /** Epoch ms at which this intent becomes due. */
32
+ notBefore: number;
33
+ /** Retry counter, for the intents that carry one. */
34
+ attempt?: number;
35
+ }
36
+ /** The single storage row holding every intent. Small, and written atomically. */
37
+ export declare const WAKE_KEY = "wake";
38
+ /** How far out {@link WakeMap.repair} re-arms when the handler itself failed. */
39
+ export declare const WAKE_REPAIR_MS = 60000;
40
+ export declare class WakeMap {
41
+ #private;
42
+ constructor(storage: DurableObjectStorage);
43
+ /**
44
+ * Every pending intent, as a **null-prototype** dictionary rebuilt from own
45
+ * entries only.
46
+ *
47
+ * {@link WakeIntent.key} is a caller-supplied string, so an ordinary object
48
+ * literal would let three of them misbehave: `get("toString")` would return an
49
+ * inherited function rather than `undefined`, `clear("constructor")` would
50
+ * treat a key it never held as present, and `set` on `"__proto__"` would hit
51
+ * `Object.prototype`'s setter and change the prototype instead of storing the
52
+ * intent. With no prototype there is nothing to inherit and nothing to poison,
53
+ * and every string round-trips as an ordinary key.
54
+ */
55
+ all(): Promise<Record<string, WakeIntent>>;
56
+ get(key: string): Promise<WakeIntent | undefined>;
57
+ set(intent: WakeIntent): Promise<void>;
58
+ clear(key: string): Promise<void>;
59
+ /** Every intent whose time has come, earliest first. */
60
+ due(now: number): Promise<WakeIntent[]>;
61
+ /**
62
+ * Point the alarm at the earliest deadline.
63
+ *
64
+ * Only ever moved **earlier**, never later: an alarm that fires too soon finds
65
+ * nothing due, re-arms, and costs one wake-up, whereas an alarm pushed later
66
+ * by a coincidental write silently delays whatever was already waiting. When
67
+ * no intents remain the alarm is deleted outright, so an idle object does not
68
+ * wake on a schedule it has no use for.
69
+ */
70
+ rearm(): Promise<void>;
71
+ /**
72
+ * Re-arm shortly, for when the handler failed before it could work out what
73
+ * it owed. Distinct from {@link rearm} because that one trusts the map, and
74
+ * the map is what we just failed to read.
75
+ */
76
+ repair(now: number): Promise<void>;
77
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `@loopingai/core/alarm` — many deadlines over a Durable Object's one alarm.
3
+ *
4
+ * A Durable Object has exactly **one** alarm, and an object that needs to wake
5
+ * for more than one reason cannot simply call `setAlarm` from each of them: the
6
+ * last writer silently wins, and whatever the loser was waiting on never
7
+ * happens. {@link WakeMap} is the fix — one storage row holding every pending
8
+ * intent, and the only thing in a DO that calls `setAlarm`.
9
+ *
10
+ * **Its own subpath, deliberately.** This is useful to a plain `DurableObject`,
11
+ * not only to a {@link LoopingAgent}, so it must be importable without pulling
12
+ * the agent machinery into a bundle.
13
+ *
14
+ * **Why not `Agent.schedule()`.** The `agents` SDK has the same mechanism, but
15
+ * sells it only as a method on `Agent`: adopting it means the object becomes an
16
+ * `Agent`, whose constructor creates `cf_agents_state`, `cf_agents_mcp_servers`
17
+ * and `cf_agents_queues` in that object's SQLite, builds an `MCPClientManager`,
18
+ * and prototype-patches every public method for tracing. For an object whose
19
+ * SQLite is something else already — a container's filesystem, say — that is a
20
+ * large import for a small one. `agents/schedule` is not an alternative: it is a
21
+ * prompt and a zod schema for parsing natural-language dates, not alarm
22
+ * machinery.
23
+ *
24
+ * This owns *when* an object wakes. What it owes on waking is the object's own:
25
+ * `alarm()` reads {@link WakeMap.due} and dispatches.
26
+ */
27
+ /** The single storage row holding every intent. Small, and written atomically. */
28
+ export const WAKE_KEY = "wake";
29
+ /** How far out {@link WakeMap.repair} re-arms when the handler itself failed. */
30
+ export const WAKE_REPAIR_MS = 60_000;
31
+ export class WakeMap {
32
+ #storage;
33
+ constructor(storage) {
34
+ this.#storage = storage;
35
+ }
36
+ /**
37
+ * Every pending intent, as a **null-prototype** dictionary rebuilt from own
38
+ * entries only.
39
+ *
40
+ * {@link WakeIntent.key} is a caller-supplied string, so an ordinary object
41
+ * literal would let three of them misbehave: `get("toString")` would return an
42
+ * inherited function rather than `undefined`, `clear("constructor")` would
43
+ * treat a key it never held as present, and `set` on `"__proto__"` would hit
44
+ * `Object.prototype`'s setter and change the prototype instead of storing the
45
+ * intent. With no prototype there is nothing to inherit and nothing to poison,
46
+ * and every string round-trips as an ordinary key.
47
+ */
48
+ async all() {
49
+ const stored = await this.#storage.get(WAKE_KEY);
50
+ const intents = Object.create(null);
51
+ // `Object.entries` is own-enumerable-only, so nothing from a prototype can
52
+ // enter here even if the stored value arrived with one.
53
+ if (stored) {
54
+ for (const [key, intent] of Object.entries(stored))
55
+ intents[key] = intent;
56
+ }
57
+ return intents;
58
+ }
59
+ async get(key) {
60
+ return (await this.all())[key];
61
+ }
62
+ async set(intent) {
63
+ const all = await this.all();
64
+ all[intent.key] = intent;
65
+ await this.#storage.put(WAKE_KEY, all);
66
+ await this.rearm();
67
+ }
68
+ async clear(key) {
69
+ const all = await this.all();
70
+ // `hasOwn`, not `in`: the dictionary has no prototype today, and this stays
71
+ // correct if that ever changes.
72
+ if (!Object.hasOwn(all, key))
73
+ return;
74
+ delete all[key];
75
+ await this.#storage.put(WAKE_KEY, all);
76
+ await this.rearm();
77
+ }
78
+ /** Every intent whose time has come, earliest first. */
79
+ async due(now) {
80
+ return Object.values(await this.all())
81
+ .filter((intent) => intent.notBefore <= now)
82
+ .sort((a, b) => a.notBefore - b.notBefore);
83
+ }
84
+ /**
85
+ * Point the alarm at the earliest deadline.
86
+ *
87
+ * Only ever moved **earlier**, never later: an alarm that fires too soon finds
88
+ * nothing due, re-arms, and costs one wake-up, whereas an alarm pushed later
89
+ * by a coincidental write silently delays whatever was already waiting. When
90
+ * no intents remain the alarm is deleted outright, so an idle object does not
91
+ * wake on a schedule it has no use for.
92
+ */
93
+ async rearm() {
94
+ const deadlines = Object.values(await this.all()).map((i) => i.notBefore);
95
+ const existing = await this.#storage.getAlarm();
96
+ if (deadlines.length === 0) {
97
+ if (existing !== null)
98
+ await this.#storage.deleteAlarm();
99
+ return;
100
+ }
101
+ const earliest = Math.min(...deadlines);
102
+ if (existing === null || existing > earliest) {
103
+ await this.#storage.setAlarm(earliest);
104
+ }
105
+ }
106
+ /**
107
+ * Re-arm shortly, for when the handler failed before it could work out what
108
+ * it owed. Distinct from {@link rearm} because that one trusts the map, and
109
+ * the map is what we just failed to read.
110
+ */
111
+ async repair(now) {
112
+ const existing = await this.#storage.getAlarm();
113
+ if (existing === null)
114
+ await this.#storage.setAlarm(now + WAKE_REPAIR_MS);
115
+ }
116
+ }
package/dist/config.d.ts CHANGED
@@ -1,16 +1,3 @@
1
- /**
2
- * The shapes of everything an agent tunes, plus a working set of defaults.
3
- *
4
- * In the predecessor repos this file held bare `export const`s that ~12 modules
5
- * imported directly. That is what this package cannot do: a module-level constant
6
- * read at import time is not overridable by a consumer, and it freezes the value
7
- * into the module graph before `env` exists. So core owns the *shapes* and a
8
- * baseline; a consuming agent passes overrides to {@link resolveConfig} once, at
9
- * DO start, and the resolved object is threaded through explicitly.
10
- *
11
- * The distinction against {@link file://./platform.ts} stays sharp: nothing here
12
- * is a platform fact, and nothing there is tunable.
13
- */
14
1
  /** Model ids and per-call generation settings. */
15
2
  export interface ModelConfig {
16
3
  /**
@@ -40,6 +27,21 @@ export interface ModelConfig {
40
27
  fallbackChatModelId: string;
41
28
  /** AI Gateway slug; `"default"` auto-provisions on first request. */
42
29
  aiGatewayId: string;
30
+ /**
31
+ * The segment after the gateway slug in `.../{gateway}/{provider}` — either a
32
+ * provider-native path like `"anthropic"` or a
33
+ * [custom provider](https://developers.cloudflare.com/ai-gateway/configuration/custom-providers/)
34
+ * slug. Core never reads it; a `ModelRuntime` that builds its own base URL
35
+ * does.
36
+ *
37
+ * Config rather than a literal at that call site because the parent agent and
38
+ * its subagent facet resolve their config independently, and a value that
39
+ * drifts between them routes delegated subtasks through a different provider
40
+ * than the round that delegated them — silently, since both satisfy
41
+ * `ModelRuntime`. Which path to pick, and why, belongs with the agent that
42
+ * picks it.
43
+ */
44
+ aiGatewayProvider: string;
43
45
  /**
44
46
  * Output-token ceiling for every chat call. Left unset, the binding applies a
45
47
  * per-model default which a reasoning model spends on `reasoning_content`
@@ -49,6 +51,38 @@ export interface ModelConfig {
49
51
  maxOutputTokens: number;
50
52
  /** Reasoning budget forwarded on the binding's `inputs` by workers-ai-provider. */
51
53
  reasoningEffort: "low" | "medium" | "high";
54
+ /**
55
+ * How many times a single model call is retried **on the same model** before
56
+ * the round gives up on that slot and moves to the fallback.
57
+ *
58
+ * This was hardcoded to `0` for a long time, with a reason that was right for
59
+ * one case and wrong for the other. The reason: primary→fallback recovery is
60
+ * ours, so provider-level backoff only adds latency and duplicates it. True
61
+ * when the two slots are different vendors, which is what
62
+ * {@link fallbackChatModelId} advises — a 429 on one says nothing about the
63
+ * other, so falling straight through is the fastest correct move.
64
+ *
65
+ * It is false for a rate limit on an agent whose slots share a credential. A
66
+ * coder running Opus with Sonnet as its step-down burns the fallback proving
67
+ * the same 429 twice, then throws, then the Workflow retries the whole round
68
+ * and does it again. Waiting the `retry-after` the provider actually sent is
69
+ * both cheaper and far more likely to work.
70
+ *
71
+ * The AI SDK does the waiting, and it does it properly:
72
+ * `retryWithExponentialBackoffRespectingRetryHeaders` honours `retry-after-ms`
73
+ * and `retry-after` and falls back to exponential backoff. It only fires for
74
+ * an `APICallError` carrying `isRetryable`, which is why
75
+ * {@link file://./agent/anthropic/language-model.ts} maps provider errors into
76
+ * that shape rather than rethrowing them raw.
77
+ *
78
+ * Bounded at 4 by `resolveConfig`. The retries happen *inside*
79
+ * `step.do("turn:<round>")`, and the SDK caps a single honoured `retry-after`
80
+ * at 60s, so the worst case is roughly `maxRetries` minutes of waiting before
81
+ * the round has spent a token of its own — against
82
+ * {@link file://./platform.ts STEP_TIMEOUT_MS}, which is where that ceiling is
83
+ * stated and the only place it should be read from.
84
+ */
85
+ maxRetries: number;
52
86
  }
53
87
  /**
54
88
  * An execution budget, in the only two currencies that mean anything: **turns**
@@ -126,8 +160,8 @@ export interface CoreConfig {
126
160
  toolOutputWindow: number;
127
161
  /**
128
162
  * Upper bound on subtasks per **round** — a core invariant: a delegating round
129
- * emits `1..maxSubtasks` subtasks, which is also what bounds its fan-out (all
130
- * dependency-ready subtasks run concurrently, with no other concurrency cap).
163
+ * emits `1..maxSubtasks` subtasks, which is also what bounds its fan-out (they
164
+ * all run concurrently, with no other concurrency cap).
131
165
  *
132
166
  * Not a budget but a shape: it is what the delegation schema offers the model,
133
167
  * and the data layer re-checks it as the durable guard.