@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/subagent/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateText, isStepCount } from "ai";
|
|
2
2
|
import { CHUNK_SOFT_MS } from "../platform.js";
|
|
3
3
|
import { stepAllowance } from "../agent/budget.js";
|
|
4
|
-
import { isTransientAiError } from "../agent/inference.js";
|
|
4
|
+
import { isTransientAiError, nonRecoverableKind } from "../agent/inference.js";
|
|
5
5
|
import { validateRecipe } from "../contract/validation.js";
|
|
6
6
|
import { renderSubagentPrompt } from "./prompt.js";
|
|
7
7
|
/**
|
|
@@ -200,6 +200,35 @@ function completed(state, deps, text, modelId) {
|
|
|
200
200
|
state
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Fail a chunk on an error no second attempt can clear.
|
|
205
|
+
*
|
|
206
|
+
* Terminal for this subtask, and deliberately *not* a throw: a throw is retried
|
|
207
|
+
* by the Workflow step, which is exactly the spend this classification exists to
|
|
208
|
+
* avoid. The parent round then hits the same condition on its own inference and
|
|
209
|
+
* fails carrying the kind, which is where an operator-facing message gets
|
|
210
|
+
* attached — a subagent has no channel of its own to say "a human must fix
|
|
211
|
+
* this", only this row's `error` string.
|
|
212
|
+
*/
|
|
213
|
+
function nonRecoverableOutcome(state, deps, modelId, kind, diagnostic) {
|
|
214
|
+
console.error("[recipe-runner] non-recoverable model failure", {
|
|
215
|
+
model: modelId,
|
|
216
|
+
kind,
|
|
217
|
+
diagnostic
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
outcome: {
|
|
221
|
+
done: true,
|
|
222
|
+
result: {
|
|
223
|
+
status: "failed",
|
|
224
|
+
error: `${kind}: ${diagnostic}`,
|
|
225
|
+
modelId
|
|
226
|
+
},
|
|
227
|
+
progress: deps.progress
|
|
228
|
+
},
|
|
229
|
+
state
|
|
230
|
+
};
|
|
231
|
+
}
|
|
203
232
|
/**
|
|
204
233
|
* Run one durable chunk. Returns a terminal result (natural completion, budget
|
|
205
234
|
* exhaustion, or exhausted models) or a `done: false` yield with the progress
|
|
@@ -263,8 +292,11 @@ export async function runResumableChunk(prev, deps) {
|
|
|
263
292
|
// The run-wide deadline. Without it the entry guard would only observe the
|
|
264
293
|
// deadline at the next chunk boundary, up to `chunkSoftMs` past it.
|
|
265
294
|
() => deps.now() - state.startedAtMs >= deps.limits.maxWallMs,
|
|
266
|
-
// Not a budget: the
|
|
267
|
-
// checkpoint and hand back a fresh step
|
|
295
|
+
// Not a budget: the configured step timeout, which needs this chunk to
|
|
296
|
+
// checkpoint and hand back a fresh step before it trips. Soft, and only
|
|
297
|
+
// checked here between turns — the turn already in flight when it trips still
|
|
298
|
+
// runs to completion, which is why `STEP_TIMEOUT_MS - CHUNK_SOFT_MS` is sized
|
|
299
|
+
// to cover a whole turn rather than a nominal moment. See `platform.ts`.
|
|
268
300
|
() => deps.now() - chunkStartMs >= deps.chunkSoftMs,
|
|
269
301
|
// Not a budget either: publish progress to the user promptly.
|
|
270
302
|
() => deps.progress.length > 0
|
|
@@ -280,8 +312,9 @@ export async function runResumableChunk(prev, deps) {
|
|
|
280
312
|
tools: deps.tools,
|
|
281
313
|
stopWhen: boundaries(),
|
|
282
314
|
maxOutputTokens: deps.maxOutputTokens,
|
|
283
|
-
//
|
|
284
|
-
|
|
315
|
+
// Not a duplicate of the fallback: the fallback answers "this model
|
|
316
|
+
// cannot do it", and a 429 says "not yet". See `ModelConfig.maxRetries`.
|
|
317
|
+
maxRetries: deps.maxRetries,
|
|
285
318
|
abortSignal: deps.abortSignal,
|
|
286
319
|
onStepEnd
|
|
287
320
|
});
|
|
@@ -298,9 +331,9 @@ export async function runResumableChunk(prev, deps) {
|
|
|
298
331
|
return { kind: "aborted" };
|
|
299
332
|
if (result.finishReason === "length") {
|
|
300
333
|
// Its own warning, not just a diagnostic string: hitting the output ceiling
|
|
301
|
-
// is a tuning signal about
|
|
302
|
-
// producing bad output, and the two are indistinguishable once folded
|
|
303
|
-
// the "recipe exhausted" message.
|
|
334
|
+
// is a tuning signal about `config.model.maxOutputTokens`, distinct from the
|
|
335
|
+
// model producing bad output, and the two are indistinguishable once folded
|
|
336
|
+
// into the "recipe exhausted" message.
|
|
304
337
|
console.warn("[recipe-runner] model output truncated", {
|
|
305
338
|
model: modelId,
|
|
306
339
|
maxOutputTokens: deps.maxOutputTokens
|
|
@@ -325,6 +358,14 @@ export async function runResumableChunk(prev, deps) {
|
|
|
325
358
|
if (a.kind === "aborted")
|
|
326
359
|
return yielded();
|
|
327
360
|
if (a.kind === "failed") {
|
|
361
|
+
// Checked before the fallback, not after: the second slot would present the
|
|
362
|
+
// same rejected credential. Returned rather than thrown — a throw here is
|
|
363
|
+
// retried by the Workflow step, which is the other cost this avoids. The
|
|
364
|
+
// chunk fails, and the parent's next round classifies it properly.
|
|
365
|
+
const blocked = nonRecoverableKind(a.error);
|
|
366
|
+
if (blocked) {
|
|
367
|
+
return nonRecoverableOutcome(state, deps, a.modelId, blocked, a.diagnostic);
|
|
368
|
+
}
|
|
328
369
|
console.warn("[recipe-runner] primary attempt failed, trying fallback", {
|
|
329
370
|
model: a.modelId,
|
|
330
371
|
diagnostic: a.diagnostic
|
|
@@ -395,7 +436,9 @@ async function summarizeBudget(state, deps) {
|
|
|
395
436
|
messages,
|
|
396
437
|
stopWhen: isStepCount(1),
|
|
397
438
|
maxOutputTokens: deps.maxOutputTokens,
|
|
398
|
-
|
|
439
|
+
// Retries on this model, honouring `retry-after`, before the fallback.
|
|
440
|
+
// See `ModelConfig.maxRetries`.
|
|
441
|
+
maxRetries: deps.maxRetries,
|
|
399
442
|
abortSignal: deps.abortSignal
|
|
400
443
|
});
|
|
401
444
|
}
|
|
@@ -421,6 +464,12 @@ async function summarizeBudget(state, deps) {
|
|
|
421
464
|
if (a.kind === "aborted")
|
|
422
465
|
return yielded();
|
|
423
466
|
if (a.kind === "failed") {
|
|
467
|
+
// Same rule as the work loop: no fallback on a credential the API already
|
|
468
|
+
// rejected. A summary is the cheapest call in the run, but it is not free.
|
|
469
|
+
const blocked = nonRecoverableKind(a.error);
|
|
470
|
+
if (blocked) {
|
|
471
|
+
return nonRecoverableOutcome(state, deps, a.modelId, blocked, a.diagnostic);
|
|
472
|
+
}
|
|
424
473
|
const primaryFailure = a;
|
|
425
474
|
a = await summarize(deps.models.fallback, deps.models.fallbackId());
|
|
426
475
|
if (a.kind === "aborted")
|
|
@@ -473,6 +522,7 @@ export async function runRecipeExecution(request, deps) {
|
|
|
473
522
|
toolOutputWindow: deps.toolOutputWindow,
|
|
474
523
|
reportMetrics: recipe.reportMetrics,
|
|
475
524
|
maxOutputTokens: deps.maxOutputTokens,
|
|
525
|
+
maxRetries: deps.maxRetries,
|
|
476
526
|
now,
|
|
477
527
|
progress: [],
|
|
478
528
|
checkpoint: () => { }
|
|
@@ -20,7 +20,7 @@ export interface ReferenceCatalogEntry extends SubtaskReference {
|
|
|
20
20
|
* reference.
|
|
21
21
|
*
|
|
22
22
|
* This is the single eligibility rule, and it has exactly one caller:
|
|
23
|
-
* {@link file://../turn.ts renderTurnMessages} numbers the messages it accepts
|
|
23
|
+
* {@link file://../round/turn.ts renderTurnMessages} numbers the messages it accepts
|
|
24
24
|
* *and* marks those same messages with their `[ref N]` index, in one pass. One
|
|
25
25
|
* predicate, one walk — the marked messages and the catalog indices cannot drift.
|
|
26
26
|
*
|
package/dist/subtasks/catalog.js
CHANGED
|
@@ -10,7 +10,7 @@ import { sessionText } from "../agent/history.js";
|
|
|
10
10
|
* reference.
|
|
11
11
|
*
|
|
12
12
|
* This is the single eligibility rule, and it has exactly one caller:
|
|
13
|
-
* {@link file://../turn.ts renderTurnMessages} numbers the messages it accepts
|
|
13
|
+
* {@link file://../round/turn.ts renderTurnMessages} numbers the messages it accepts
|
|
14
14
|
* *and* marks those same messages with their `[ref N]` index, in one pass. One
|
|
15
15
|
* predicate, one walk — the marked messages and the catalog indices cannot drift.
|
|
16
16
|
*
|
|
@@ -8,16 +8,15 @@ import type { DecompositionProposal, SubtaskDraft } from "./types.js";
|
|
|
8
8
|
* reference catalog it was generated against, this either produces the drafts to
|
|
9
9
|
* persist or throws.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* One invariant lives here: **the model selects references by index only.** It
|
|
12
|
+
* emits catalog indices; this module copies the catalog entry's exact role+text
|
|
13
|
+
* onto the draft. Model output never becomes reference text, so a Subtask cannot
|
|
14
|
+
* carry a rewritten, summarized, or fabricated "quote" of the conversation.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - **The dependency graph is a validated DAG.** Unknown, duplicate,
|
|
18
|
-
* self-referential, and cyclic edges are all rejected here. The data layer's
|
|
19
|
-
* `createDecomposition` re-checks the cheap structural rules as a storage guard,
|
|
20
|
-
* but full cycle detection is this module's job.
|
|
16
|
+
* A proposal's subtasks are **independent of one another**: they all run at once
|
|
17
|
+
* and none can read another's output, so there is no graph to validate and
|
|
18
|
+
* nothing to order. Sequencing is expressed across rounds instead — the main
|
|
19
|
+
* agent delegates, reads the results, and delegates the next step.
|
|
21
20
|
*
|
|
22
21
|
* Invalid output is never repaired: a throw fails the attempt, which falls back to
|
|
23
22
|
* the other model, and two failed attempts fail the parent Task. Silently
|
|
@@ -39,12 +38,11 @@ export declare class DecompositionValidationError extends Error {
|
|
|
39
38
|
* constraint reaches the model as part of the tool schema, on every field, with
|
|
40
39
|
* no per-field `.describe()` needed to restate it.
|
|
41
40
|
*
|
|
42
|
-
* Being shown a rule is not the same as being held to it
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* own schema before the round uses it.
|
|
41
|
+
* Being shown a rule is not the same as being held to it. This schema reaches
|
|
42
|
+
* the model through a **control** tool, which has no `execute`, so the SDK never
|
|
43
|
+
* validates its input — what enforces the constraint is
|
|
44
|
+
* {@link file://../agent/control.ts control.ts}, which parses every control call
|
|
45
|
+
* with the tool's own schema before the round uses it.
|
|
48
46
|
*/
|
|
49
47
|
export declare const nonBlank: (label: string) => z.ZodString;
|
|
50
48
|
/**
|
|
@@ -63,13 +61,11 @@ export declare const nonBlank: (label: string) => z.ZodString;
|
|
|
63
61
|
export declare function makeDecompositionProposalSchema(types: SubtaskTypeRegistry, maxSubtasks: number): z.ZodObject<{
|
|
64
62
|
reply: z.ZodString;
|
|
65
63
|
subtasks: z.ZodArray<z.ZodObject<{
|
|
66
|
-
localKey: z.ZodString;
|
|
67
64
|
type: z.ZodEnum<{
|
|
68
65
|
[x: string]: string;
|
|
69
66
|
}>;
|
|
70
67
|
prompt: z.ZodString;
|
|
71
68
|
referenceIndexes: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
72
|
-
dependsOn: z.ZodArray<z.ZodString>;
|
|
73
69
|
params: z.ZodOptional<z.ZodObject<{
|
|
74
70
|
[x: string]: z.ZodType<string | undefined, unknown, z.core.$ZodTypeInternals<string | undefined, unknown>>;
|
|
75
71
|
}, z.core.$strip>>;
|
|
@@ -79,9 +75,9 @@ export declare function makeDecompositionProposalSchema(types: SubtaskTypeRegist
|
|
|
79
75
|
* Resolve a validated model proposal into the drafts to persist.
|
|
80
76
|
*
|
|
81
77
|
* Throws {@link DecompositionValidationError} on any structural problem: blank
|
|
82
|
-
* fields,
|
|
83
|
-
*
|
|
84
|
-
*
|
|
78
|
+
* fields, unknown or duplicate reference indices, and params a type refuses. On
|
|
79
|
+
* success, array order is preserved — the data layer derives each Subtask's
|
|
80
|
+
* `ordinal` from it.
|
|
85
81
|
*/
|
|
86
82
|
export declare function resolveDecomposition(proposal: DecompositionProposal, catalog: ReferenceCatalogEntry[], types: SubtaskTypeRegistry): {
|
|
87
83
|
reply: string;
|
|
@@ -6,16 +6,15 @@ import { SubtaskParamsError } from "./subtask-types.js";
|
|
|
6
6
|
* reference catalog it was generated against, this either produces the drafts to
|
|
7
7
|
* persist or throws.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* One invariant lives here: **the model selects references by index only.** It
|
|
10
|
+
* emits catalog indices; this module copies the catalog entry's exact role+text
|
|
11
|
+
* onto the draft. Model output never becomes reference text, so a Subtask cannot
|
|
12
|
+
* carry a rewritten, summarized, or fabricated "quote" of the conversation.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - **The dependency graph is a validated DAG.** Unknown, duplicate,
|
|
16
|
-
* self-referential, and cyclic edges are all rejected here. The data layer's
|
|
17
|
-
* `createDecomposition` re-checks the cheap structural rules as a storage guard,
|
|
18
|
-
* but full cycle detection is this module's job.
|
|
14
|
+
* A proposal's subtasks are **independent of one another**: they all run at once
|
|
15
|
+
* and none can read another's output, so there is no graph to validate and
|
|
16
|
+
* nothing to order. Sequencing is expressed across rounds instead — the main
|
|
17
|
+
* agent delegates, reads the results, and delegates the next step.
|
|
19
18
|
*
|
|
20
19
|
* Invalid output is never repaired: a throw fails the attempt, which falls back to
|
|
21
20
|
* the other model, and two failed attempts fail the parent Task. Silently
|
|
@@ -40,12 +39,11 @@ export class DecompositionValidationError extends Error {
|
|
|
40
39
|
* constraint reaches the model as part of the tool schema, on every field, with
|
|
41
40
|
* no per-field `.describe()` needed to restate it.
|
|
42
41
|
*
|
|
43
|
-
* Being shown a rule is not the same as being held to it
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* own schema before the round uses it.
|
|
42
|
+
* Being shown a rule is not the same as being held to it. This schema reaches
|
|
43
|
+
* the model through a **control** tool, which has no `execute`, so the SDK never
|
|
44
|
+
* validates its input — what enforces the constraint is
|
|
45
|
+
* {@link file://../agent/control.ts control.ts}, which parses every control call
|
|
46
|
+
* with the tool's own schema before the round uses it.
|
|
49
47
|
*/
|
|
50
48
|
export const nonBlank = (label) => z
|
|
51
49
|
.string()
|
|
@@ -53,13 +51,11 @@ export const nonBlank = (label) => z
|
|
|
53
51
|
.regex(/\S/, { message: `${label} must not be blank` });
|
|
54
52
|
function makeSubtaskProposalSchema(types) {
|
|
55
53
|
return z.object({
|
|
56
|
-
localKey: nonBlank("localKey"),
|
|
57
54
|
// A closed enum, not prose: an invented type is rejected by the tool schema
|
|
58
55
|
// itself rather than silently resolving to some default recipe.
|
|
59
56
|
type: z.enum(types.enumKeys()),
|
|
60
57
|
prompt: nonBlank("prompt"),
|
|
61
58
|
referenceIndexes: z.array(z.number().int().min(1)).optional(),
|
|
62
|
-
dependsOn: z.array(nonBlank("dependsOn entry")),
|
|
63
59
|
/**
|
|
64
60
|
* The type's required inputs — ids the model quotes from a tool result.
|
|
65
61
|
* Every key any type declares is named here, gathered from those types by
|
|
@@ -94,45 +90,27 @@ export function makeDecompositionProposalSchema(types, maxSubtasks) {
|
|
|
94
90
|
subtasks: z.array(makeSubtaskProposalSchema(types)).min(1).max(maxSubtasks)
|
|
95
91
|
});
|
|
96
92
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Reject an edge set that contains a cycle, by Kahn's algorithm over the
|
|
99
|
-
* draft-local keys: repeatedly remove nodes with no unresolved prerequisites; if
|
|
100
|
-
* any node survives, it is part of (or downstream of) a cycle.
|
|
101
|
-
*/
|
|
102
|
-
function assertAcyclic(proposal) {
|
|
103
|
-
const remaining = new Map(proposal.subtasks.map((s) => [s.localKey, new Set(s.dependsOn)]));
|
|
104
|
-
let progressed = true;
|
|
105
|
-
while (progressed && remaining.size > 0) {
|
|
106
|
-
progressed = false;
|
|
107
|
-
for (const [key, deps] of remaining) {
|
|
108
|
-
// Ready when every prerequisite has already been removed.
|
|
109
|
-
if ([...deps].every((d) => !remaining.has(d))) {
|
|
110
|
-
remaining.delete(key);
|
|
111
|
-
progressed = true;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (remaining.size > 0) {
|
|
116
|
-
throw new DecompositionValidationError(`dependency cycle among subtasks: ${[...remaining.keys()].join(", ")}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
93
|
/**
|
|
120
94
|
* Snapshot the selected catalog entries onto a draft: validate every index against
|
|
121
95
|
* the catalog, reject duplicates, and copy each entry's exact role+text. Indexes
|
|
122
96
|
* are stored ascending so a Subtask's references read in conversation order
|
|
123
97
|
* regardless of the order the model listed them.
|
|
98
|
+
*
|
|
99
|
+
* `label` names the offending subtask by its 1-based position in the proposal —
|
|
100
|
+
* the only handle a subtask has, and the one the model can map back to what it
|
|
101
|
+
* just wrote.
|
|
124
102
|
*/
|
|
125
|
-
function resolveReferences(
|
|
103
|
+
function resolveReferences(label, referenceIndexes, catalog) {
|
|
126
104
|
if (!referenceIndexes)
|
|
127
105
|
return [];
|
|
128
106
|
const seen = new Set();
|
|
129
107
|
for (const index of referenceIndexes) {
|
|
130
108
|
if (seen.has(index)) {
|
|
131
|
-
throw new DecompositionValidationError(
|
|
109
|
+
throw new DecompositionValidationError(`${label} references index ${index} more than once`);
|
|
132
110
|
}
|
|
133
111
|
seen.add(index);
|
|
134
112
|
if (index > catalog.length) {
|
|
135
|
-
throw new DecompositionValidationError(
|
|
113
|
+
throw new DecompositionValidationError(`${label} references unknown catalog index ${index} ` +
|
|
136
114
|
`(catalog has ${catalog.length} ${catalog.length === 1 ? "entry" : "entries"})`);
|
|
137
115
|
}
|
|
138
116
|
}
|
|
@@ -148,37 +126,13 @@ function resolveReferences(localKey, referenceIndexes, catalog) {
|
|
|
148
126
|
* Resolve a validated model proposal into the drafts to persist.
|
|
149
127
|
*
|
|
150
128
|
* Throws {@link DecompositionValidationError} on any structural problem: blank
|
|
151
|
-
* fields,
|
|
152
|
-
*
|
|
153
|
-
*
|
|
129
|
+
* fields, unknown or duplicate reference indices, and params a type refuses. On
|
|
130
|
+
* success, array order is preserved — the data layer derives each Subtask's
|
|
131
|
+
* `ordinal` from it.
|
|
154
132
|
*/
|
|
155
133
|
export function resolveDecomposition(proposal, catalog, types) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
if (keys.has(s.localKey)) {
|
|
159
|
-
throw new DecompositionValidationError(`duplicate subtask local key: ${s.localKey}`);
|
|
160
|
-
}
|
|
161
|
-
keys.add(s.localKey);
|
|
162
|
-
}
|
|
163
|
-
// Every key must be registered before any edge is checked: an edge may point
|
|
164
|
-
// forward to a subtask defined later in the array.
|
|
165
|
-
for (const s of proposal.subtasks) {
|
|
166
|
-
const seen = new Set();
|
|
167
|
-
for (const dep of s.dependsOn) {
|
|
168
|
-
if (dep === s.localKey) {
|
|
169
|
-
throw new DecompositionValidationError(`subtask ${s.localKey} depends on itself`);
|
|
170
|
-
}
|
|
171
|
-
if (!keys.has(dep)) {
|
|
172
|
-
throw new DecompositionValidationError(`subtask ${s.localKey} depends on unknown key: ${dep}`);
|
|
173
|
-
}
|
|
174
|
-
if (seen.has(dep)) {
|
|
175
|
-
throw new DecompositionValidationError(`subtask ${s.localKey} depends on ${dep} more than once`);
|
|
176
|
-
}
|
|
177
|
-
seen.add(dep);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
assertAcyclic(proposal);
|
|
181
|
-
const drafts = proposal.subtasks.map((s) => {
|
|
134
|
+
const drafts = proposal.subtasks.map((s, index) => {
|
|
135
|
+
const label = `subtask ${index + 1}`;
|
|
182
136
|
const type = s.type.trim();
|
|
183
137
|
let params;
|
|
184
138
|
try {
|
|
@@ -189,14 +143,12 @@ export function resolveDecomposition(proposal, catalog, types) {
|
|
|
189
143
|
catch (err) {
|
|
190
144
|
if (!(err instanceof SubtaskParamsError))
|
|
191
145
|
throw err;
|
|
192
|
-
throw new DecompositionValidationError(
|
|
146
|
+
throw new DecompositionValidationError(`${label}: ${err.message}`);
|
|
193
147
|
}
|
|
194
148
|
return {
|
|
195
|
-
localKey: s.localKey,
|
|
196
149
|
type,
|
|
197
150
|
prompt: s.prompt.trim(),
|
|
198
|
-
references: resolveReferences(
|
|
199
|
-
dependsOn: [...s.dependsOn],
|
|
151
|
+
references: resolveReferences(label, s.referenceIndexes, catalog),
|
|
200
152
|
params
|
|
201
153
|
};
|
|
202
154
|
});
|
|
@@ -41,8 +41,22 @@ export declare function makeDelegateTool(types: SubtaskTypeRegistry, maxSubtasks
|
|
|
41
41
|
/**
|
|
42
42
|
* The call's id, derived from the parent Task and the round that emitted it —
|
|
43
43
|
* deterministic and replay-safe, the same discipline as the Session message ids
|
|
44
|
-
* it sits alongside (see {@link file://../history.ts}). Later rounds rebuild it
|
|
44
|
+
* it sits alongside (see {@link file://../agent/history.ts}). Later rounds rebuild it
|
|
45
45
|
* rather than storing it.
|
|
46
|
+
*
|
|
47
|
+
* **Underscores, not colons, and this is load-bearing.** Unlike a Session message
|
|
48
|
+
* id, this one is sent to a provider as a `tool_use.id`, and Anthropic validates
|
|
49
|
+
* that field against `^[a-zA-Z0-9_-]+$`. The colon-separated form this used to
|
|
50
|
+
* return failed every round from the first delegation onwards — round 0 was fine
|
|
51
|
+
* because the model authors its own ids, and round 1 reconstructs this one, so
|
|
52
|
+
* the request 400d deterministically on both the primary and the fallback until
|
|
53
|
+
* the deterministic join fired. Workers AI never validated the field, which is
|
|
54
|
+
* why it took a Claude-backed agent to surface it.
|
|
55
|
+
*
|
|
56
|
+
* Nothing persists this: both halves of the pair are rebuilt together on every
|
|
57
|
+
* request, so changing the shape needs no migration. See
|
|
58
|
+
* {@link file://../agent/anthropic/prompt.ts providerSafeToolCallId} for the
|
|
59
|
+
* backstop that catches the next one of these.
|
|
46
60
|
*/
|
|
47
61
|
export declare function delegateToolCallId(taskId: string, round: number): string;
|
|
48
62
|
/**
|
|
@@ -71,6 +85,11 @@ export type DelegateSubtaskOutcome = {
|
|
|
71
85
|
* `referenceIndexes` is omitted rather than faked: this round's references were
|
|
72
86
|
* snapshotted verbatim onto the rows when it ran, and the catalog they were
|
|
73
87
|
* chosen from is long gone.
|
|
88
|
+
*
|
|
89
|
+
* A subtask proposal carries no identifier of its own, so the model pairs each
|
|
90
|
+
* entry here with its outcome **by position**: both arrays are built from the
|
|
91
|
+
* same ordinal-ordered `branches`, and the outcome additionally repeats `type`
|
|
92
|
+
* and carries the durable `subtaskId`.
|
|
74
93
|
*/
|
|
75
94
|
export declare function delegateCallInput(reply: string, branches: CompositionBranch[]): DecompositionProposal;
|
|
76
95
|
/** Rebuild one round's call result from its durable rows, in stable ordinal order. */
|
|
@@ -47,23 +47,25 @@ export function makeDelegateTool(types, maxSubtasks) {
|
|
|
47
47
|
/**
|
|
48
48
|
* The call's id, derived from the parent Task and the round that emitted it —
|
|
49
49
|
* deterministic and replay-safe, the same discipline as the Session message ids
|
|
50
|
-
* it sits alongside (see {@link file://../history.ts}). Later rounds rebuild it
|
|
50
|
+
* it sits alongside (see {@link file://../agent/history.ts}). Later rounds rebuild it
|
|
51
51
|
* rather than storing it.
|
|
52
|
-
*/
|
|
53
|
-
export function delegateToolCallId(taskId, round) {
|
|
54
|
-
return `task:${taskId}:round:${round}:delegate`;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* The draft-local key a reconstructed call uses for a durable Subtask.
|
|
58
52
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
53
|
+
* **Underscores, not colons, and this is load-bearing.** Unlike a Session message
|
|
54
|
+
* id, this one is sent to a provider as a `tool_use.id`, and Anthropic validates
|
|
55
|
+
* that field against `^[a-zA-Z0-9_-]+$`. The colon-separated form this used to
|
|
56
|
+
* return failed every round from the first delegation onwards — round 0 was fine
|
|
57
|
+
* because the model authors its own ids, and round 1 reconstructs this one, so
|
|
58
|
+
* the request 400d deterministically on both the primary and the fallback until
|
|
59
|
+
* the deterministic join fired. Workers AI never validated the field, which is
|
|
60
|
+
* why it took a Claude-backed agent to surface it.
|
|
61
|
+
*
|
|
62
|
+
* Nothing persists this: both halves of the pair are rebuilt together on every
|
|
63
|
+
* request, so changing the shape needs no migration. See
|
|
64
|
+
* {@link file://../agent/anthropic/prompt.ts providerSafeToolCallId} for the
|
|
65
|
+
* backstop that catches the next one of these.
|
|
64
66
|
*/
|
|
65
|
-
function
|
|
66
|
-
return `
|
|
67
|
+
export function delegateToolCallId(taskId, round) {
|
|
68
|
+
return `task_${taskId}_round_${round}_delegate`;
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
69
71
|
* Rebuild one round's call input from its durable rows, in stable ordinal order.
|
|
@@ -74,15 +76,18 @@ function localKeyForId(id) {
|
|
|
74
76
|
* `referenceIndexes` is omitted rather than faked: this round's references were
|
|
75
77
|
* snapshotted verbatim onto the rows when it ran, and the catalog they were
|
|
76
78
|
* chosen from is long gone.
|
|
79
|
+
*
|
|
80
|
+
* A subtask proposal carries no identifier of its own, so the model pairs each
|
|
81
|
+
* entry here with its outcome **by position**: both arrays are built from the
|
|
82
|
+
* same ordinal-ordered `branches`, and the outcome additionally repeats `type`
|
|
83
|
+
* and carries the durable `subtaskId`.
|
|
77
84
|
*/
|
|
78
85
|
export function delegateCallInput(reply, branches) {
|
|
79
86
|
return {
|
|
80
87
|
reply,
|
|
81
88
|
subtasks: branches.map((branch) => ({
|
|
82
|
-
localKey: localKeyForId(branch.subtaskId),
|
|
83
89
|
type: branch.type,
|
|
84
90
|
prompt: branch.prompt,
|
|
85
|
-
dependsOn: branch.dependsOn.map(localKeyForId),
|
|
86
91
|
// Reconstructed verbatim from the row: a later round must see the same
|
|
87
92
|
// params the call really carried, or it cannot reason about what ran.
|
|
88
93
|
params: branch.params
|
package/dist/subtasks/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@loopingai/core/subtasks` — the delegation layer: what a round may hand out,
|
|
3
|
-
* how a proposal becomes durable drafts
|
|
3
|
+
* and how a proposal becomes durable drafts.
|
|
4
4
|
*/
|
|
5
5
|
export { makeSubtaskTypes, SubtaskParamsError, type SubtaskTypeRegistry } from "./subtask-types.js";
|
|
6
6
|
export { DecompositionValidationError, makeDecompositionProposalSchema, nonBlank, resolveDecomposition } from "./decomposition.js";
|
|
7
7
|
export { DELEGATE_TOOL_NAME, makeDelegateTool, delegateToolCallId, delegateCallInput, delegateCallOutput, type DelegateSubtaskOutcome } from "./delegate.js";
|
|
8
|
-
export { selectWave, type WaveDecision } from "./scheduler.js";
|
|
9
8
|
export { isCatalogEligible, type ReferenceCatalogEntry } from "./catalog.js";
|
|
10
9
|
export * from "./types.js";
|
package/dist/subtasks/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@loopingai/core/subtasks` — the delegation layer: what a round may hand out,
|
|
3
|
-
* how a proposal becomes durable drafts
|
|
3
|
+
* and how a proposal becomes durable drafts.
|
|
4
4
|
*/
|
|
5
5
|
export { makeSubtaskTypes, SubtaskParamsError } from "./subtask-types.js";
|
|
6
6
|
export { DecompositionValidationError, makeDecompositionProposalSchema, nonBlank, resolveDecomposition } from "./decomposition.js";
|
|
7
7
|
export { DELEGATE_TOOL_NAME, makeDelegateTool, delegateToolCallId, delegateCallInput, delegateCallOutput } from "./delegate.js";
|
|
8
|
-
export { selectWave } from "./scheduler.js";
|
|
9
8
|
export { isCatalogEligible } from "./catalog.js";
|
|
10
9
|
export * from "./types.js";
|
|
@@ -78,14 +78,6 @@ export interface SubtaskTypeRegistry {
|
|
|
78
78
|
paramProperties(): Record<string, z.ZodType<string | undefined>>;
|
|
79
79
|
/** The type catalogue as the delegating model is shown it. */
|
|
80
80
|
renderTypes(): string;
|
|
81
|
-
/**
|
|
82
|
-
* Every type's {@link SubtaskTypeSpec.capability}, for the main agent's soul.
|
|
83
|
-
*
|
|
84
|
-
* Returns `""` when no type declares one, and that case is worth naming: it is
|
|
85
|
-
* what lets each call site append unconditionally instead of emitting a
|
|
86
|
-
* separator around nothing.
|
|
87
|
-
*/
|
|
88
|
-
renderCapabilities(): string;
|
|
89
81
|
/** Every type's {@link SubtaskTypeSpec.delegationGuidance}, for the round contract. */
|
|
90
82
|
renderDelegationGuidance(names: DelegationNames): string;
|
|
91
83
|
}
|
|
@@ -91,13 +91,6 @@ export function makeSubtaskTypes(specs) {
|
|
|
91
91
|
})
|
|
92
92
|
.join("\n");
|
|
93
93
|
},
|
|
94
|
-
renderCapabilities() {
|
|
95
|
-
const blocks = [];
|
|
96
|
-
for (const s of specs)
|
|
97
|
-
if (s.capability)
|
|
98
|
-
blocks.push(s.capability);
|
|
99
|
-
return blocks.join("\n\n");
|
|
100
|
-
},
|
|
101
94
|
renderDelegationGuidance(names) {
|
|
102
95
|
const sections = [];
|
|
103
96
|
for (const s of specs) {
|