@loopingai/core 0.7.1 → 0.8.1
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 +3 -4
- package/dist/a2a/self-origin.d.ts +7 -7
- package/dist/a2a/self-origin.js +7 -7
- 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 +5 -6
- package/dist/host/agent.js +5 -6
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/round/subagent.d.ts +7 -4
- package/dist/round/subagent.js +7 -4
- 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 +8 -16
- 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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { isRearmable } from "./state.js";
|
|
2
|
+
/**
|
|
3
|
+
* `WakeMap`'s own storage row, spelled here rather than imported.
|
|
4
|
+
*
|
|
5
|
+
* Importing `WAKE_KEY` would be a *value* import from `../alarm`, and this
|
|
6
|
+
* module is careful to reach that package only for types — a runtime edge would
|
|
7
|
+
* pull the whole alarm module into any bundle that imports `/job`. So the string
|
|
8
|
+
* is duplicated, and `lifecycle.spec.ts` asserts it still equals `WAKE_KEY`;
|
|
9
|
+
* specs never ship, so the check costs nothing at runtime and fails loudly if
|
|
10
|
+
* the two ever drift.
|
|
11
|
+
*/
|
|
12
|
+
const WAKE_MAP_KEY = "wake";
|
|
13
|
+
const DEFAULT_STALE_MS = 5 * 60_000;
|
|
14
|
+
const DEFAULT_WATCH_MS = 60_000;
|
|
15
|
+
const DEFAULT_ARM_COOLDOWN_MS = 5 * 60_000;
|
|
16
|
+
export class JobLifecycle {
|
|
17
|
+
#o;
|
|
18
|
+
/** `install` — the state record. */
|
|
19
|
+
stateKey;
|
|
20
|
+
/** `install:armed` — the stamp the arming path wrote, for the alarm to match. */
|
|
21
|
+
armedKey;
|
|
22
|
+
/** `install:last-armed` — the cooldown floor. */
|
|
23
|
+
lastArmedKey;
|
|
24
|
+
/** `install:context` — where the generation marker lives. */
|
|
25
|
+
contextKey;
|
|
26
|
+
/** `install-run` — the intent that *runs* a job. */
|
|
27
|
+
runIntent;
|
|
28
|
+
/** `install-watch` — the intent that re-attaches to one nobody is draining. */
|
|
29
|
+
watchIntent;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
/**
|
|
32
|
+
* An id is a storage key, so a bad one is not a bad name — it is a write
|
|
33
|
+
* landing on somebody else's row.
|
|
34
|
+
*
|
|
35
|
+
* `"wake"` is the one that matters and the reason this guard exists: it is
|
|
36
|
+
* `WakeMap`'s single row, so a job with that id would overwrite the whole
|
|
37
|
+
* intent map on its first state write, and the `wake.set()` immediately
|
|
38
|
+
* after would then read job fields as intents. Every pending wake-up on the
|
|
39
|
+
* object — not just this job's — silently stops happening.
|
|
40
|
+
*
|
|
41
|
+
* Empty is rejected for the same reason one level down: it yields the
|
|
42
|
+
* intents `-run` and `-watch`, which two differently-broken callers would
|
|
43
|
+
* share.
|
|
44
|
+
*/
|
|
45
|
+
if (!options.id)
|
|
46
|
+
throw new Error("a job id must be a non-empty string");
|
|
47
|
+
if (options.id === WAKE_MAP_KEY) {
|
|
48
|
+
throw new Error(`"${WAKE_MAP_KEY}" is reserved: it is WakeMap's storage row, and a job ` +
|
|
49
|
+
`with that id would overwrite every pending intent on this object`);
|
|
50
|
+
}
|
|
51
|
+
this.#o = {
|
|
52
|
+
...options,
|
|
53
|
+
staleMs: options.staleMs ?? DEFAULT_STALE_MS,
|
|
54
|
+
watchMs: options.watchMs ?? DEFAULT_WATCH_MS,
|
|
55
|
+
armCooldownMs: options.armCooldownMs ?? DEFAULT_ARM_COOLDOWN_MS
|
|
56
|
+
};
|
|
57
|
+
this.stateKey = options.id;
|
|
58
|
+
this.armedKey = `${options.id}:armed`;
|
|
59
|
+
this.lastArmedKey = `${options.id}:last-armed`;
|
|
60
|
+
this.contextKey = `${options.id}:context`;
|
|
61
|
+
this.runIntent = `${options.id}-run`;
|
|
62
|
+
this.watchIntent = `${options.id}-watch`;
|
|
63
|
+
}
|
|
64
|
+
// --- the record ------------------------------------------------------------
|
|
65
|
+
/** The raw record, with no staleness repair. `idle` when nothing is written. */
|
|
66
|
+
async read() {
|
|
67
|
+
return ((await this.#o.storage.get(this.stateKey)) ??
|
|
68
|
+
{ state: "idle" });
|
|
69
|
+
}
|
|
70
|
+
async write(state) {
|
|
71
|
+
await this.#o.storage.put(this.stateKey, state);
|
|
72
|
+
}
|
|
73
|
+
async context() {
|
|
74
|
+
return await this.#o.storage.get(this.contextKey);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record which run this is, **before** spawning.
|
|
78
|
+
*
|
|
79
|
+
* The order is the whole point: a drain captures `startedAt` after the spawn,
|
|
80
|
+
* so a context written afterwards would let two runs share a generation.
|
|
81
|
+
*/
|
|
82
|
+
async putContext(context) {
|
|
83
|
+
await this.#o.storage.put(this.contextKey, context);
|
|
84
|
+
}
|
|
85
|
+
// --- arming ----------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Hand a cold job to the alarm, if one is not already pending.
|
|
88
|
+
*
|
|
89
|
+
* Returns the stamp it armed with, or `undefined` when it declined — the
|
|
90
|
+
* caller needs the stamp because it is what the alarm must present to
|
|
91
|
+
* {@link claim} to get past the single-flight guard.
|
|
92
|
+
*
|
|
93
|
+
* An arming caller must **not** own the run. The predecessor handed one to
|
|
94
|
+
* `ctx.waitUntil` from a gate poll that returned in milliseconds, and the
|
|
95
|
+
* drain was disposed underneath it mid-command. An alarm invocation belongs to
|
|
96
|
+
* the object rather than to any request, so nothing it awaits can be cut short.
|
|
97
|
+
*/
|
|
98
|
+
async arm(placeholder) {
|
|
99
|
+
const state = await this.read();
|
|
100
|
+
if (!isRearmable(state))
|
|
101
|
+
return undefined;
|
|
102
|
+
const lastArmed = await this.#o.storage.get(this.lastArmedKey);
|
|
103
|
+
if (lastArmed !== undefined &&
|
|
104
|
+
Date.now() - lastArmed < this.#o.armCooldownMs)
|
|
105
|
+
return undefined;
|
|
106
|
+
const armedAt = Date.now();
|
|
107
|
+
await this.write({
|
|
108
|
+
...placeholder,
|
|
109
|
+
state: "running",
|
|
110
|
+
startedAt: armedAt
|
|
111
|
+
});
|
|
112
|
+
await this.#o.storage.put(this.armedKey, armedAt);
|
|
113
|
+
// Kept even if the scheduling below fails, deliberately: a floor that only
|
|
114
|
+
// applied to *successful* arming would let a persistently failing schedule
|
|
115
|
+
// re-arm on every call into the object, which is what it exists to prevent.
|
|
116
|
+
await this.#o.storage.put(this.lastArmedKey, armedAt);
|
|
117
|
+
/**
|
|
118
|
+
* The placeholder and the alarm that owns it are two writes, and between
|
|
119
|
+
* them is the one window where this can strand a job: a `running` record no
|
|
120
|
+
* run intent points at, which every later {@link arm} then declines to
|
|
121
|
+
* replace *because* it is running.
|
|
122
|
+
*
|
|
123
|
+
* The staleness bound in {@link claim} would eventually free it, but only
|
|
124
|
+
* after a full timeout — so unwind instead, and leave the record exactly as
|
|
125
|
+
* re-armable as it was found.
|
|
126
|
+
*/
|
|
127
|
+
try {
|
|
128
|
+
await this.#o.wake.set({ key: this.runIntent, notBefore: armedAt });
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
await this.write(state);
|
|
132
|
+
await this.#o.storage.delete(this.armedKey).catch(() => { });
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
return armedAt;
|
|
136
|
+
}
|
|
137
|
+
/** The stamp {@link arm} wrote, so the alarm can recognise its own placeholder. */
|
|
138
|
+
async armedAt() {
|
|
139
|
+
return await this.#o.storage.get(this.armedKey);
|
|
140
|
+
}
|
|
141
|
+
async clearArmed() {
|
|
142
|
+
await this.#o.storage.delete(this.armedKey);
|
|
143
|
+
}
|
|
144
|
+
// --- the single-flight guard ------------------------------------------------
|
|
145
|
+
/**
|
|
146
|
+
* Decide whether a new run may start.
|
|
147
|
+
*
|
|
148
|
+
* `takeOverArmedAt` is the one exemption and it is narrow on purpose. The
|
|
149
|
+
* alarm's placeholder *is* a `running` record for a job that has not started,
|
|
150
|
+
* so the alarm has to pass its own guard — and only its own. Matching the
|
|
151
|
+
* exact stamp it wrote is what stops this becoming "take over any running
|
|
152
|
+
* job", which is the displacement bug the guard exists to prevent: three
|
|
153
|
+
* callers spawning under one exec id in fifty seconds, each displacing the
|
|
154
|
+
* last, every displaced drain still attached and still writing verdicts.
|
|
155
|
+
*
|
|
156
|
+
* Applies the staleness bound **itself**, rather than trusting the caller to
|
|
157
|
+
* have repaired the record first. An earlier draft took an
|
|
158
|
+
* "already-repaired" state and said so in prose, which enforced nothing: the
|
|
159
|
+
* repaired and raw types are identical, so a caller passing a raw read got a
|
|
160
|
+
* `running` record that could never be claimed and a job wedged forever.
|
|
161
|
+
* `timeoutMs` is the job's own budget; see {@link isStale}.
|
|
162
|
+
*/
|
|
163
|
+
claim(state, timeoutMs, takeOverArmedAt) {
|
|
164
|
+
if (state.state !== "running")
|
|
165
|
+
return { ok: true };
|
|
166
|
+
// The alarm presenting its own placeholder — the one narrow exemption.
|
|
167
|
+
if (state.startedAt === takeOverArmedAt)
|
|
168
|
+
return { ok: true };
|
|
169
|
+
// A record whose isolate is gone must not block every later run.
|
|
170
|
+
if (this.isStale(state, timeoutMs))
|
|
171
|
+
return { ok: true };
|
|
172
|
+
return { ok: false, current: state };
|
|
173
|
+
}
|
|
174
|
+
// --- staleness and re-attach -------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Whether a `running` record has stood long enough to be presumed dead.
|
|
177
|
+
*
|
|
178
|
+
* `timeoutMs` is the job's own budget; the bound is that plus `staleMs`, so a
|
|
179
|
+
* job that is merely slow is never declared gone.
|
|
180
|
+
*/
|
|
181
|
+
isStale(state, timeoutMs, now = Date.now()) {
|
|
182
|
+
return now - state.startedAt > timeoutMs + this.#o.staleMs;
|
|
183
|
+
}
|
|
184
|
+
/** Arm the watchdog that re-attaches to a job nobody is draining. */
|
|
185
|
+
async armWatch(now = Date.now()) {
|
|
186
|
+
await this.#o.wake.set({
|
|
187
|
+
key: this.watchIntent,
|
|
188
|
+
notBefore: now + this.#o.watchMs
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Disarm the watchdog.
|
|
193
|
+
*
|
|
194
|
+
* Never call this from a superseded drain: the watchdog belongs to whichever
|
|
195
|
+
* run owns the record *now*, and clearing it there disarms the one recovery
|
|
196
|
+
* path the live run has.
|
|
197
|
+
*/
|
|
198
|
+
async clearWatch() {
|
|
199
|
+
await this.#o.wake.clear(this.watchIntent).catch(() => { });
|
|
200
|
+
}
|
|
201
|
+
// --- generation --------------------------------------------------------------
|
|
202
|
+
/**
|
|
203
|
+
* A predicate a drain calls before every write, to ask whether it still owns
|
|
204
|
+
* the record.
|
|
205
|
+
*
|
|
206
|
+
* Captures the stamp once, at drain start, and compares it against disk each
|
|
207
|
+
* time. The closure also latches, so a drain can ask afterwards whether it was
|
|
208
|
+
* superseded — which is what decides if it may touch the watchdog.
|
|
209
|
+
*/
|
|
210
|
+
generation(startedAt) {
|
|
211
|
+
let superseded = false;
|
|
212
|
+
return {
|
|
213
|
+
stillMine: async () => {
|
|
214
|
+
// The latch is checked *before* the read, not after. Ownership is not
|
|
215
|
+
// recoverable: once another run has owned this record, a stamp that
|
|
216
|
+
// happens to match again does not hand it back, and a drain that
|
|
217
|
+
// regained write access here would be the corruption the marker exists
|
|
218
|
+
// to prevent.
|
|
219
|
+
if (superseded)
|
|
220
|
+
return false;
|
|
221
|
+
const now = await this.context();
|
|
222
|
+
if (now?.startedAt === startedAt)
|
|
223
|
+
return true;
|
|
224
|
+
superseded = true;
|
|
225
|
+
return false;
|
|
226
|
+
},
|
|
227
|
+
superseded: () => superseded
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record an alarm-owned job writes about itself, and the shape a gate reads.
|
|
3
|
+
*
|
|
4
|
+
* This is the contract between two halves that do not own each other: the
|
|
5
|
+
* Durable Object *runs* the job, and something else — a shell tool, a subagent
|
|
6
|
+
* executor — refuses to proceed while one is in flight. A shape they agreed on
|
|
7
|
+
* informally would drift, and the drift shows up as a command running halfway
|
|
8
|
+
* through the job it was supposed to wait for.
|
|
9
|
+
*
|
|
10
|
+
* ## Why `TExtra` intersects rather than nests
|
|
11
|
+
*
|
|
12
|
+
* The obvious generic is `{ state: "running"; meta: TExtra }`. It is wrong here,
|
|
13
|
+
* and expensively so: every existing reader spells the job's own field at the
|
|
14
|
+
* top level (`status.command`), so nesting would rewrite every read site and
|
|
15
|
+
* every spec assertion in both consumers to buy nothing. Intersecting keeps
|
|
16
|
+
* `JobState<{ command: string }>` *byte-identical* to the hand-written union it
|
|
17
|
+
* replaces, which is what makes adopting this a type change and not a refactor.
|
|
18
|
+
*
|
|
19
|
+
* The cost of the choice is that `TExtra` must not collide with the field names
|
|
20
|
+
* below. That is a real constraint, and it is why they are named for the
|
|
21
|
+
* mechanism (`startedAt`, `finishedAt`, `exitCode`) rather than for any job.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* A job's durable state.
|
|
25
|
+
*
|
|
26
|
+
* Five variants, and the two that look redundant are not:
|
|
27
|
+
*
|
|
28
|
+
* - `idle` — nothing has ever run. There is no context recording *where* or
|
|
29
|
+
* *what*, so a caller cannot re-drive it; that is the owner's job.
|
|
30
|
+
* - `skipped` — something looked and decided there was nothing to do. Terminal
|
|
31
|
+
* and *correct*, which is why it is not `done`: a gate must not treat a
|
|
32
|
+
* deliberate no-op as a failure to retry, and an arming path must not re-drive
|
|
33
|
+
* it forever.
|
|
34
|
+
* - `running` — in flight, or believed to be. Never trusted without the
|
|
35
|
+
* staleness bound in {@link JobLifecycle.claim}, because the isolate that
|
|
36
|
+
* wrote it may be long gone.
|
|
37
|
+
* - `done` / `failed` — terminal, carrying enough to explain the outcome without
|
|
38
|
+
* the caller reaching for the transcript.
|
|
39
|
+
*/
|
|
40
|
+
/** Nothing has ever run; no context exists naming what would. */
|
|
41
|
+
export type IdleJob = {
|
|
42
|
+
state: "idle";
|
|
43
|
+
};
|
|
44
|
+
/** Something looked and decided there was nothing to do. Terminal and correct. */
|
|
45
|
+
export type SkippedJob = {
|
|
46
|
+
state: "skipped";
|
|
47
|
+
reason: string;
|
|
48
|
+
};
|
|
49
|
+
/** In flight, or believed to be. Never trusted without the staleness bound. */
|
|
50
|
+
export type RunningJob<TExtra = Record<never, never>> = {
|
|
51
|
+
state: "running";
|
|
52
|
+
startedAt: number;
|
|
53
|
+
tail?: string;
|
|
54
|
+
} & TExtra;
|
|
55
|
+
export type DoneJob<TExtra = Record<never, never>> = {
|
|
56
|
+
state: "done";
|
|
57
|
+
exitCode: number;
|
|
58
|
+
finishedAt: number;
|
|
59
|
+
ms: number;
|
|
60
|
+
tail?: string;
|
|
61
|
+
} & TExtra;
|
|
62
|
+
export type FailedJob<TExtra = Record<never, never>> = {
|
|
63
|
+
state: "failed";
|
|
64
|
+
finishedAt: number;
|
|
65
|
+
error: string;
|
|
66
|
+
exitCode?: number;
|
|
67
|
+
tail?: string;
|
|
68
|
+
} & TExtra;
|
|
69
|
+
/**
|
|
70
|
+
* A job's durable state.
|
|
71
|
+
*
|
|
72
|
+
* The variants are named types rather than inlined into the union because
|
|
73
|
+
* `Extract<JobState<TExtra>, { state: "running" }>` cannot narrow while `TExtra`
|
|
74
|
+
* is generic — the compiler has no way to prove `DoneJob & TExtra` does not also
|
|
75
|
+
* carry `state: "running"`. Naming them is what lets a caller say
|
|
76
|
+
* `RunningJob<TExtra>` and get its fields.
|
|
77
|
+
*/
|
|
78
|
+
export type JobState<TExtra = Record<never, never>> = IdleJob | SkippedJob | RunningJob<TExtra> | DoneJob<TExtra> | FailedJob<TExtra>;
|
|
79
|
+
/**
|
|
80
|
+
* Whether a state is one a new run may start from.
|
|
81
|
+
*
|
|
82
|
+
* `done` and `failed` both qualify, and the second was a gap worth closing in
|
|
83
|
+
* the predecessor: arming used to require `done`, so one bad run left a record
|
|
84
|
+
* that declined to re-arm forever — one failure poisoning every task after it.
|
|
85
|
+
*
|
|
86
|
+
* `skipped` and `idle` are excluded for different reasons. `skipped` means the
|
|
87
|
+
* answer is already correct and permanent. `idle` means no context exists naming
|
|
88
|
+
* what to run, so there is nothing to re-drive.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isRearmable<TExtra extends object>(state: JobState<TExtra>): boolean;
|
|
91
|
+
/** Whether a state claims a job is in flight. Never conclusive on its own. */
|
|
92
|
+
export declare function isRunning<TExtra extends object>(state: JobState<TExtra>): state is RunningJob<TExtra>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record an alarm-owned job writes about itself, and the shape a gate reads.
|
|
3
|
+
*
|
|
4
|
+
* This is the contract between two halves that do not own each other: the
|
|
5
|
+
* Durable Object *runs* the job, and something else — a shell tool, a subagent
|
|
6
|
+
* executor — refuses to proceed while one is in flight. A shape they agreed on
|
|
7
|
+
* informally would drift, and the drift shows up as a command running halfway
|
|
8
|
+
* through the job it was supposed to wait for.
|
|
9
|
+
*
|
|
10
|
+
* ## Why `TExtra` intersects rather than nests
|
|
11
|
+
*
|
|
12
|
+
* The obvious generic is `{ state: "running"; meta: TExtra }`. It is wrong here,
|
|
13
|
+
* and expensively so: every existing reader spells the job's own field at the
|
|
14
|
+
* top level (`status.command`), so nesting would rewrite every read site and
|
|
15
|
+
* every spec assertion in both consumers to buy nothing. Intersecting keeps
|
|
16
|
+
* `JobState<{ command: string }>` *byte-identical* to the hand-written union it
|
|
17
|
+
* replaces, which is what makes adopting this a type change and not a refactor.
|
|
18
|
+
*
|
|
19
|
+
* The cost of the choice is that `TExtra` must not collide with the field names
|
|
20
|
+
* below. That is a real constraint, and it is why they are named for the
|
|
21
|
+
* mechanism (`startedAt`, `finishedAt`, `exitCode`) rather than for any job.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Whether a state is one a new run may start from.
|
|
25
|
+
*
|
|
26
|
+
* `done` and `failed` both qualify, and the second was a gap worth closing in
|
|
27
|
+
* the predecessor: arming used to require `done`, so one bad run left a record
|
|
28
|
+
* that declined to re-arm forever — one failure poisoning every task after it.
|
|
29
|
+
*
|
|
30
|
+
* `skipped` and `idle` are excluded for different reasons. `skipped` means the
|
|
31
|
+
* answer is already correct and permanent. `idle` means no context exists naming
|
|
32
|
+
* what to run, so there is nothing to re-drive.
|
|
33
|
+
*/
|
|
34
|
+
export function isRearmable(state) {
|
|
35
|
+
return state.state === "done" || state.state === "failed";
|
|
36
|
+
}
|
|
37
|
+
/** Whether a state claims a job is in flight. Never conclusive on its own. */
|
|
38
|
+
export function isRunning(state) {
|
|
39
|
+
return state.state === "running";
|
|
40
|
+
}
|
package/dist/round/subagent.d.ts
CHANGED
|
@@ -59,14 +59,17 @@ export declare abstract class RecipeSubagentHost<TEnv extends Cloudflare.Env & A
|
|
|
59
59
|
* Which provider this facet's chunks run on. Mirrors
|
|
60
60
|
* {@link file://../host/agent.ts LoopingAgent.modelRuntime}, and **must be
|
|
61
61
|
* overridden to match it** — a facet that keeps the Workers AI default while
|
|
62
|
-
* its parent runs on
|
|
63
|
-
* different model than the round that delegated it.
|
|
62
|
+
* its parent runs on another provider would silently execute every subtask on
|
|
63
|
+
* a different model than the round that delegated it.
|
|
64
64
|
*
|
|
65
65
|
* The two seams take the same arguments precisely so that keeping them in step
|
|
66
66
|
* needs no discipline: write the provider once as a
|
|
67
67
|
* {@link file://../agent/model.ts ModelRuntimeFactory} and have both return
|
|
68
|
-
* it. Two hand-copied
|
|
69
|
-
*
|
|
68
|
+
* it. Two hand-copied runtime-construction bodies is what this shape exists to
|
|
69
|
+
* stop, because nothing type-checks their agreement.
|
|
70
|
+
*
|
|
71
|
+
* Note the cheapest way to satisfy this is to override *neither* seam, which
|
|
72
|
+
* is what an agent on core's default does.
|
|
70
73
|
*
|
|
71
74
|
* Takes the model config rather than reading `this.config`, because the facet
|
|
72
75
|
* resolves its config inside `buildRuntime` and this is called from there.
|
package/dist/round/subagent.js
CHANGED
|
@@ -54,14 +54,17 @@ export class RecipeSubagentHost extends RecipeSubagentBase {
|
|
|
54
54
|
* Which provider this facet's chunks run on. Mirrors
|
|
55
55
|
* {@link file://../host/agent.ts LoopingAgent.modelRuntime}, and **must be
|
|
56
56
|
* overridden to match it** — a facet that keeps the Workers AI default while
|
|
57
|
-
* its parent runs on
|
|
58
|
-
* different model than the round that delegated it.
|
|
57
|
+
* its parent runs on another provider would silently execute every subtask on
|
|
58
|
+
* a different model than the round that delegated it.
|
|
59
59
|
*
|
|
60
60
|
* The two seams take the same arguments precisely so that keeping them in step
|
|
61
61
|
* needs no discipline: write the provider once as a
|
|
62
62
|
* {@link file://../agent/model.ts ModelRuntimeFactory} and have both return
|
|
63
|
-
* it. Two hand-copied
|
|
64
|
-
*
|
|
63
|
+
* it. Two hand-copied runtime-construction bodies is what this shape exists to
|
|
64
|
+
* stop, because nothing type-checks their agreement.
|
|
65
|
+
*
|
|
66
|
+
* Note the cheapest way to satisfy this is to override *neither* seam, which
|
|
67
|
+
* is what an agent on core's default does.
|
|
65
68
|
*
|
|
66
69
|
* Takes the model config rather than reading `this.config`, because the facet
|
|
67
70
|
* resolves its config inside `buildRuntime` and this is called from there.
|
|
@@ -54,9 +54,10 @@ export declare function makeDelegateTool(types: SubtaskTypeRegistry, maxSubtasks
|
|
|
54
54
|
* why it took a Claude-backed agent to surface it.
|
|
55
55
|
*
|
|
56
56
|
* Nothing persists this: both halves of the pair are rebuilt together on every
|
|
57
|
-
* request, so changing the shape needs no migration.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* request, so changing the shape needs no migration. There is no longer a
|
|
58
|
+
* provider-side backstop in core — the adapter that carried one went with
|
|
59
|
+
* `./anthropic` in 0.8.0 — so a provider added here that validates tool-call ids
|
|
60
|
+
* needs to sanitize them on its own way out.
|
|
60
61
|
*/
|
|
61
62
|
export declare function delegateToolCallId(taskId: string, round: number): string;
|
|
62
63
|
/**
|
|
@@ -60,9 +60,10 @@ export function makeDelegateTool(types, maxSubtasks) {
|
|
|
60
60
|
* why it took a Claude-backed agent to surface it.
|
|
61
61
|
*
|
|
62
62
|
* Nothing persists this: both halves of the pair are rebuilt together on every
|
|
63
|
-
* request, so changing the shape needs no migration.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* request, so changing the shape needs no migration. There is no longer a
|
|
64
|
+
* provider-side backstop in core — the adapter that carried one went with
|
|
65
|
+
* `./anthropic` in 0.8.0 — so a provider added here that validates tool-call ids
|
|
66
|
+
* needs to sanitize them on its own way out.
|
|
66
67
|
*/
|
|
67
68
|
export function delegateToolCallId(taskId, round) {
|
|
68
69
|
return `task_${taskId}_round_${round}_delegate`;
|
|
@@ -122,7 +122,7 @@ export function rateLimitedModel(failures, ...steps) {
|
|
|
122
122
|
if (calls <= failures) {
|
|
123
123
|
throw new APICallError({
|
|
124
124
|
message: "429 Wholesale Rate limited",
|
|
125
|
-
url: "
|
|
125
|
+
url: "mock:chat:test",
|
|
126
126
|
requestBodyValues: {},
|
|
127
127
|
statusCode: 429,
|
|
128
128
|
responseHeaders: { "retry-after": "0" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopingai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Shared, mandatory foundation for Looping agents on Cloudflare Workers: zero-trust A2A, durable task lifecycle, delegation and subagent runtime, test harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a2a",
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"types": "./dist/alarm/index.d.ts",
|
|
62
62
|
"import": "./dist/alarm/index.js"
|
|
63
63
|
},
|
|
64
|
+
"./job": {
|
|
65
|
+
"types": "./dist/job/index.d.ts",
|
|
66
|
+
"import": "./dist/job/index.js"
|
|
67
|
+
},
|
|
64
68
|
"./round": {
|
|
65
69
|
"types": "./dist/round/index.d.ts",
|
|
66
70
|
"import": "./dist/round/index.js"
|
|
@@ -94,10 +98,6 @@
|
|
|
94
98
|
"import": "./dist/testing/vcr-global-setup.js"
|
|
95
99
|
},
|
|
96
100
|
"./eslint": "./eslint-rules/index.js",
|
|
97
|
-
"./anthropic": {
|
|
98
|
-
"types": "./dist/agent/anthropic/index.d.ts",
|
|
99
|
-
"import": "./dist/agent/anthropic/index.js"
|
|
100
|
-
},
|
|
101
101
|
"./package.json": "./package.json"
|
|
102
102
|
},
|
|
103
103
|
"scripts": {
|
|
@@ -112,7 +112,8 @@
|
|
|
112
112
|
"test:watch": "vitest",
|
|
113
113
|
"verify:exports": "node scripts/verify-exports.mjs",
|
|
114
114
|
"prepack": "npm run build && npm run verify:exports",
|
|
115
|
-
"prepublishOnly": "npm run check && npm test && npm run verify:exports"
|
|
115
|
+
"prepublishOnly": "npm run check && npm test && npm run verify:exports",
|
|
116
|
+
"prepare": "husky"
|
|
116
117
|
},
|
|
117
118
|
"dependencies": {
|
|
118
119
|
"@a2a-js/sdk": "^1.0.0",
|
|
@@ -123,8 +124,6 @@
|
|
|
123
124
|
"zod": "^4.4.3"
|
|
124
125
|
},
|
|
125
126
|
"peerDependencies": {
|
|
126
|
-
"@ai-sdk/provider": "^4.0.0",
|
|
127
|
-
"@anthropic-ai/sdk": "^0.116.0",
|
|
128
127
|
"@cloudflare/vitest-pool-workers": ">=0.18",
|
|
129
128
|
"@typescript-eslint/utils": ">=8",
|
|
130
129
|
"agents": "^0.20.0",
|
|
@@ -133,12 +132,6 @@
|
|
|
133
132
|
"workers-ai-provider": "^4.0.0"
|
|
134
133
|
},
|
|
135
134
|
"peerDependenciesMeta": {
|
|
136
|
-
"@ai-sdk/provider": {
|
|
137
|
-
"optional": true
|
|
138
|
-
},
|
|
139
|
-
"@anthropic-ai/sdk": {
|
|
140
|
-
"optional": true
|
|
141
|
-
},
|
|
142
135
|
"@cloudflare/vitest-pool-workers": {
|
|
143
136
|
"optional": true
|
|
144
137
|
},
|
|
@@ -150,14 +143,13 @@
|
|
|
150
143
|
}
|
|
151
144
|
},
|
|
152
145
|
"devDependencies": {
|
|
153
|
-
"@ai-sdk/provider": "^4.0.0",
|
|
154
|
-
"@anthropic-ai/sdk": "^0.116.0",
|
|
155
146
|
"@cloudflare/vitest-pool-workers": "^0.20.1",
|
|
156
147
|
"@types/node": "^26.1.1",
|
|
157
148
|
"agents": "^0.20.0",
|
|
158
149
|
"ai": "^7.0.52",
|
|
159
150
|
"drizzle-kit": "^0.31.10",
|
|
160
151
|
"eslint": "^10.8.0",
|
|
152
|
+
"husky": "^9.1.7",
|
|
161
153
|
"prettier": "^3.9.6",
|
|
162
154
|
"typescript": "^6.0.3",
|
|
163
155
|
"typescript-eslint": "^8.66.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@loopingai/core/anthropic` — Claude as a second model provider.
|
|
3
|
-
*
|
|
4
|
-
* Its own subpath, and an **optional** peer dependency on `@anthropic-ai/sdk`,
|
|
5
|
-
* for the same reason `/round` is not re-exported from the root barrel: an agent
|
|
6
|
-
* that runs on Workers AI should not pay — in install size, in bundle bytes, or
|
|
7
|
-
* in a dependency it must keep current — for a provider it never calls.
|
|
8
|
-
*
|
|
9
|
-
* What lives here is a provider, not a capability. It ships no tools, no prompt
|
|
10
|
-
* copy and no policy; it satisfies {@link ModelRuntime} and stops.
|
|
11
|
-
*/
|
|
12
|
-
export { CredentialRejectedError, type CredentialRejectedBy } from "../errors.js";
|
|
13
|
-
export { createAnthropicLanguageModel, type AnthropicModelDeps } from "./language-model.js";
|
|
14
|
-
export { createAnthropicModelRuntime, type AnthropicRuntimeDeps } from "./runtime.js";
|
|
15
|
-
export { ANTHROPIC_PROVIDER, type CacheTtl } from "./prompt.js";
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@loopingai/core/anthropic` — Claude as a second model provider.
|
|
3
|
-
*
|
|
4
|
-
* Its own subpath, and an **optional** peer dependency on `@anthropic-ai/sdk`,
|
|
5
|
-
* for the same reason `/round` is not re-exported from the root barrel: an agent
|
|
6
|
-
* that runs on Workers AI should not pay — in install size, in bundle bytes, or
|
|
7
|
-
* in a dependency it must keep current — for a provider it never calls.
|
|
8
|
-
*
|
|
9
|
-
* What lives here is a provider, not a capability. It ships no tools, no prompt
|
|
10
|
-
* copy and no policy; it satisfies {@link ModelRuntime} and stops.
|
|
11
|
-
*/
|
|
12
|
-
// Re-exported, not owned: a rejected credential is a fact about the path to a
|
|
13
|
-
// model, not about Anthropic, so the error lives with the rest of the provider
|
|
14
|
-
// contract in {@link file://../errors.ts}. An agent that only imports this
|
|
15
|
-
// subpath still gets it from one place.
|
|
16
|
-
export { CredentialRejectedError } from "../errors.js";
|
|
17
|
-
export { createAnthropicLanguageModel } from "./language-model.js";
|
|
18
|
-
export { createAnthropicModelRuntime } from "./runtime.js";
|
|
19
|
-
export { ANTHROPIC_PROVIDER } from "./prompt.js";
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
import type { LanguageModelV4 } from "@ai-sdk/provider";
|
|
3
|
-
import { type CredentialRejectedBy } from "../errors.js";
|
|
4
|
-
import { type CacheTtl } from "./prompt.js";
|
|
5
|
-
/**
|
|
6
|
-
* A `LanguageModelV4` over `@anthropic-ai/sdk`, so core's loops can call Claude
|
|
7
|
-
* without knowing they are.
|
|
8
|
-
*
|
|
9
|
-
* Every loop in core is written against `generateText` from `ai` and a
|
|
10
|
-
* `LanguageModel` — {@link file://../../round/turn.ts turn.ts},
|
|
11
|
-
* {@link file://../../subagent/run.ts run.ts} and
|
|
12
|
-
* {@link file://../session.ts session.ts}. Satisfying that interface is what
|
|
13
|
-
* keeps the round loop, the control-tool repair ladder, subtask execution and
|
|
14
|
-
* the Workflow untouched by a second provider. The alternative — a bespoke
|
|
15
|
-
* Messages-API loop for one agent — would have given all of that up.
|
|
16
|
-
*
|
|
17
|
-
* `ai@7` accepts `LanguageModelV2 | V3 | V4`; this targets **v4**, the newest
|
|
18
|
-
* the installed `@ai-sdk/provider` defines.
|
|
19
|
-
*/
|
|
20
|
-
/** How the adapter reaches the API. Everything is injected so nothing reads env. */
|
|
21
|
-
export interface AnthropicModelDeps {
|
|
22
|
-
/**
|
|
23
|
-
* Constructed lazily by the runtime — see the note in
|
|
24
|
-
* {@link file://./runtime.ts}. Awaited, because building it has to resolve
|
|
25
|
-
* `env.AI.gateway(id).getUrl()`, which is async.
|
|
26
|
-
*/
|
|
27
|
-
client: () => Anthropic | Promise<Anthropic>;
|
|
28
|
-
/** Anthropic model id, e.g. `claude-opus-5`. */
|
|
29
|
-
modelId: string;
|
|
30
|
-
/** `max_tokens` when a caller supplies none. Anthropic requires the field. */
|
|
31
|
-
defaultMaxTokens: number;
|
|
32
|
-
/** Prompt-cache TTL, or `false` to place no breakpoints. Defaults to `"5m"`. */
|
|
33
|
-
cache?: CacheTtl | false;
|
|
34
|
-
/**
|
|
35
|
-
* Reasoning effort, when the caller does not set one per-call.
|
|
36
|
-
*
|
|
37
|
-
* Maps to `output_config.effort`. Coding and agentic work wants `"xhigh"`;
|
|
38
|
-
* `"high"` is the API default. Core never picks this — an agent does.
|
|
39
|
-
*/
|
|
40
|
-
effort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
41
|
-
/** Extra headers merged into every request (AI Gateway metadata lives here). */
|
|
42
|
-
headers?: Record<string, string>;
|
|
43
|
-
/**
|
|
44
|
-
* Recognise a deployment-specific authority in a `401`/`403` body.
|
|
45
|
-
*
|
|
46
|
-
* Consulted before the built-in shapes; return `undefined` to fall through to
|
|
47
|
-
* them. It exists because a deployment may put an authenticated intermediary
|
|
48
|
-
* between the gateway and Anthropic, and only that deployment knows what its
|
|
49
|
-
* refusal looks like — core recognising one particular proxy's error body
|
|
50
|
-
* would be exactly the deployment policy this package does not ship.
|
|
51
|
-
*
|
|
52
|
-
* The remedy is what makes it worth distinguishing at all: a proxy that mints
|
|
53
|
-
* its caller credential per request has no secret to rotate, so reporting its
|
|
54
|
-
* `401` as `credential` sends an operator to replace a working token. See
|
|
55
|
-
* {@link file://../errors.ts CredentialRejectedBy}.
|
|
56
|
-
*/
|
|
57
|
-
classifyAuthFailure?: (body: unknown) => CredentialRejectedBy | undefined;
|
|
58
|
-
}
|
|
59
|
-
export declare function createAnthropicLanguageModel(deps: AnthropicModelDeps): LanguageModelV4;
|