@loopingai/core 0.5.1 → 0.6.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 +6 -4
- package/dist/a2a/notify.d.ts +4 -3
- package/dist/a2a/notify.js +4 -3
- package/dist/agent/anthropic/index.d.ts +15 -0
- package/dist/agent/anthropic/index.js +19 -0
- package/dist/agent/anthropic/language-model.d.ts +59 -0
- package/dist/agent/anthropic/language-model.js +442 -0
- package/dist/agent/anthropic/prompt.d.ts +84 -0
- package/dist/agent/anthropic/prompt.js +541 -0
- package/dist/agent/anthropic/runtime.d.ts +79 -0
- package/dist/agent/anthropic/runtime.js +130 -0
- package/dist/agent/control.js +10 -9
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +14 -13
- package/dist/agent/final-reply.js +28 -11
- package/dist/agent/history.d.ts +3 -3
- package/dist/agent/history.js +2 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +4 -2
- package/dist/agent/inference.d.ts +58 -1
- package/dist/agent/inference.js +44 -0
- package/dist/agent/model.d.ts +42 -25
- package/dist/agent/model.js +1 -48
- package/dist/agent/session.d.ts +6 -7
- package/dist/agent/session.js +3 -3
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/config.d.ts +49 -15
- package/dist/config.js +30 -1
- package/dist/contract/plugin.d.ts +63 -3
- package/dist/contract/plugin.js +76 -0
- package/dist/contract/recipe.d.ts +16 -17
- package/dist/db/db.d.ts +0 -1
- package/dist/db/migrations/index.js +8 -1
- package/dist/db/models/subtasks.d.ts +24 -25
- package/dist/db/models/subtasks.js +33 -76
- package/dist/db/schema.d.ts +2 -21
- package/dist/db/schema.js +2 -4
- package/dist/host/agent.d.ts +58 -4
- package/dist/host/agent.js +63 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/platform.d.ts +74 -11
- package/dist/platform.js +76 -13
- package/dist/round/agent.d.ts +36 -31
- package/dist/round/agent.js +61 -89
- package/dist/round/index.d.ts +3 -2
- package/dist/round/index.js +2 -2
- package/dist/round/policy.d.ts +2 -2
- package/dist/round/subagent.d.ts +19 -1
- package/dist/round/subagent.js +22 -5
- package/dist/round/turn.d.ts +32 -13
- package/dist/round/turn.js +83 -16
- package/dist/round/workflow.d.ts +23 -7
- package/dist/round/workflow.js +132 -65
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +6 -0
- package/dist/subagent/fingerprint.d.ts +2 -2
- package/dist/subagent/fingerprint.js +8 -17
- package/dist/subagent/index.d.ts +6 -4
- package/dist/subagent/index.js +8 -6
- package/dist/subagent/prompt.d.ts +4 -5
- package/dist/subagent/prompt.js +0 -8
- package/dist/subagent/run.d.ts +8 -1
- package/dist/subagent/run.js +59 -9
- package/dist/subtasks/catalog.d.ts +1 -1
- package/dist/subtasks/catalog.js +1 -1
- package/dist/subtasks/decomposition.d.ts +16 -20
- package/dist/subtasks/decomposition.js +27 -75
- package/dist/subtasks/delegate.d.ts +20 -1
- package/dist/subtasks/delegate.js +21 -16
- package/dist/subtasks/index.d.ts +1 -2
- package/dist/subtasks/index.js +1 -2
- package/dist/subtasks/subtask-types.d.ts +0 -8
- package/dist/subtasks/subtask-types.js +0 -7
- package/dist/subtasks/types.d.ts +45 -70
- package/dist/testing/mock-model.d.ts +35 -0
- package/dist/testing/mock-model.js +75 -0
- package/dist/testing/vcr-global-setup.d.ts +1 -3
- package/dist/testing/vcr-global-setup.js +1 -3
- package/dist/worker/index.d.ts +5 -12
- package/dist/worker/index.js +5 -12
- package/package.json +19 -1
- package/dist/subtasks/scheduler.d.ts +0 -48
- package/dist/subtasks/scheduler.js +0 -47
package/dist/subtasks/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedRecipe, SubtaskParams } from "../contract/recipe.js";
|
|
2
|
-
|
|
2
|
+
import type { RoundFailureKind } from "../agent/inference.js";
|
|
3
|
+
export type SubtaskStatus = "pending" | "running" | "completed" | "failed" | "canceled";
|
|
3
4
|
/** A per-caller, SQLite-assigned, monotonically increasing Subtask identifier. */
|
|
4
5
|
export type SubtaskId = number;
|
|
5
6
|
/**
|
|
@@ -22,18 +23,15 @@ export interface SubtaskResultPart {
|
|
|
22
23
|
text: string;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
* Creation input for one Subtask, before it is persisted. `
|
|
26
|
-
* draft
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Creation input for one Subtask, before it is persisted. `ordinal` is derived
|
|
27
|
+
* from the draft's position in the decomposition array, which is the only thing
|
|
28
|
+
* that distinguishes one draft of a decomposition from another — they carry no
|
|
29
|
+
* identity of their own until the data layer assigns a {@link SubtaskId}.
|
|
29
30
|
*/
|
|
30
31
|
export interface SubtaskDraft {
|
|
31
|
-
localKey: string;
|
|
32
32
|
type: string;
|
|
33
33
|
prompt: string;
|
|
34
34
|
references: SubtaskReference[];
|
|
35
|
-
/** Draft-local keys of prerequisite drafts. */
|
|
36
|
-
dependsOn: string[];
|
|
37
35
|
/** The type's required inputs, already validated for shape. */
|
|
38
36
|
params: SubtaskParams;
|
|
39
37
|
}
|
|
@@ -93,24 +91,11 @@ export interface SubtaskChunkOutcome {
|
|
|
93
91
|
status: SubtaskStatus;
|
|
94
92
|
progress: ProgressEvent[];
|
|
95
93
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Generated output of one completed prerequisite Subtask, loaded by the parent
|
|
98
|
-
* from its durable row for a dependent's invocation. `type` is the
|
|
99
|
-
* prerequisite's semantic type — used only to label the rendered section;
|
|
100
|
-
* dependency output is always presented as generated, never as conversation
|
|
101
|
-
* evidence.
|
|
102
|
-
*/
|
|
103
|
-
export interface DependencyResult {
|
|
104
|
-
subtaskId: SubtaskId;
|
|
105
|
-
type: string;
|
|
106
|
-
resultParts: SubtaskResultPart[];
|
|
107
|
-
}
|
|
108
94
|
/**
|
|
109
95
|
* RPC-safe input for one isolated `RecipeSubagent` execution, assembled by the
|
|
110
96
|
* parent at execution start: the already-resolved (and code-validated) Recipe,
|
|
111
|
-
* the Subtask's non-session prompt, its verbatim reference snapshots
|
|
112
|
-
*
|
|
113
|
-
* Recipe defensively but never resolves one itself.
|
|
97
|
+
* the Subtask's non-session prompt, and its verbatim reference snapshots. The
|
|
98
|
+
* child re-validates the Recipe defensively but never resolves one itself.
|
|
114
99
|
*/
|
|
115
100
|
export interface RecipeExecutionRequest {
|
|
116
101
|
taskId: string;
|
|
@@ -124,7 +109,6 @@ export interface RecipeExecutionRequest {
|
|
|
124
109
|
recipe: ResolvedRecipe;
|
|
125
110
|
prompt: string;
|
|
126
111
|
references: SubtaskReference[];
|
|
127
|
-
dependencyResults: DependencyResult[];
|
|
128
112
|
/**
|
|
129
113
|
* The Subtask's validated params. Part of the execution's identity — two plays
|
|
130
114
|
* of different games are different work — so this IS fingerprinted, unlike
|
|
@@ -154,11 +138,9 @@ export type RecipeExecutionResult = {
|
|
|
154
138
|
* One Subtask as the delegating model emits it. The model selects references
|
|
155
139
|
* by **catalog index only** — it never emits reference text, and application code
|
|
156
140
|
* snapshots the catalog entry's exact role+text onto the Subtask (see
|
|
157
|
-
* `agent/subtasks/decomposition.ts`).
|
|
158
|
-
* to SQLite-assigned {@link SubtaskId}s by the data layer.
|
|
141
|
+
* `agent/subtasks/decomposition.ts`).
|
|
159
142
|
*/
|
|
160
143
|
export interface SubtaskProposal {
|
|
161
|
-
localKey: string;
|
|
162
144
|
type: string;
|
|
163
145
|
prompt: string;
|
|
164
146
|
/**
|
|
@@ -167,8 +149,6 @@ export interface SubtaskProposal {
|
|
|
167
149
|
* *reconstructed* historical call, whose references were resolved rounds ago.
|
|
168
150
|
*/
|
|
169
151
|
referenceIndexes?: number[];
|
|
170
|
-
/** Draft-local keys of prerequisite proposals. */
|
|
171
|
-
dependsOn: string[];
|
|
172
152
|
/** The type's required inputs; omitted for a type that takes none. */
|
|
173
153
|
params?: SubtaskParams;
|
|
174
154
|
}
|
|
@@ -188,18 +168,19 @@ export interface DecompositionProposal {
|
|
|
188
168
|
*
|
|
189
169
|
* `replied` is the terminal answer — the round chose to answer the user rather
|
|
190
170
|
* than delegate, and the Workflow delivers it. `delegated` means the round's
|
|
191
|
-
* Subtask rows are durable and
|
|
192
|
-
* begins. `failed` means
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* (mirrors
|
|
171
|
+
* Subtask rows are durable and the Workflow should execute them, after which
|
|
172
|
+
* another round begins. `failed` means the round produced no answer, with
|
|
173
|
+
* {@link RoundFailureKind} carrying why; no Subtask is ever synthesized to cover
|
|
174
|
+
* for it. `canceled` means the caller cancelled during the round: nothing was
|
|
175
|
+
* persisted and nothing was published. Transient platform faults are not
|
|
176
|
+
* results: they throw so the enclosing Workflow step can retry (mirrors
|
|
177
|
+
* {@link RecipeExecutionResult}).
|
|
197
178
|
*
|
|
198
179
|
* `turns` is what this round cost, which the Workflow meters against the Task's
|
|
199
180
|
* budget. This is the **only** type that carries it, and it carries it because the
|
|
200
181
|
* count has to cross an RPC boundary to reach a Workflow in another isolate;
|
|
201
|
-
* everything inside the DO shares one mutable
|
|
202
|
-
* instead. The field is attached in a single place — see `runTaskTurn` — so no
|
|
182
|
+
* everything inside the DO shares one mutable
|
|
183
|
+
* {@link file://../agent/budget.ts TurnBudget} instead. The field is attached in a single place — see `runTaskTurn` — so no
|
|
203
184
|
* branch can drop it and no branch can invent it.
|
|
204
185
|
*
|
|
205
186
|
* The idempotent recovery paths — a round replayed from durable rows rather than
|
|
@@ -220,8 +201,17 @@ export type TurnTaskResult = {
|
|
|
220
201
|
reply: string;
|
|
221
202
|
subtasks: Subtask[];
|
|
222
203
|
turns: number;
|
|
223
|
-
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* `kind` distinguishes a round that spent both models and got nothing usable
|
|
207
|
+
* (`exhausted`) from one that stopped on a fault no attempt could clear —
|
|
208
|
+
* where the fallback was deliberately *not* tried, and the kind is what lets
|
|
209
|
+
* the host say why in words an operator can act on. Same terminal Task either
|
|
210
|
+
* way; only the words differ.
|
|
211
|
+
*/
|
|
212
|
+
| {
|
|
224
213
|
status: "failed";
|
|
214
|
+
kind: RoundFailureKind;
|
|
225
215
|
error: string;
|
|
226
216
|
turns: number;
|
|
227
217
|
} | {
|
|
@@ -243,15 +233,13 @@ export type TurnVerdict = WithoutTurns<TurnTaskResult>;
|
|
|
243
233
|
/**
|
|
244
234
|
* One branch's outcome as a later round sees it — a plain, RPC-safe subset of the
|
|
245
235
|
* durable {@link Subtask} row, loaded across **every** round in stable ordinal
|
|
246
|
-
* order. Completed
|
|
247
|
-
*
|
|
236
|
+
* order. Completed and failed branches are both included so the reply can use
|
|
237
|
+
* available successes and disclose relevant failures.
|
|
248
238
|
*
|
|
249
|
-
* Carries `round
|
|
250
|
-
*
|
|
251
|
-
* `agent/subtasks/delegate.ts`).
|
|
252
|
-
*
|
|
253
|
-
* that keeps the scheduler's view narrow does not apply. `references` still stays
|
|
254
|
-
* out: it is unbounded history text, and the call's shape does not need it.
|
|
239
|
+
* Carries `round` and `prompt` — not for composing, but for reconstructing the
|
|
240
|
+
* per-round `delegate` call that produced these branches (see
|
|
241
|
+
* `agent/subtasks/delegate.ts`). `references` stays out: it is unbounded history
|
|
242
|
+
* text, and the call's shape does not need it.
|
|
255
243
|
*/
|
|
256
244
|
export interface CompositionBranch {
|
|
257
245
|
subtaskId: SubtaskId;
|
|
@@ -259,41 +247,29 @@ export interface CompositionBranch {
|
|
|
259
247
|
ordinal: number;
|
|
260
248
|
type: string;
|
|
261
249
|
prompt: string;
|
|
262
|
-
dependsOn: SubtaskId[];
|
|
263
250
|
params: SubtaskParams;
|
|
264
251
|
status: SubtaskStatus;
|
|
265
252
|
resultParts: SubtaskResultPart[] | null;
|
|
266
253
|
error: string | null;
|
|
267
254
|
}
|
|
268
255
|
/**
|
|
269
|
-
* The `scan:<
|
|
270
|
-
*
|
|
271
|
-
* the
|
|
272
|
-
* round trip, and no gap between asking and acting.
|
|
256
|
+
* The `scan:<round>` projection (RPC-safe): either the caller cancelled, or the
|
|
257
|
+
* ids of the round's Subtasks that still owe an outcome, in ordinal order.
|
|
258
|
+
* Returning the verdict with the ids is what lets the Workflow drop its separate
|
|
259
|
+
* cancellation probe — one round trip, and no gap between asking and acting.
|
|
260
|
+
*
|
|
261
|
+
* Ids and nothing else, deliberately: a Workflow step return is capped at 1 MiB
|
|
262
|
+
* and a Subtask carries verbatim history snapshots bounded only by
|
|
263
|
+
* `MAX_INBOUND_TEXT_BYTES`, so a scan returning rows would overflow on a large
|
|
264
|
+
* task. The durable rows are the source of truth; the Workflow carries
|
|
265
|
+
* references to them and re-reads through the parent when it needs more.
|
|
273
266
|
*/
|
|
274
267
|
export type SubtaskScan = {
|
|
275
268
|
canceled: true;
|
|
276
269
|
} | {
|
|
277
270
|
canceled: false;
|
|
278
|
-
|
|
271
|
+
ids: SubtaskId[];
|
|
279
272
|
};
|
|
280
|
-
/**
|
|
281
|
-
* The scheduler's view of one Subtask (Phase 2) — everything needed to pick the
|
|
282
|
-
* next dependency-ready wave, and nothing else.
|
|
283
|
-
*
|
|
284
|
-
* Deliberately excludes `prompt`, `references`, and `resultParts`: a Workflow
|
|
285
|
-
* step return is capped at 1 MiB, and a reference is a verbatim history snapshot
|
|
286
|
-
* bounded only by `MAX_INBOUND_TEXT_BYTES`, so a wave scan returning full rows
|
|
287
|
-
* would overflow on a large task. The durable rows — not Workflow state — are the
|
|
288
|
-
* source of truth, so the Workflow carries ids and statuses and re-reads the rest
|
|
289
|
-
* through the parent when it actually needs it.
|
|
290
|
-
*/
|
|
291
|
-
export interface SubtaskNode {
|
|
292
|
-
id: SubtaskId;
|
|
293
|
-
ordinal: number;
|
|
294
|
-
status: SubtaskStatus;
|
|
295
|
-
dependsOn: SubtaskId[];
|
|
296
|
-
}
|
|
297
273
|
/** Durable state owned by the main agent for one delegated unit of work. */
|
|
298
274
|
export interface Subtask {
|
|
299
275
|
id: SubtaskId;
|
|
@@ -307,7 +283,6 @@ export interface Subtask {
|
|
|
307
283
|
recipeVersion: number | null;
|
|
308
284
|
prompt: string;
|
|
309
285
|
references: SubtaskReference[];
|
|
310
|
-
dependsOn: SubtaskId[];
|
|
311
286
|
/** The type's required inputs, validated at delegation time. */
|
|
312
287
|
params: SubtaskParams;
|
|
313
288
|
status: SubtaskStatus;
|
|
@@ -40,3 +40,38 @@ export declare function finalReply(text: string, opts?: {
|
|
|
40
40
|
* last step.
|
|
41
41
|
*/
|
|
42
42
|
export declare function mockModel(...steps: MockStep[]): MockLanguageModelV3;
|
|
43
|
+
/**
|
|
44
|
+
* A model whose every call throws, and a count of how many times it was asked.
|
|
45
|
+
*
|
|
46
|
+
* The count is the point. Several of the attempt ladder's rules are about a call
|
|
47
|
+
* that must *not* happen — a fallback slot left unspent, a repair not attempted
|
|
48
|
+
* — and those are invisible to an assertion on the returned outcome alone, which
|
|
49
|
+
* can be right for the wrong reason.
|
|
50
|
+
*/
|
|
51
|
+
export declare function throwingModel(error: unknown): {
|
|
52
|
+
model: MockLanguageModelV3;
|
|
53
|
+
calls: () => number;
|
|
54
|
+
};
|
|
55
|
+
/** {@link mockModel}, plus the same call count {@link throwingModel} reports. */
|
|
56
|
+
export declare function countingModel(...steps: MockStep[]): {
|
|
57
|
+
model: MockLanguageModelV3;
|
|
58
|
+
calls: () => number;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A model that fails the first `failures` calls with a retryable `APICallError`,
|
|
62
|
+
* then behaves like {@link mockModel}.
|
|
63
|
+
*
|
|
64
|
+
* Exists for the one behaviour a scripted-outcome assertion cannot see: whether
|
|
65
|
+
* a rate limit was *waited out on the same model* or fell straight through to
|
|
66
|
+
* the fallback. Only the call count distinguishes them — both produce a
|
|
67
|
+
* successful round.
|
|
68
|
+
*
|
|
69
|
+
* `retry-after: 0` is deliberate. The AI SDK honours the header and its own
|
|
70
|
+
* backoff opens at two seconds, which would spend real seconds asserting
|
|
71
|
+
* something that has nothing to do with duration. Zero exercises the identical
|
|
72
|
+
* path — header parsed, preferred over the exponential delay, waited — for free.
|
|
73
|
+
*/
|
|
74
|
+
export declare function rateLimitedModel(failures: number, ...steps: MockStep[]): {
|
|
75
|
+
model: MockLanguageModelV3;
|
|
76
|
+
calls: () => number;
|
|
77
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { MockLanguageModelV3 } from "ai/test";
|
|
2
|
+
// From `ai`, not `@ai-sdk/provider`. `ai` re-exports the class and is a declared
|
|
3
|
+
// peer; reaching for the provider package directly makes this module — which is
|
|
4
|
+
// on the published `/testing` subpath every consumer loads — depend on a package
|
|
5
|
+
// core does not declare and only resolves today by hoisting.
|
|
6
|
+
import { APICallError } from "ai";
|
|
2
7
|
/**
|
|
3
8
|
* Test doubles for the LLM. Lets the tool-loop / executor specs run the real
|
|
4
9
|
* `generateText` machinery (tool execution, multi-step, fallback) against a
|
|
@@ -59,3 +64,73 @@ export function mockModel(...steps) {
|
|
|
59
64
|
doGenerate: async () => stepResult(steps[Math.min(i++, steps.length - 1)])
|
|
60
65
|
});
|
|
61
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* A model whose every call throws, and a count of how many times it was asked.
|
|
69
|
+
*
|
|
70
|
+
* The count is the point. Several of the attempt ladder's rules are about a call
|
|
71
|
+
* that must *not* happen — a fallback slot left unspent, a repair not attempted
|
|
72
|
+
* — and those are invisible to an assertion on the returned outcome alone, which
|
|
73
|
+
* can be right for the wrong reason.
|
|
74
|
+
*/
|
|
75
|
+
export function throwingModel(error) {
|
|
76
|
+
let calls = 0;
|
|
77
|
+
return {
|
|
78
|
+
model: new MockLanguageModelV3({
|
|
79
|
+
doGenerate: async () => {
|
|
80
|
+
calls += 1;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
calls: () => calls
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** {@link mockModel}, plus the same call count {@link throwingModel} reports. */
|
|
88
|
+
export function countingModel(...steps) {
|
|
89
|
+
let calls = 0;
|
|
90
|
+
let i = 0;
|
|
91
|
+
return {
|
|
92
|
+
model: new MockLanguageModelV3({
|
|
93
|
+
doGenerate: async () => {
|
|
94
|
+
calls += 1;
|
|
95
|
+
return stepResult(steps[Math.min(i++, steps.length - 1)]);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
calls: () => calls
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A model that fails the first `failures` calls with a retryable `APICallError`,
|
|
103
|
+
* then behaves like {@link mockModel}.
|
|
104
|
+
*
|
|
105
|
+
* Exists for the one behaviour a scripted-outcome assertion cannot see: whether
|
|
106
|
+
* a rate limit was *waited out on the same model* or fell straight through to
|
|
107
|
+
* the fallback. Only the call count distinguishes them — both produce a
|
|
108
|
+
* successful round.
|
|
109
|
+
*
|
|
110
|
+
* `retry-after: 0` is deliberate. The AI SDK honours the header and its own
|
|
111
|
+
* backoff opens at two seconds, which would spend real seconds asserting
|
|
112
|
+
* something that has nothing to do with duration. Zero exercises the identical
|
|
113
|
+
* path — header parsed, preferred over the exponential delay, waited — for free.
|
|
114
|
+
*/
|
|
115
|
+
export function rateLimitedModel(failures, ...steps) {
|
|
116
|
+
let calls = 0;
|
|
117
|
+
let i = 0;
|
|
118
|
+
return {
|
|
119
|
+
model: new MockLanguageModelV3({
|
|
120
|
+
doGenerate: async () => {
|
|
121
|
+
calls += 1;
|
|
122
|
+
if (calls <= failures) {
|
|
123
|
+
throw new APICallError({
|
|
124
|
+
message: "429 Wholesale Rate limited",
|
|
125
|
+
url: "anthropic:messages:test",
|
|
126
|
+
requestBodyValues: {},
|
|
127
|
+
statusCode: 429,
|
|
128
|
+
responseHeaders: { "retry-after": "0" }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return stepResult(steps[Math.min(i++, steps.length - 1)]);
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
calls: () => calls
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* Belt and braces rather than load-bearing — each cassette is already flushed
|
|
7
7
|
* when its test releases it, so a run that ends normally has nothing left to do
|
|
8
8
|
* here, and a run that dies mid-test keeps everything recorded up to that point.
|
|
9
|
-
*
|
|
10
|
-
* and a self-refreshing auto-flush timer open, and a `RECORD=1` run hung on
|
|
11
|
-
* "close timed out" without it. Neither exists now.)
|
|
9
|
+
* The recorder holds no sockets and no timers, so nothing here has to close.
|
|
12
10
|
*/
|
|
13
11
|
export declare function setup(): void;
|
|
14
12
|
export declare function teardown(): Promise<void>;
|
|
@@ -7,9 +7,7 @@ import { closeVcr } from "./vcr.js";
|
|
|
7
7
|
* Belt and braces rather than load-bearing — each cassette is already flushed
|
|
8
8
|
* when its test releases it, so a run that ends normally has nothing left to do
|
|
9
9
|
* here, and a run that dies mid-test keeps everything recorded up to that point.
|
|
10
|
-
*
|
|
11
|
-
* and a self-refreshing auto-flush timer open, and a `RECORD=1` run hung on
|
|
12
|
-
* "close timed out" without it. Neither exists now.)
|
|
10
|
+
* The recorder holds no sockets and no timers, so nothing here has to close.
|
|
13
11
|
*/
|
|
14
12
|
export function setup() { }
|
|
15
13
|
export async function teardown() {
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -7,18 +7,11 @@ import type { MountedAgent } from "./define-agent.js";
|
|
|
7
7
|
* The A2A Worker: the zero-trust, no-shared-secrets edge every Looping agent
|
|
8
8
|
* puts in front of its Durable Object.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 3. **Verify the gateway's identity JWT** on every JSON-RPC call against the
|
|
16
|
-
* gateway's public JWKS ("R knows G"), then run the A2A JSON-RPC server for
|
|
17
|
-
* that call. {@link A2AExecutor} dispatches into the caller's agent DO — one
|
|
18
|
-
* instance per calling gateway-agent, keyed by the verified `identity.key`.
|
|
19
|
-
*
|
|
20
|
-
* No secret is ever shared in either direction: trust flows entirely on domains
|
|
21
|
-
* and asymmetric (Ed25519) signatures over public JWKS.
|
|
10
|
+
* Trust flows entirely on domains and asymmetric (Ed25519) signatures over
|
|
11
|
+
* public JWKS: a gateway verifies and pins this agent's identity from its signed
|
|
12
|
+
* card at registration ("G knows R"), and this Worker verifies the gateway's
|
|
13
|
+
* identity JWT on every JSON-RPC call ("R knows G"). No secret is ever shared in
|
|
14
|
+
* either direction.
|
|
22
15
|
*
|
|
23
16
|
* **Several agents share one Worker, addressed by `tenant`.** They share one
|
|
24
17
|
* origin, one endpoint, one signing key and one card at the well-known path;
|
package/dist/worker/index.js
CHANGED
|
@@ -12,18 +12,11 @@ import { parseGatewayOrigins } from "../env.js";
|
|
|
12
12
|
* The A2A Worker: the zero-trust, no-shared-secrets edge every Looping agent
|
|
13
13
|
* puts in front of its Durable Object.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 3. **Verify the gateway's identity JWT** on every JSON-RPC call against the
|
|
21
|
-
* gateway's public JWKS ("R knows G"), then run the A2A JSON-RPC server for
|
|
22
|
-
* that call. {@link A2AExecutor} dispatches into the caller's agent DO — one
|
|
23
|
-
* instance per calling gateway-agent, keyed by the verified `identity.key`.
|
|
24
|
-
*
|
|
25
|
-
* No secret is ever shared in either direction: trust flows entirely on domains
|
|
26
|
-
* and asymmetric (Ed25519) signatures over public JWKS.
|
|
15
|
+
* Trust flows entirely on domains and asymmetric (Ed25519) signatures over
|
|
16
|
+
* public JWKS: a gateway verifies and pins this agent's identity from its signed
|
|
17
|
+
* card at registration ("G knows R"), and this Worker verifies the gateway's
|
|
18
|
+
* identity JWT on every JSON-RPC call ("R knows G"). No secret is ever shared in
|
|
19
|
+
* either direction.
|
|
27
20
|
*
|
|
28
21
|
* **Several agents share one Worker, addressed by `tenant`.** They share one
|
|
29
22
|
* origin, one endpoint, one signing key and one card at the well-known path;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopingai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -90,6 +90,10 @@
|
|
|
90
90
|
"import": "./dist/testing/vcr-global-setup.js"
|
|
91
91
|
},
|
|
92
92
|
"./eslint": "./eslint-rules/index.js",
|
|
93
|
+
"./anthropic": {
|
|
94
|
+
"types": "./dist/agent/anthropic/index.d.ts",
|
|
95
|
+
"import": "./dist/agent/anthropic/index.js"
|
|
96
|
+
},
|
|
93
97
|
"./package.json": "./package.json"
|
|
94
98
|
},
|
|
95
99
|
"scripts": {
|
|
@@ -115,21 +119,35 @@
|
|
|
115
119
|
"zod": "^4.4.3"
|
|
116
120
|
},
|
|
117
121
|
"peerDependencies": {
|
|
122
|
+
"@ai-sdk/provider": "^4.0.0",
|
|
123
|
+
"@anthropic-ai/sdk": "^0.116.0",
|
|
118
124
|
"@cloudflare/vitest-pool-workers": ">=0.18",
|
|
125
|
+
"@typescript-eslint/utils": ">=8",
|
|
119
126
|
"agents": "^0.20.0",
|
|
120
127
|
"ai": "^7.0.52",
|
|
121
128
|
"vitest": ">=4",
|
|
122
129
|
"workers-ai-provider": "^4.0.0"
|
|
123
130
|
},
|
|
124
131
|
"peerDependenciesMeta": {
|
|
132
|
+
"@ai-sdk/provider": {
|
|
133
|
+
"optional": true
|
|
134
|
+
},
|
|
135
|
+
"@anthropic-ai/sdk": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
125
138
|
"@cloudflare/vitest-pool-workers": {
|
|
126
139
|
"optional": true
|
|
127
140
|
},
|
|
141
|
+
"@typescript-eslint/utils": {
|
|
142
|
+
"optional": true
|
|
143
|
+
},
|
|
128
144
|
"vitest": {
|
|
129
145
|
"optional": true
|
|
130
146
|
}
|
|
131
147
|
},
|
|
132
148
|
"devDependencies": {
|
|
149
|
+
"@ai-sdk/provider": "^4.0.0",
|
|
150
|
+
"@anthropic-ai/sdk": "^0.116.0",
|
|
133
151
|
"@cloudflare/vitest-pool-workers": "^0.20.1",
|
|
134
152
|
"@types/node": "^26.1.1",
|
|
135
153
|
"agents": "^0.20.0",
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { SubtaskId, SubtaskNode } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* The Phase 2 wave scheduler: given the DAG's current state, decide what runs
|
|
4
|
-
* next.
|
|
5
|
-
*
|
|
6
|
-
* Pure and non-mutating. Skipping nodes blocked by a failed prerequisite is the
|
|
7
|
-
* parent's job (`skipBlockedSubtasks`, which runs to a fixpoint and owns the
|
|
8
|
-
* durable writes); this only reads statuses and picks a wave. The Workflow calls
|
|
9
|
-
* it between durable steps, so it must be a plain function of its input — no I/O,
|
|
10
|
-
* no clock, no randomness.
|
|
11
|
-
*/
|
|
12
|
-
/** What the Workflow should do with the DAG as it currently stands. */
|
|
13
|
-
export type WaveDecision =
|
|
14
|
-
/** These nodes' prerequisites all succeeded — run them concurrently, now. */
|
|
15
|
-
{
|
|
16
|
-
kind: "ready";
|
|
17
|
-
ids: SubtaskId[];
|
|
18
|
-
}
|
|
19
|
-
/** Every node reached a terminal status. Phase 2 is over. */
|
|
20
|
-
| {
|
|
21
|
-
kind: "done";
|
|
22
|
-
}
|
|
23
|
-
/** Non-terminal nodes remain but none can ever run. An invariant violation. */
|
|
24
|
-
| {
|
|
25
|
-
kind: "stuck";
|
|
26
|
-
active: SubtaskId[];
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Select the next wave of dependency-ready Subtasks.
|
|
30
|
-
*
|
|
31
|
-
* A node is ready when it is still active and **every** dependency completed
|
|
32
|
-
* successfully — `completed` specifically, not merely terminal: a dependent must
|
|
33
|
-
* never run on a failed prerequisite's absent output.
|
|
34
|
-
*
|
|
35
|
-
* Ready ids come back in `ordinal` order. The Workflow builds durable step names
|
|
36
|
-
* from them (`execute:<id>`), and step names are a cache key, so the traversal
|
|
37
|
-
* that produces them has to be deterministic.
|
|
38
|
-
*
|
|
39
|
-
* **On `stuck`:** one "active nodes, none ready" check is the whole safety net. A
|
|
40
|
-
* cycle, a self-dependency, and a dangling dependency id all manifest identically
|
|
41
|
-
* here — every node in a cycle waits on a non-completed peer, a self-dependency
|
|
42
|
-
* waits on its own non-completed self, and an unknown id resolves to `undefined`,
|
|
43
|
-
* which is not `completed`. None of them are reachable: `createDecomposition`
|
|
44
|
-
* validates the DAG before it persists a single row. So this is a loud assertion
|
|
45
|
-
* for a corrupted DAG, not a control path — which is exactly why it is one check
|
|
46
|
-
* and not three detectors that can never fire.
|
|
47
|
-
*/
|
|
48
|
-
export declare function selectWave(nodes: SubtaskNode[]): WaveDecision;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Non-terminal statuses — nodes Phase 2 still owes an outcome for.
|
|
3
|
-
*
|
|
4
|
-
* `running` counts as active on purpose. `executeSubtask` accepts a row that is
|
|
5
|
-
* `pending` **or** `running`: the latter is its ambiguous-retry path, where a
|
|
6
|
-
* previous attempt crashed mid-execution and the managed child's fingerprint
|
|
7
|
-
* cache may still hold the terminal result that makes the retry free. So a row
|
|
8
|
-
* stranded `running` is re-runnable, and treating it as inert would deadlock its
|
|
9
|
-
* dependents instead of recovering them.
|
|
10
|
-
*/
|
|
11
|
-
const ACTIVE = new Set([
|
|
12
|
-
"pending",
|
|
13
|
-
"running"
|
|
14
|
-
]);
|
|
15
|
-
/**
|
|
16
|
-
* Select the next wave of dependency-ready Subtasks.
|
|
17
|
-
*
|
|
18
|
-
* A node is ready when it is still active and **every** dependency completed
|
|
19
|
-
* successfully — `completed` specifically, not merely terminal: a dependent must
|
|
20
|
-
* never run on a failed prerequisite's absent output.
|
|
21
|
-
*
|
|
22
|
-
* Ready ids come back in `ordinal` order. The Workflow builds durable step names
|
|
23
|
-
* from them (`execute:<id>`), and step names are a cache key, so the traversal
|
|
24
|
-
* that produces them has to be deterministic.
|
|
25
|
-
*
|
|
26
|
-
* **On `stuck`:** one "active nodes, none ready" check is the whole safety net. A
|
|
27
|
-
* cycle, a self-dependency, and a dangling dependency id all manifest identically
|
|
28
|
-
* here — every node in a cycle waits on a non-completed peer, a self-dependency
|
|
29
|
-
* waits on its own non-completed self, and an unknown id resolves to `undefined`,
|
|
30
|
-
* which is not `completed`. None of them are reachable: `createDecomposition`
|
|
31
|
-
* validates the DAG before it persists a single row. So this is a loud assertion
|
|
32
|
-
* for a corrupted DAG, not a control path — which is exactly why it is one check
|
|
33
|
-
* and not three detectors that can never fire.
|
|
34
|
-
*/
|
|
35
|
-
export function selectWave(nodes) {
|
|
36
|
-
const statusById = new Map(nodes.map((n) => [n.id, n.status]));
|
|
37
|
-
const active = nodes.filter((n) => ACTIVE.has(n.status));
|
|
38
|
-
if (active.length === 0)
|
|
39
|
-
return { kind: "done" };
|
|
40
|
-
const ready = active
|
|
41
|
-
.filter((n) => n.dependsOn.every((d) => statusById.get(d) === "completed"))
|
|
42
|
-
.sort((a, b) => a.ordinal - b.ordinal)
|
|
43
|
-
.map((n) => n.id);
|
|
44
|
-
if (ready.length === 0)
|
|
45
|
-
return { kind: "stuck", active: active.map((n) => n.id) };
|
|
46
|
-
return { kind: "ready", ids: ready };
|
|
47
|
-
}
|