@loopingai/core 0.7.0 → 0.8.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.
- package/README.md +37 -5
- package/dist/a2a/caller-token.d.ts +6 -0
- package/dist/a2a/index.d.ts +1 -0
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/agent/errors.d.ts +16 -16
- package/dist/agent/errors.js +1 -1
- package/dist/agent/inference.d.ts +7 -5
- package/dist/agent/inference.js +0 -2
- package/dist/agent/model.d.ts +11 -5
- package/dist/agent/workers-ai/index.d.ts +13 -13
- package/dist/agent/workers-ai/index.js +13 -13
- package/dist/config.d.ts +3 -18
- package/dist/config.js +0 -4
- package/dist/host/agent.d.ts +41 -6
- package/dist/host/agent.js +49 -6
- package/dist/round/agent.js +18 -2
- package/dist/round/subagent.d.ts +7 -4
- package/dist/round/subagent.js +7 -4
- package/dist/subagent/index.d.ts +26 -5
- package/dist/subagent/index.js +35 -5
- package/dist/subtasks/delegate.d.ts +4 -3
- package/dist/subtasks/delegate.js +4 -3
- package/dist/testing/mock-model.js +1 -1
- package/package.json +1 -15
- package/scripts/generate-keys.mjs +15 -20
- package/dist/agent/anthropic/index.d.ts +0 -15
- package/dist/agent/anthropic/index.js +0 -19
- package/dist/agent/anthropic/language-model.d.ts +0 -59
- package/dist/agent/anthropic/language-model.js +0 -442
- package/dist/agent/anthropic/prompt.d.ts +0 -84
- package/dist/agent/anthropic/prompt.js +0 -541
- package/dist/agent/anthropic/runtime.d.ts +0 -79
- package/dist/agent/anthropic/runtime.js +0 -130
package/README.md
CHANGED
|
@@ -39,11 +39,12 @@ optional is a plugin. Anything opinionated belongs to your app.
|
|
|
39
39
|
npx looping-keys
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Set the private JWK as `A2A_SIGNING_KEY` (`.
|
|
43
|
-
when deployed) and the origins you accept
|
|
42
|
+
Set the private JWK as `A2A_SIGNING_KEY` (`.env` locally; `wrangler deploy
|
|
43
|
+
--secrets-file .env` or `wrangler secret put` when deployed) and the origins you accept
|
|
44
|
+
calls from as `GATEWAY_ORIGINS`:
|
|
44
45
|
|
|
45
46
|
```ini
|
|
46
|
-
# .
|
|
47
|
+
# .env
|
|
47
48
|
A2A_SIGNING_KEY={"crv":"Ed25519","d":"…","x":"…","kty":"OKP","kid":"a2a-2026-08-01"}
|
|
48
49
|
GATEWAY_ORIGINS=["https://gateway.example.com"]
|
|
49
50
|
```
|
|
@@ -228,7 +229,6 @@ not drag in the A2A adapter, and the test harness cannot reach a production bund
|
|
|
228
229
|
| `@loopingai/core/agent` | session, history, models + Workers AI, inference, budget, control tools |
|
|
229
230
|
| `@loopingai/core/host` | `LoopingAgent` — the Durable Object body — and `PluginHost` |
|
|
230
231
|
| `@loopingai/core/round` | the delegating round loop: `RoundAgentBase`, `runHandleTask`, `runTurn` |
|
|
231
|
-
| `@loopingai/core/anthropic` | Claude as a second provider — _optional peer on `@anthropic-ai/sdk`_ |
|
|
232
232
|
| `@loopingai/core/subtasks` | delegation types, decomposition, the `delegate` tool |
|
|
233
233
|
| `@loopingai/core/subagent` | `RecipeSubagentBase`, resumable runs, fingerprinting, workspace |
|
|
234
234
|
| `@loopingai/core/db` | `AgentDB`, `notify_tasks` + `subtasks` schema, migrations, `PluginStore` |
|
|
@@ -268,6 +268,39 @@ makes the served document a fixed point under the repeated decoding a verifier
|
|
|
268
268
|
performs. A gateway pins the card's `kid` + `jku` on first registration
|
|
269
269
|
(Trust-On-First-Use).
|
|
270
270
|
|
|
271
|
+
### Calling out, and knowing your own origin
|
|
272
|
+
|
|
273
|
+
The same key proves this agent to services that are not the gateway — another agent,
|
|
274
|
+
or any service that verifies against the published JWKS. `signCallerToken` mints the
|
|
275
|
+
short-lived token for that: `iss` is
|
|
276
|
+
this deployment's origin, `jku` is derived from it, and the audience is normalized to a
|
|
277
|
+
bare origin because the far side compares it byte-for-byte.
|
|
278
|
+
|
|
279
|
+
Its `iss` is **not** something to configure. Inside a Durable Object it is:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
protected override modelRuntime(model: ModelConfig): ModelRuntime {
|
|
283
|
+
return myProvider(this.env, model, () => this.requireSelfOrigin());
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`requireSelfOrigin()` (and `selfOrigin()`, which returns `undefined` instead of
|
|
288
|
+
throwing) answer with the origin core already delivers: the executor computes the
|
|
289
|
+
callback `jku` from `new URL(request.url).origin`, and it rides every turn into the DO
|
|
290
|
+
and on into each subagent facet. A `SELF_ORIGIN` secret only restates that, and has to
|
|
291
|
+
be kept byte-identical with the verifier's allowlist by hand in every environment.
|
|
292
|
+
|
|
293
|
+
The first turn an instance serves **pins** it, and nothing is persisted. Pinning is
|
|
294
|
+
what makes it safe to read: turns run concurrently in one Durable Object and a
|
|
295
|
+
credential thunk fires several frames below the turn that set the value, so a mutable
|
|
296
|
+
field could hand one turn another's origin. An agent has one endpoint anyway — the one
|
|
297
|
+
its card advertises and a verifier allowlists — and a fresh isolate on deploy re-learns
|
|
298
|
+
it.
|
|
299
|
+
|
|
300
|
+
It is known **inside a turn or a chunk**: `onStart`, a constructor and a scheduled
|
|
301
|
+
callback all run before any request has said what this deployment is called, and
|
|
302
|
+
`requireSelfOrigin()` throws there saying so.
|
|
303
|
+
|
|
271
304
|
---
|
|
272
305
|
|
|
273
306
|
## Plugins
|
|
@@ -466,7 +499,6 @@ await withDb("accepts a turn once", async (db) => {
|
|
|
466
499
|
- **Bindings:** `AI`, one Durable Object, one Workflow
|
|
467
500
|
- **Secrets:** `A2A_SIGNING_KEY`, `GATEWAY_ORIGINS`
|
|
468
501
|
- **Peers, never bundled:** `agents`, `ai`, `workers-ai-provider`
|
|
469
|
-
- **Optional peer:** `@anthropic-ai/sdk`, needed only by `@loopingai/core/anthropic`
|
|
470
502
|
|
|
471
503
|
That last point is not stylistic: two copies of `agents` in one Worker breaks the
|
|
472
504
|
`Session` / `SessionMessage` types and every `instanceof`. For local development
|
|
@@ -5,6 +5,12 @@ export interface CallerTokenOptions {
|
|
|
5
5
|
* This agent's own origin. It becomes `iss`, and `jku` is derived from it, and
|
|
6
6
|
* the two must agree: a verifier that accepts a `jku` on a different origin
|
|
7
7
|
* than `iss` lets one allowlisted origin impersonate another.
|
|
8
|
+
*
|
|
9
|
+
* **Not something to configure.** Inside a Durable Object it is
|
|
10
|
+
* `requireSelfOrigin()` — see {@link file://./self-origin.ts SelfOrigin},
|
|
11
|
+
* which learns it from the `jku` every turn already carries. A `SELF_ORIGIN`
|
|
12
|
+
* secret restates what the request path knows and has to be kept
|
|
13
|
+
* byte-identical with the verifier's allowlist by hand.
|
|
8
14
|
*/
|
|
9
15
|
issuer: string;
|
|
10
16
|
/**
|
package/dist/a2a/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { A2A_RPC_PATH, buildBaseCard, signCard, wireCard, parsePrivateJwk, publi
|
|
|
24
24
|
export { NOTIFICATION_TOKEN_HEADER, buildSubmittedTask, buildWorkingTask, buildCompletedTask, buildFailedTask, buildNoReplyCompletedTask, signCallbackJwt, postNotification } from "./notify.js";
|
|
25
25
|
export { deliverTerminalTask, type DeliverTerminalOptions } from "./deliver.js";
|
|
26
26
|
export { signCallerToken, type CallerTokenOptions } from "./caller-token.js";
|
|
27
|
+
export { SelfOrigin } from "./self-origin.js";
|
|
27
28
|
export { callerContext } from "./caller.js";
|
|
28
29
|
export { createPushChannel, type PushChannel, type TurnPushContext } from "./push.js";
|
|
29
30
|
export { taskStateLabel, type PlainArtifact, type PlainMessage, type PlainPart, type PlainStatus, type PlainTask } from "./task.js";
|
package/dist/a2a/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { A2A_RPC_PATH, buildBaseCard, signCard, wireCard, parsePrivateJwk, publi
|
|
|
24
24
|
export { NOTIFICATION_TOKEN_HEADER, buildSubmittedTask, buildWorkingTask, buildCompletedTask, buildFailedTask, buildNoReplyCompletedTask, signCallbackJwt, postNotification } from "./notify.js";
|
|
25
25
|
export { deliverTerminalTask } from "./deliver.js";
|
|
26
26
|
export { signCallerToken } from "./caller-token.js";
|
|
27
|
+
export { SelfOrigin } from "./self-origin.js";
|
|
27
28
|
export { callerContext } from "./caller.js";
|
|
28
29
|
export { createPushChannel } from "./push.js";
|
|
29
30
|
export { taskStateLabel } from "./task.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This deployment's own public origin — learned from the request path, never
|
|
3
|
+
* configured.
|
|
4
|
+
*
|
|
5
|
+
* ## Why an agent needs it at all
|
|
6
|
+
*
|
|
7
|
+
* A Worker that only *answers* never needs to know its own name.
|
|
8
|
+
* {@link file://../worker/index.ts createA2AWorker} derives its audience, its
|
|
9
|
+
* card and its `jku` from `new URL(request.url).origin`, and none of it outlives
|
|
10
|
+
* the request. An agent that **calls out** mint-signed does need it:
|
|
11
|
+
* {@link file://./caller-token.ts signCallerToken} puts it in `iss` and derives
|
|
12
|
+
* the token's `jku` from it. That call happens inside a Durable Object, where
|
|
13
|
+
* there is no `Request` — which is the whole difficulty.
|
|
14
|
+
*
|
|
15
|
+
* The obvious answer is a `SELF_ORIGIN` secret, and it is the wrong one. It
|
|
16
|
+
* restates a value the request already carries, and it has to be kept
|
|
17
|
+
* byte-identical by hand with the origin allowlist on the far side, in every
|
|
18
|
+
* environment, forever. Every sibling that tried it took it back out —
|
|
19
|
+
* `looping-gateway` discovers its own origin from the first signature-verified
|
|
20
|
+
* request rather than being told, and a verifying Worker that once carried a
|
|
21
|
+
* configured audience replaced it with `url.origin`.
|
|
22
|
+
*
|
|
23
|
+
* ## Where the value comes from
|
|
24
|
+
*
|
|
25
|
+
* Core already sends the origin into the Durable Object on every turn, one field
|
|
26
|
+
* short of this use. `A2AExecutor` computes `jku` as `${origin}${jwksPath}` and
|
|
27
|
+
* it rides {@link file://./push.ts TurnPushContext} through the Workflow into
|
|
28
|
+
* `runTaskTurn` and `executeSubtaskChunk`.
|
|
29
|
+
*
|
|
30
|
+
* That is the same origin `signCallerToken` needs, and not by coincidence: a
|
|
31
|
+
* caller token's `jku` **must** be the JWKS the verifier fetches, and `iss` must
|
|
32
|
+
* agree with it — the third check in {@link file://./verify.ts verify.ts}. An
|
|
33
|
+
* origin derived from anywhere else is exactly what that check exists to catch,
|
|
34
|
+
* so deriving it from the `jku` core already serves makes the agreement
|
|
35
|
+
* structural instead of clerical.
|
|
36
|
+
*
|
|
37
|
+
* ## Pinned on the first turn, and in memory
|
|
38
|
+
*
|
|
39
|
+
* The first origin an isolate is told wins, and later ones are ignored. That is
|
|
40
|
+
* not laziness about staleness — it is what makes the value safe to *read*.
|
|
41
|
+
*
|
|
42
|
+
* A Durable Object's input gate stays open across a non-storage await, and this
|
|
43
|
+
* package runs concurrent RPCs into one object by design (`round/workflow.ts`
|
|
44
|
+
* runs a round's branches under `Promise.all`). Mutable instance state can
|
|
45
|
+
* therefore change while a turn is awaiting a model call, and the credential
|
|
46
|
+
* thunks that read this are lazy — they run several frames below the turn, when
|
|
47
|
+
* the client is built. Pinned, the field is immutable after its first write, so
|
|
48
|
+
* every concurrent reader in the isolate gets the same string and no turn can
|
|
49
|
+
* sign as another turn's origin.
|
|
50
|
+
*
|
|
51
|
+
* The cost of pinning is what an agent does not have: several identities. An
|
|
52
|
+
* agent has one endpoint — the one its card advertises, the one a gateway calls
|
|
53
|
+
* and a verifier allowlists — so there is nothing to follow. Note the asymmetry
|
|
54
|
+
* with a *verifier*, which derives the audience it expects per request and must
|
|
55
|
+
* not cache it: a verifier has to accept every hostname it answers on, while a
|
|
56
|
+
* signer needs one stable identity.
|
|
57
|
+
*
|
|
58
|
+
* Nothing is persisted, which is what keeps a pin from outliving its truth. An
|
|
59
|
+
* isolate is fresh on every `wrangler deploy` and recycles on its own, so a moved
|
|
60
|
+
* deployment re-learns its origin from the next turn it serves.
|
|
61
|
+
*/
|
|
62
|
+
export declare class SelfOrigin {
|
|
63
|
+
private observed?;
|
|
64
|
+
/**
|
|
65
|
+
* Offer the origin of an absolute URL seen on the request path — a `jku`, an
|
|
66
|
+
* endpoint, or a bare origin. Only `.origin` is kept, so a path or a trailing
|
|
67
|
+
* slash cannot reach a token claim.
|
|
68
|
+
*
|
|
69
|
+
* **The first usable value wins**; every later call is a no-op, including one
|
|
70
|
+
* naming a different origin. Called at each RPC entry and again when the push
|
|
71
|
+
* channel is built, so most calls are already no-ops — but the reason for the
|
|
72
|
+
* pin is the read side, not the write side. See the note above the class.
|
|
73
|
+
*
|
|
74
|
+
* Silently ignores anything unusable (absent, relative, or a scheme that has
|
|
75
|
+
* no meaningful origin), and an unusable value never pins: the parse comes
|
|
76
|
+
* first and the field is assigned only on success. This runs at the top of a
|
|
77
|
+
* turn, where a diagnostic value must never be the thing that fails it; the
|
|
78
|
+
* throw belongs at {@link require}, where something actually wanted the value.
|
|
79
|
+
*/
|
|
80
|
+
note(url: string | undefined): void;
|
|
81
|
+
/** The pinned origin, or `undefined` when nothing has carried one yet. */
|
|
82
|
+
peek(): string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The pinned origin, for a caller that cannot proceed without it.
|
|
85
|
+
*
|
|
86
|
+
* Throws naming the timing, because that is what the mistake always is: the
|
|
87
|
+
* value arrives with a turn, so `onStart`, a constructor and a scheduled
|
|
88
|
+
* callback all run before any request has said what this deployment is called.
|
|
89
|
+
*/
|
|
90
|
+
require(): string;
|
|
91
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This deployment's own public origin — learned from the request path, never
|
|
3
|
+
* configured.
|
|
4
|
+
*
|
|
5
|
+
* ## Why an agent needs it at all
|
|
6
|
+
*
|
|
7
|
+
* A Worker that only *answers* never needs to know its own name.
|
|
8
|
+
* {@link file://../worker/index.ts createA2AWorker} derives its audience, its
|
|
9
|
+
* card and its `jku` from `new URL(request.url).origin`, and none of it outlives
|
|
10
|
+
* the request. An agent that **calls out** mint-signed does need it:
|
|
11
|
+
* {@link file://./caller-token.ts signCallerToken} puts it in `iss` and derives
|
|
12
|
+
* the token's `jku` from it. That call happens inside a Durable Object, where
|
|
13
|
+
* there is no `Request` — which is the whole difficulty.
|
|
14
|
+
*
|
|
15
|
+
* The obvious answer is a `SELF_ORIGIN` secret, and it is the wrong one. It
|
|
16
|
+
* restates a value the request already carries, and it has to be kept
|
|
17
|
+
* byte-identical by hand with the origin allowlist on the far side, in every
|
|
18
|
+
* environment, forever. Every sibling that tried it took it back out —
|
|
19
|
+
* `looping-gateway` discovers its own origin from the first signature-verified
|
|
20
|
+
* request rather than being told, and a verifying Worker that once carried a
|
|
21
|
+
* configured audience replaced it with `url.origin`.
|
|
22
|
+
*
|
|
23
|
+
* ## Where the value comes from
|
|
24
|
+
*
|
|
25
|
+
* Core already sends the origin into the Durable Object on every turn, one field
|
|
26
|
+
* short of this use. `A2AExecutor` computes `jku` as `${origin}${jwksPath}` and
|
|
27
|
+
* it rides {@link file://./push.ts TurnPushContext} through the Workflow into
|
|
28
|
+
* `runTaskTurn` and `executeSubtaskChunk`.
|
|
29
|
+
*
|
|
30
|
+
* That is the same origin `signCallerToken` needs, and not by coincidence: a
|
|
31
|
+
* caller token's `jku` **must** be the JWKS the verifier fetches, and `iss` must
|
|
32
|
+
* agree with it — the third check in {@link file://./verify.ts verify.ts}. An
|
|
33
|
+
* origin derived from anywhere else is exactly what that check exists to catch,
|
|
34
|
+
* so deriving it from the `jku` core already serves makes the agreement
|
|
35
|
+
* structural instead of clerical.
|
|
36
|
+
*
|
|
37
|
+
* ## Pinned on the first turn, and in memory
|
|
38
|
+
*
|
|
39
|
+
* The first origin an isolate is told wins, and later ones are ignored. That is
|
|
40
|
+
* not laziness about staleness — it is what makes the value safe to *read*.
|
|
41
|
+
*
|
|
42
|
+
* A Durable Object's input gate stays open across a non-storage await, and this
|
|
43
|
+
* package runs concurrent RPCs into one object by design (`round/workflow.ts`
|
|
44
|
+
* runs a round's branches under `Promise.all`). Mutable instance state can
|
|
45
|
+
* therefore change while a turn is awaiting a model call, and the credential
|
|
46
|
+
* thunks that read this are lazy — they run several frames below the turn, when
|
|
47
|
+
* the client is built. Pinned, the field is immutable after its first write, so
|
|
48
|
+
* every concurrent reader in the isolate gets the same string and no turn can
|
|
49
|
+
* sign as another turn's origin.
|
|
50
|
+
*
|
|
51
|
+
* The cost of pinning is what an agent does not have: several identities. An
|
|
52
|
+
* agent has one endpoint — the one its card advertises, the one a gateway calls
|
|
53
|
+
* and a verifier allowlists — so there is nothing to follow. Note the asymmetry
|
|
54
|
+
* with a *verifier*, which derives the audience it expects per request and must
|
|
55
|
+
* not cache it: a verifier has to accept every hostname it answers on, while a
|
|
56
|
+
* signer needs one stable identity.
|
|
57
|
+
*
|
|
58
|
+
* Nothing is persisted, which is what keeps a pin from outliving its truth. An
|
|
59
|
+
* isolate is fresh on every `wrangler deploy` and recycles on its own, so a moved
|
|
60
|
+
* deployment re-learns its origin from the next turn it serves.
|
|
61
|
+
*/
|
|
62
|
+
export class SelfOrigin {
|
|
63
|
+
observed;
|
|
64
|
+
/**
|
|
65
|
+
* Offer the origin of an absolute URL seen on the request path — a `jku`, an
|
|
66
|
+
* endpoint, or a bare origin. Only `.origin` is kept, so a path or a trailing
|
|
67
|
+
* slash cannot reach a token claim.
|
|
68
|
+
*
|
|
69
|
+
* **The first usable value wins**; every later call is a no-op, including one
|
|
70
|
+
* naming a different origin. Called at each RPC entry and again when the push
|
|
71
|
+
* channel is built, so most calls are already no-ops — but the reason for the
|
|
72
|
+
* pin is the read side, not the write side. See the note above the class.
|
|
73
|
+
*
|
|
74
|
+
* Silently ignores anything unusable (absent, relative, or a scheme that has
|
|
75
|
+
* no meaningful origin), and an unusable value never pins: the parse comes
|
|
76
|
+
* first and the field is assigned only on success. This runs at the top of a
|
|
77
|
+
* turn, where a diagnostic value must never be the thing that fails it; the
|
|
78
|
+
* throw belongs at {@link require}, where something actually wanted the value.
|
|
79
|
+
*/
|
|
80
|
+
note(url) {
|
|
81
|
+
if (this.observed || !url)
|
|
82
|
+
return;
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(url);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
91
|
+
return;
|
|
92
|
+
this.observed = parsed.origin;
|
|
93
|
+
}
|
|
94
|
+
/** The pinned origin, or `undefined` when nothing has carried one yet. */
|
|
95
|
+
peek() {
|
|
96
|
+
return this.observed;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The pinned origin, for a caller that cannot proceed without it.
|
|
100
|
+
*
|
|
101
|
+
* Throws naming the timing, because that is what the mistake always is: the
|
|
102
|
+
* value arrives with a turn, so `onStart`, a constructor and a scheduled
|
|
103
|
+
* callback all run before any request has said what this deployment is called.
|
|
104
|
+
*/
|
|
105
|
+
require() {
|
|
106
|
+
if (!this.observed) {
|
|
107
|
+
throw new Error("this deployment's own origin is not known on this instance yet: it is " +
|
|
108
|
+
"learned from the `jku` that arrives with every turn, so it is " +
|
|
109
|
+
"available inside a turn or a subtask chunk — not from onStart, a " +
|
|
110
|
+
"constructor or a scheduled callback");
|
|
111
|
+
}
|
|
112
|
+
return this.observed;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/agent/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* any particular one exists. `nonRecoverableKind` in
|
|
7
7
|
* {@link file://./inference.ts inference.ts} keys on this file, so a provider
|
|
8
8
|
* written outside core — the thing `ModelRuntimeFactory` exists to make cheap —
|
|
9
|
-
* gets the same handling as
|
|
9
|
+
* gets the same handling as core's own, with no change to core.
|
|
10
10
|
*
|
|
11
11
|
* ## Why a third classification was needed at all
|
|
12
12
|
*
|
|
@@ -24,26 +24,26 @@
|
|
|
24
24
|
/**
|
|
25
25
|
* Who refused the request, when a `401` came back.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* it was sends an operator to rotate the wrong secret — which is exactly what
|
|
33
|
-
* happened before this existed.
|
|
34
|
-
*
|
|
35
|
-
* `"proxy"` is the odd one out: it is generally *not* a secret to rotate. An
|
|
36
|
-
* intermediary that mints its caller credential per request fails for reasons
|
|
37
|
-
* upstream of any stored secret — configuration drift, a rotated signing key,
|
|
38
|
-
* clock skew — so the remedy is to look, not to rotate. Core recognises no
|
|
39
|
-
* particular intermediary; a deployment that has one supplies its own classifier
|
|
40
|
-
* (see `AnthropicModelDeps.classifyAuthFailure`).
|
|
27
|
+
* Two authorities sit on the path, each with its own credential: the AI Gateway
|
|
28
|
+
* (`cf-aig-authorization`) and the model provider itself (`Authorization`). They
|
|
29
|
+
* fail with the same status code and have completely different remedies, so a
|
|
30
|
+
* rejection that does not say which one it was sends an operator to rotate the
|
|
31
|
+
* wrong secret — which is exactly what happened before this existed.
|
|
41
32
|
*
|
|
42
33
|
* `"unknown"` is a real answer and the default. Guessing `"provider"` for an
|
|
43
34
|
* unrecognised body is how the misdiagnosis happens; saying "one of these, here
|
|
44
35
|
* is how to check each" is worse copy and better information.
|
|
36
|
+
*
|
|
37
|
+
* A third arm, `"proxy"`, named an optional intermediary between the two — the
|
|
38
|
+
* shape where a deployment terminates the gateway's request at its own Worker to
|
|
39
|
+
* attach a credential. It was removed in 0.8.0 with the deployment that had one.
|
|
40
|
+
* If you build that topology again, the honest classification for its refusals
|
|
41
|
+
* is `"unknown"` until you widen this union, because the remedy genuinely
|
|
42
|
+
* differs: an intermediary minting its caller credential per request fails for
|
|
43
|
+
* reasons upstream of any stored secret, so the fix is to look rather than to
|
|
44
|
+
* rotate.
|
|
45
45
|
*/
|
|
46
|
-
export type CredentialRejectedBy = "provider" | "gateway" | "
|
|
46
|
+
export type CredentialRejectedBy = "provider" | "gateway" | "unknown";
|
|
47
47
|
/**
|
|
48
48
|
* A credential on the path to the model was rejected (HTTP 401 / 403).
|
|
49
49
|
*
|
package/dist/agent/errors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* any particular one exists. `nonRecoverableKind` in
|
|
7
7
|
* {@link file://./inference.ts inference.ts} keys on this file, so a provider
|
|
8
8
|
* written outside core — the thing `ModelRuntimeFactory` exists to make cheap —
|
|
9
|
-
* gets the same handling as
|
|
9
|
+
* gets the same handling as core's own, with no change to core.
|
|
10
10
|
*
|
|
11
11
|
* ## Why a third classification was needed at all
|
|
12
12
|
*
|
|
@@ -49,14 +49,16 @@ export declare function isTransientAiError(err: unknown): boolean;
|
|
|
49
49
|
* - `gateway-credential` — the gateway *in front of* the provider rejected the
|
|
50
50
|
* request, which the provider therefore never saw. Rotate the gateway's token
|
|
51
51
|
* instead; the model credential is very likely fine.
|
|
52
|
-
* - `proxy-credential` — an intermediary between the gateway and the provider
|
|
53
|
-
* rejected the caller. Notably **not** a token to rotate: the credential it
|
|
54
|
-
* refused is minted per request, so the fault is upstream of the secret —
|
|
55
|
-
* configuration drift, a rotated signing key, or clock skew.
|
|
56
52
|
* - `unknown-credential` — a `401`/`403` matching none of the shapes. Says so,
|
|
57
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.
|
|
58
60
|
*/
|
|
59
|
-
export type NonRecoverableKind = "credential" | "gateway-credential" | "
|
|
61
|
+
export type NonRecoverableKind = "credential" | "gateway-credential" | "unknown-credential";
|
|
60
62
|
/**
|
|
61
63
|
* Why a round ended with no answer — one terminal status, two situations.
|
|
62
64
|
*
|
package/dist/agent/inference.js
CHANGED
|
@@ -80,8 +80,6 @@ export function nonRecoverableKind(err) {
|
|
|
80
80
|
return "credential";
|
|
81
81
|
case "gateway":
|
|
82
82
|
return "gateway-credential";
|
|
83
|
-
case "proxy":
|
|
84
|
-
return "proxy-credential";
|
|
85
83
|
// Includes an error that crossed a realm boundary carrying no `source` at
|
|
86
84
|
// all: `isInstance` is structural, so that is reachable, and "unknown" is
|
|
87
85
|
// the honest reading of it.
|
package/dist/agent/model.d.ts
CHANGED
|
@@ -10,11 +10,17 @@ import type { ModelConfig } from "../config.js";
|
|
|
10
10
|
* more than a `LanguageModel` from `ai`. It never learns which provider produced
|
|
11
11
|
* it.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* {@link file://./workers-ai/index.ts `./workers-ai`}
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
24
|
*
|
|
19
25
|
* Which is why this file has no runtime imports at all. The Workers AI factory
|
|
20
26
|
* used to live in it, and a contract that ships one implementation inline reads
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workers AI — core's default model provider.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
* provider rather than a capability: it ships no tools, no prompt copy and no
|
|
4
|
+
* A provider rather than a capability: it ships no tools, no prompt copy and no
|
|
6
5
|
* policy; it satisfies {@link file://../model.ts ModelRuntime} and stops.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Two things about its shape are worth stating, because both look like
|
|
8
|
+
* omissions:
|
|
9
9
|
*
|
|
10
10
|
* **No `LanguageModel` adapter.** `workers-ai-provider` ships one, so this
|
|
11
|
-
* directory is a factory and nothing else
|
|
12
|
-
* adapter
|
|
13
|
-
*
|
|
11
|
+
* directory is a factory and nothing else — one file rather than the five a
|
|
12
|
+
* hand-written adapter costs (a prompt mapping, a cache-control policy, an
|
|
13
|
+
* error taxonomy). That is what makes it the cheap default.
|
|
14
14
|
*
|
|
15
|
-
* **No package subpath.**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* **No package subpath.** `workers-ai-provider` is a *required* peer and this is
|
|
16
|
+
* the default every un-overridden seam reaches through `LoopingAgent` itself, so
|
|
17
|
+
* it is in every consumer's module graph already and a subpath would buy
|
|
18
|
+
* nothing. It is exported from `@loopingai/core/agent` — one symbol, one import
|
|
19
|
+
* path. A provider behind an *optional* peer would want its own subpath instead,
|
|
20
|
+
* so that an agent never calling it does not pay for it; `./anthropic` was one
|
|
21
|
+
* until 0.8.0.
|
|
22
22
|
*/
|
|
23
23
|
export { createWorkersAIModelRuntime, workersAIModels, type WorkersAIRuntimeDeps } from "./runtime.js";
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workers AI — core's default model provider.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
* provider rather than a capability: it ships no tools, no prompt copy and no
|
|
4
|
+
* A provider rather than a capability: it ships no tools, no prompt copy and no
|
|
6
5
|
* policy; it satisfies {@link file://../model.ts ModelRuntime} and stops.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Two things about its shape are worth stating, because both look like
|
|
8
|
+
* omissions:
|
|
9
9
|
*
|
|
10
10
|
* **No `LanguageModel` adapter.** `workers-ai-provider` ships one, so this
|
|
11
|
-
* directory is a factory and nothing else
|
|
12
|
-
* adapter
|
|
13
|
-
*
|
|
11
|
+
* directory is a factory and nothing else — one file rather than the five a
|
|
12
|
+
* hand-written adapter costs (a prompt mapping, a cache-control policy, an
|
|
13
|
+
* error taxonomy). That is what makes it the cheap default.
|
|
14
14
|
*
|
|
15
|
-
* **No package subpath.**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* **No package subpath.** `workers-ai-provider` is a *required* peer and this is
|
|
16
|
+
* the default every un-overridden seam reaches through `LoopingAgent` itself, so
|
|
17
|
+
* it is in every consumer's module graph already and a subpath would buy
|
|
18
|
+
* nothing. It is exported from `@loopingai/core/agent` — one symbol, one import
|
|
19
|
+
* path. A provider behind an *optional* peer would want its own subpath instead,
|
|
20
|
+
* so that an agent never calling it does not pay for it; `./anthropic` was one
|
|
21
|
+
* until 0.8.0.
|
|
22
22
|
*/
|
|
23
23
|
export { createWorkersAIModelRuntime, workersAIModels } from "./runtime.js";
|
package/dist/config.d.ts
CHANGED
|
@@ -27,21 +27,6 @@ export interface ModelConfig {
|
|
|
27
27
|
fallbackChatModelId: string;
|
|
28
28
|
/** AI Gateway slug; `"default"` auto-provisions on first request. */
|
|
29
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;
|
|
45
30
|
/**
|
|
46
31
|
* Output-token ceiling for every chat call. Left unset, the binding applies a
|
|
47
32
|
* per-model default which a reasoning model spends on `reasoning_content`
|
|
@@ -71,9 +56,9 @@ export interface ModelConfig {
|
|
|
71
56
|
* The AI SDK does the waiting, and it does it properly:
|
|
72
57
|
* `retryWithExponentialBackoffRespectingRetryHeaders` honours `retry-after-ms`
|
|
73
58
|
* and `retry-after` and falls back to exponential backoff. It only fires for
|
|
74
|
-
* an `APICallError` carrying `isRetryable`,
|
|
75
|
-
*
|
|
76
|
-
*
|
|
59
|
+
* an `APICallError` carrying `isRetryable`, so a provider written outside core
|
|
60
|
+
* has to map its errors into that shape rather than rethrowing them raw, or
|
|
61
|
+
* none of this fires for it.
|
|
77
62
|
*
|
|
78
63
|
* Bounded at 4 by `resolveConfig`. The retries happen *inside*
|
|
79
64
|
* `step.do("turn:<round>")`, and the SDK caps a single honoured `retry-after`
|
package/dist/config.js
CHANGED
|
@@ -32,10 +32,6 @@ export const DEFAULT_CORE_CONFIG = {
|
|
|
32
32
|
// Not a model: an AI Gateway slug, and `"default"` is Cloudflare's own
|
|
33
33
|
// auto-provision behaviour rather than a choice core is making for anyone.
|
|
34
34
|
aiGatewayId: "default",
|
|
35
|
-
// The provider-native Anthropic endpoint, which is what a deployment with
|
|
36
|
-
// no custom provider registered has. Agents that must own the credential
|
|
37
|
-
// themselves override this with their `custom-*` slug.
|
|
38
|
-
aiGatewayProvider: "anthropic",
|
|
39
35
|
maxOutputTokens: 16_384,
|
|
40
36
|
reasoningEffort: "medium",
|
|
41
37
|
// Two, which is the AI SDK's own default and enough to ride out the kind of
|
package/dist/host/agent.d.ts
CHANGED
|
@@ -69,6 +69,17 @@ export declare abstract class LoopingAgent<TEnv extends Cloudflare.Env & AiEnv &
|
|
|
69
69
|
* not depend on it surviving.
|
|
70
70
|
*/
|
|
71
71
|
private identityKey?;
|
|
72
|
+
/**
|
|
73
|
+
* This deployment's own public origin, learned from the `jku` every turn
|
|
74
|
+
* carries and **pinned on the first one** this instance serves.
|
|
75
|
+
*
|
|
76
|
+
* Unlike {@link identityKey} this is shared by concurrent turns — the object
|
|
77
|
+
* is keyed by caller, not by origin — so it is pinned rather than
|
|
78
|
+
* last-write-wins: an immutable field cannot change under a credential thunk
|
|
79
|
+
* that reads it while a turn awaits a model call. See {@link SelfOrigin} for
|
|
80
|
+
* the full argument, and for why nothing is persisted.
|
|
81
|
+
*/
|
|
82
|
+
private readonly selfOriginMemo;
|
|
72
83
|
/**
|
|
73
84
|
* Test-only model injection. A **field**, not a constructor argument or an RPC
|
|
74
85
|
* parameter, so it never appears on the generated DO stub: production callers
|
|
@@ -107,12 +118,11 @@ export declare abstract class LoopingAgent<TEnv extends Cloudflare.Env & AiEnv &
|
|
|
107
118
|
* a trap that only shows up as a performance bug. This is called once.
|
|
108
119
|
*
|
|
109
120
|
* `ModelRuntime` is the whole contract: return anything satisfying it and
|
|
110
|
-
* every loop in core keeps working unchanged. Core ships
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* anything on this path.
|
|
121
|
+
* every loop in core keeps working unchanged. Core ships one implementation,
|
|
122
|
+
* {@link file://../agent/workers-ai/index.ts `agent/workers-ai`} (the default
|
|
123
|
+
* below); a second provider is one more
|
|
124
|
+
* {@link file://../agent/model.ts ModelRuntimeFactory}, defined here or in the
|
|
125
|
+
* consumer, not a change to anything on this path.
|
|
116
126
|
*
|
|
117
127
|
* Takes the resolved {@link ModelConfig} rather than reading `this.config`, so
|
|
118
128
|
* that this signature matches
|
|
@@ -197,6 +207,31 @@ export declare abstract class LoopingAgent<TEnv extends Cloudflare.Env & AiEnv &
|
|
|
197
207
|
* exists.
|
|
198
208
|
*/
|
|
199
209
|
protected requireIdentityKey(): string;
|
|
210
|
+
/**
|
|
211
|
+
* Offer this deployment's own origin from a value that carries it. The first
|
|
212
|
+
* usable one is kept for the life of the instance.
|
|
213
|
+
*
|
|
214
|
+
* Called wherever a {@link TurnPushContext} arrives — here for every agent
|
|
215
|
+
* shape, and at the entry of `RoundAgentBase`'s two RPCs, where the origin is
|
|
216
|
+
* needed *before* this channel would be built. All three matter because any of
|
|
217
|
+
* them can be the call that wakes a fresh isolate. Cheap and unfailing: past
|
|
218
|
+
* the first turn it is one truthiness check, and an unusable value is ignored
|
|
219
|
+
* rather than thrown, because a turn must not fail over this.
|
|
220
|
+
*/
|
|
221
|
+
protected noteSelfOrigin(url: string | undefined): void;
|
|
222
|
+
/**
|
|
223
|
+
* This deployment's own public origin, if a turn has carried it to this
|
|
224
|
+
* instance yet. Constant once set, so it reads the same from any turn running
|
|
225
|
+
* on this object. See {@link SelfOrigin}.
|
|
226
|
+
*/
|
|
227
|
+
protected selfOrigin(): string | undefined;
|
|
228
|
+
/**
|
|
229
|
+
* The same, for a caller that cannot proceed without it — signing a caller
|
|
230
|
+
* token with {@link file://../a2a/caller-token.ts signCallerToken} above all,
|
|
231
|
+
* whose `iss` this is. Throws naming the timing rather than producing a token
|
|
232
|
+
* with a nonsense issuer.
|
|
233
|
+
*/
|
|
234
|
+
protected requireSelfOrigin(): string;
|
|
200
235
|
/** The gateway callback channel for one turn. See {@link PushChannel}. */
|
|
201
236
|
protected push(context: TurnPushContext): PushChannel;
|
|
202
237
|
/**
|