@dynamicagents/core 0.9.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/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- 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/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- 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/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Tool, ToolSet } from "ai";
|
|
2
|
+
import type { ReferenceCatalogEntry } from "../subtasks/catalog.js";
|
|
3
|
+
import type { SubtaskTypeRegistry } from "../subtasks/subtask-types.js";
|
|
4
|
+
import type { SubtaskDraft } from "../subtasks/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The **control tools** — the calls that end a round — and the one thing they all
|
|
7
|
+
* need that the SDK cannot do for them: checking their own input.
|
|
8
|
+
*
|
|
9
|
+
* A work tool carries an `execute`, so the SDK validates its input against the
|
|
10
|
+
* tool's schema, runs it, and feeds any failure — bad input, unknown tool, a throw
|
|
11
|
+
* from inside — back into the loop as a failed tool result. The model sees what
|
|
12
|
+
* went wrong and gets to try again, for free, for every work tool that exists or
|
|
13
|
+
* ever will.
|
|
14
|
+
*
|
|
15
|
+
* A control tool has no `execute`. The call *is* the round's output, so the loop
|
|
16
|
+
* halts on it and none of that machinery runs: **its input is never validated at
|
|
17
|
+
* all**. That is not a subtlety, it is a hole. A `final_reply` whose `text` was
|
|
18
|
+
* blank passed `nonBlank` untouched and was delivered to the user as an empty
|
|
19
|
+
* message; a `delegate` whose payload was not a decomposition at all reached
|
|
20
|
+
* {@link resolveDecomposition} and failed there on a raw `TypeError`.
|
|
21
|
+
*
|
|
22
|
+
* So each control tool declares a {@link ControlTool.parse}, and the round runs it
|
|
23
|
+
* where the SDK would have: between the call and any use of it. `parse` either
|
|
24
|
+
* produces the {@link TurnDecision} or throws, and a throw is handed straight back
|
|
25
|
+
* to the model as a failed result for that call — the same repair loop, from the
|
|
26
|
+
* same shape of feedback, that a work tool gets from the SDK. Adding a control tool
|
|
27
|
+
* means writing its `parse`; it inherits validation and repair by existing.
|
|
28
|
+
*/
|
|
29
|
+
/** What one round decided. A future `escalate` is another variant here. */
|
|
30
|
+
export type TurnDecision = {
|
|
31
|
+
kind: "reply";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "delegate";
|
|
35
|
+
reply: string;
|
|
36
|
+
drafts: SubtaskDraft[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Thrown by a {@link ControlTool.parse} for a call the round cannot use.
|
|
40
|
+
*
|
|
41
|
+
* Its `message` is written for the **model**, not for a log: it is what comes back
|
|
42
|
+
* in the failed tool result, and it is the only thing the next attempt knows about
|
|
43
|
+
* why this one was rejected. Say what was wrong with the call.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ControlCallError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/** One control tool: what the model sees, and how the round reads what comes back. */
|
|
49
|
+
export interface ControlTool {
|
|
50
|
+
/** The tool's name, as declared to the model. */
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** The tool as the model sees it — no `execute`, by definition. */
|
|
53
|
+
readonly tool: Tool;
|
|
54
|
+
/**
|
|
55
|
+
* How this ending competes with another emitted in the same step. The highest
|
|
56
|
+
* precedence among the tools actually called wins, and the rest are dropped.
|
|
57
|
+
*
|
|
58
|
+
* It ranks by **commitment**: `delegate` starts durable work that a reply cannot
|
|
59
|
+
* undo, so a `delegate` alongside a `final_reply` means the reply is the
|
|
60
|
+
* acknowledgment *for* that work, not an answer instead of it. A future
|
|
61
|
+
* `escalate` slots in by saying how committal it is, and nothing else changes.
|
|
62
|
+
*/
|
|
63
|
+
readonly precedence: number;
|
|
64
|
+
/**
|
|
65
|
+
* Pick the one call that counts out of **every** call the attempt made to this
|
|
66
|
+
* tool, or throw a {@link ControlCallError} if a repeat is itself the error.
|
|
67
|
+
*
|
|
68
|
+
* Separate from {@link ControlTool.parse} because what a repeat means is the
|
|
69
|
+
* tool's own business — a second `delegate` would start a second batch of durable
|
|
70
|
+
* work and is refused, while a repeated `final_reply` is just the model restating
|
|
71
|
+
* its answer and the last one stands — and the round needs that answer *before* it
|
|
72
|
+
* has a decision: the call it shows back to the model in a repair has to be the
|
|
73
|
+
* same one validation rejected, not whichever happened to come first.
|
|
74
|
+
*
|
|
75
|
+
* Never called with an empty array: a tool with no calls did not end the round.
|
|
76
|
+
*/
|
|
77
|
+
select(inputs: readonly unknown[]): unknown;
|
|
78
|
+
/**
|
|
79
|
+
* Turn the selected call into the round's decision, or throw a
|
|
80
|
+
* {@link ControlCallError} describing what is wrong with it.
|
|
81
|
+
*
|
|
82
|
+
* The input is `unknown` on purpose. Nothing has checked it yet — that is this
|
|
83
|
+
* method's job, and typing it as anything else would be the assumption that
|
|
84
|
+
* caused the hole this interface exists to close.
|
|
85
|
+
*/
|
|
86
|
+
parse(input: unknown): TurnDecision;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The control tools for one round, in the order the model is shown them.
|
|
90
|
+
*
|
|
91
|
+
* Built per round rather than defined once, because validating a `delegate` needs
|
|
92
|
+
* this round's reference catalog — the same values the model was shown as
|
|
93
|
+
* `[ref N]` markers, which is what makes "reference 4" checkable at all.
|
|
94
|
+
*
|
|
95
|
+
* `delegate` is withheld from a `final` round, which has no budget left to spend on
|
|
96
|
+
* work. `final_reply` is declared on every round including that one: withhold both
|
|
97
|
+
* and the round has no legal way to end.
|
|
98
|
+
*/
|
|
99
|
+
export declare function controlTools(opts: {
|
|
100
|
+
catalog: ReferenceCatalogEntry[];
|
|
101
|
+
/** Whether this round may still hand out work (`false` on a `final` round). */
|
|
102
|
+
delegable: boolean;
|
|
103
|
+
/** The installed subtask types — what `delegate` may name. */
|
|
104
|
+
types: SubtaskTypeRegistry;
|
|
105
|
+
/** `CoreConfig.maxSubtasks`, the per-round fan-out bound. */
|
|
106
|
+
maxSubtasks: number;
|
|
107
|
+
}): ControlTool[];
|
|
108
|
+
/** The tools as the SDK takes them, in declaration order. */
|
|
109
|
+
export declare function controlToolSet(tools: ControlTool[]): ToolSet;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FINAL_REPLY_TOOL_NAME, finalReplyInputSchema, finalReplyTool } from "./final-reply.js";
|
|
2
|
+
import { makeDecompositionProposalSchema, resolveDecomposition } from "../subtasks/decomposition.js";
|
|
3
|
+
import { DELEGATE_TOOL_NAME, makeDelegateTool } from "../subtasks/delegate.js";
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by a {@link ControlTool.parse} for a call the round cannot use.
|
|
6
|
+
*
|
|
7
|
+
* Its `message` is written for the **model**, not for a log: it is what comes back
|
|
8
|
+
* in the failed tool result, and it is the only thing the next attempt knows about
|
|
9
|
+
* why this one was rejected. Say what was wrong with the call.
|
|
10
|
+
*/
|
|
11
|
+
export class ControlCallError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ControlCallError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The control tools for one round, in the order the model is shown them.
|
|
19
|
+
*
|
|
20
|
+
* Built per round rather than defined once, because validating a `delegate` needs
|
|
21
|
+
* this round's reference catalog — the same values the model was shown as
|
|
22
|
+
* `[ref N]` markers, which is what makes "reference 4" checkable at all.
|
|
23
|
+
*
|
|
24
|
+
* `delegate` is withheld from a `final` round, which has no budget left to spend on
|
|
25
|
+
* work. `final_reply` is declared on every round including that one: withhold both
|
|
26
|
+
* and the round has no legal way to end.
|
|
27
|
+
*/
|
|
28
|
+
export function controlTools(opts) {
|
|
29
|
+
const tools = [
|
|
30
|
+
{
|
|
31
|
+
name: FINAL_REPLY_TOOL_NAME,
|
|
32
|
+
tool: finalReplyTool,
|
|
33
|
+
precedence: 0,
|
|
34
|
+
// The last call of a repeated set: a model that restated its answer meant
|
|
35
|
+
// the restatement.
|
|
36
|
+
select: (inputs) => inputs.at(-1),
|
|
37
|
+
parse(input) {
|
|
38
|
+
const parsed = finalReplyInputSchema.safeParse(input);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
throw new ControlCallError(`${FINAL_REPLY_TOOL_NAME} input is invalid — ${issues(parsed.error)}`);
|
|
41
|
+
}
|
|
42
|
+
return { kind: "reply", text: parsed.data.text.trim() };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
// A round with no registered types has nothing to delegate to, so `delegate`
|
|
47
|
+
// is withheld for the same reason a `final` round withholds it: the tool would
|
|
48
|
+
// advertise an empty enum the model has no legal way to satisfy.
|
|
49
|
+
if (opts.delegable && opts.types.keys.length > 0) {
|
|
50
|
+
const proposalSchema = makeDecompositionProposalSchema(opts.types, opts.maxSubtasks);
|
|
51
|
+
tools.push({
|
|
52
|
+
name: DELEGATE_TOOL_NAME,
|
|
53
|
+
tool: makeDelegateTool(opts.types, opts.maxSubtasks),
|
|
54
|
+
precedence: 1,
|
|
55
|
+
select(inputs) {
|
|
56
|
+
if (inputs.length > 1) {
|
|
57
|
+
throw new ControlCallError(`${DELEGATE_TOOL_NAME} was called ${inputs.length} times in one turn. ` +
|
|
58
|
+
`Delegate once, with every subtask this round needs in that single call.`);
|
|
59
|
+
}
|
|
60
|
+
return inputs[0];
|
|
61
|
+
},
|
|
62
|
+
parse(input) {
|
|
63
|
+
const parsed = proposalSchema.safeParse(input);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
throw new ControlCallError(`${DELEGATE_TOOL_NAME} input is invalid — ${issues(parsed.error)}`);
|
|
66
|
+
}
|
|
67
|
+
// Everything past the schema — unknown reference indexes, a type's
|
|
68
|
+
// required params — throws its own error, already worded for the model.
|
|
69
|
+
const { reply, drafts } = resolveDecomposition({
|
|
70
|
+
...parsed.data,
|
|
71
|
+
subtasks: parsed.data.subtasks.map((s) => ({
|
|
72
|
+
...s,
|
|
73
|
+
params: definedParams(s.params)
|
|
74
|
+
}))
|
|
75
|
+
}, opts.catalog, opts.types);
|
|
76
|
+
return { kind: "delegate", reply, drafts };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return tools;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Drop params the model sent as an explicit `undefined`.
|
|
84
|
+
*
|
|
85
|
+
* The delegate schema declares the union of every type's param keys, all optional,
|
|
86
|
+
* so one tool schema can serve every type (see
|
|
87
|
+
* `SubtaskTypeRegistry.paramProperties`). A model that names a key and leaves it
|
|
88
|
+
* empty has sent no param, and forwarding the key with an `undefined` value would
|
|
89
|
+
* only make a missing required param report itself as a *present* one. Whether
|
|
90
|
+
* what survives satisfies the type is still
|
|
91
|
+
* `SubtaskTypeRegistry.validateParams`'s call, downstream.
|
|
92
|
+
*/
|
|
93
|
+
function definedParams(params) {
|
|
94
|
+
if (!params)
|
|
95
|
+
return undefined;
|
|
96
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined);
|
|
97
|
+
return entries.length > 0
|
|
98
|
+
? Object.fromEntries(entries)
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A zod failure as one line the model can act on: which field, and what was wrong.
|
|
103
|
+
* The same rendering
|
|
104
|
+
* {@link file://../subtasks/subtask-types.ts SubtaskTypeRegistry.validateParams}
|
|
105
|
+
* uses, so every rejection a control call can produce reads the same way.
|
|
106
|
+
*/
|
|
107
|
+
function issues(error) {
|
|
108
|
+
return error.issues
|
|
109
|
+
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
110
|
+
.join("; ");
|
|
111
|
+
}
|
|
112
|
+
/** The tools as the SDK takes them, in declaration order. */
|
|
113
|
+
export function controlToolSet(tools) {
|
|
114
|
+
return Object.fromEntries(tools.map((c) => [c.name, c.tool]));
|
|
115
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failures the loops must treat specially, whichever provider raised them.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of {@link file://./model.ts model.ts} and neutral for the same
|
|
5
|
+
* reason: a provider directory may throw these, and nothing here may know that
|
|
6
|
+
* any particular one exists. `nonRecoverableKind` in
|
|
7
|
+
* {@link file://./inference.ts inference.ts} keys on this file, so a provider
|
|
8
|
+
* written outside core — the thing `ModelRuntimeFactory` exists to make cheap —
|
|
9
|
+
* gets the same handling as core's own, with no change to core.
|
|
10
|
+
*
|
|
11
|
+
* ## Why a third classification was needed at all
|
|
12
|
+
*
|
|
13
|
+
* Core's attempt ladder splits every failure two ways — transient conditions
|
|
14
|
+
* throw out so the Workflow step retries the whole round, everything else burns
|
|
15
|
+
* the model slot and hands over to the fallback (see
|
|
16
|
+
* {@link file://./inference.ts isTransientAiError}). An expired credential fits
|
|
17
|
+
* neither: retrying spends the Workflow's budget on a request that can never
|
|
18
|
+
* succeed, and falling back spends the second slot on the *same* rejected token.
|
|
19
|
+
* It has to stop the round and say what a human must do.
|
|
20
|
+
*
|
|
21
|
+
* Note that being non-transient is not enough on its own — "not transient" is
|
|
22
|
+
* precisely the signal that means "try the fallback".
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Who refused the request, when a `401` came back.
|
|
26
|
+
*
|
|
27
|
+
* Two authorities sit on the path, each with its own credential: the AI Gateway
|
|
28
|
+
* (`cf-aig-authorization`) and the model provider itself (`Authorization`). They
|
|
29
|
+
* fail with the same status code and have completely different remedies, so a
|
|
30
|
+
* rejection that does not say which one it was sends an operator to rotate the
|
|
31
|
+
* wrong secret — which is exactly what happened before this existed.
|
|
32
|
+
*
|
|
33
|
+
* `"unknown"` is a real answer and the default. Guessing `"provider"` for an
|
|
34
|
+
* unrecognised body is how the misdiagnosis happens; saying "one of these, here
|
|
35
|
+
* is how to check each" is worse copy and better information.
|
|
36
|
+
*
|
|
37
|
+
* A third arm, `"proxy"`, named an optional intermediary between the two — the
|
|
38
|
+
* shape where a deployment terminates the AI Gateway request at its own Worker to
|
|
39
|
+
* attach a credential. It was removed in 0.8.0 with the deployment that had one.
|
|
40
|
+
* If you build that topology again, the honest classification for its refusals
|
|
41
|
+
* is `"unknown"` until you widen this union, because the remedy genuinely
|
|
42
|
+
* differs: an intermediary minting its caller credential per request fails for
|
|
43
|
+
* reasons upstream of any stored secret, so the fix is to look rather than to
|
|
44
|
+
* rotate.
|
|
45
|
+
*/
|
|
46
|
+
export type CredentialRejectedBy = "provider" | "gateway" | "unknown";
|
|
47
|
+
/**
|
|
48
|
+
* A credential on the path to the model was rejected (HTTP 401 / 403).
|
|
49
|
+
*
|
|
50
|
+
* Deliberately not an `APICallError`: the AI SDK's classifier treats those as
|
|
51
|
+
* potentially retryable, and this never is.
|
|
52
|
+
* {@link file://./inference.ts nonRecoverableKind} maps it to one of the
|
|
53
|
+
* credential kinds — which one depends on {@link source} — and that is what
|
|
54
|
+
* stops the round before the fallback slot; `isTransientAiError` additionally
|
|
55
|
+
* returns `false` so the message text can never be mistaken for a rate limit.
|
|
56
|
+
*
|
|
57
|
+
* The round then fails carrying that kind, and the host supplies the
|
|
58
|
+
* operator-facing copy through `HandleTaskDeps.failureCopy` — core owns the
|
|
59
|
+
* signal and the delivery, never the wording.
|
|
60
|
+
*/
|
|
61
|
+
export declare class CredentialRejectedError extends Error {
|
|
62
|
+
readonly name = "CredentialRejectedError";
|
|
63
|
+
/** The upstream status, when one was available. */
|
|
64
|
+
readonly status: number | undefined;
|
|
65
|
+
/** Which authority rejected it. See {@link CredentialRejectedBy}. */
|
|
66
|
+
readonly source: CredentialRejectedBy;
|
|
67
|
+
constructor(message: string, options?: {
|
|
68
|
+
status?: number;
|
|
69
|
+
source?: CredentialRejectedBy;
|
|
70
|
+
cause?: unknown;
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Structural check rather than `instanceof`.
|
|
74
|
+
*
|
|
75
|
+
* A Worker bundle can end up with two copies of this module (core linked as a
|
|
76
|
+
* tarball while a plugin resolves its own), and `instanceof` fails across
|
|
77
|
+
* them — the same realm hazard `AGENTS.md` calls out for `agents`. The name is
|
|
78
|
+
* a readonly literal, so this is as strong in practice and survives bundling.
|
|
79
|
+
*
|
|
80
|
+
* It is also what lets a provider outside core raise one: anything named
|
|
81
|
+
* `CredentialRejectedError` with a `source` is honoured, no shared class
|
|
82
|
+
* identity required.
|
|
83
|
+
*/
|
|
84
|
+
static isInstance(err: unknown): err is CredentialRejectedError;
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failures the loops must treat specially, whichever provider raised them.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of {@link file://./model.ts model.ts} and neutral for the same
|
|
5
|
+
* reason: a provider directory may throw these, and nothing here may know that
|
|
6
|
+
* any particular one exists. `nonRecoverableKind` in
|
|
7
|
+
* {@link file://./inference.ts inference.ts} keys on this file, so a provider
|
|
8
|
+
* written outside core — the thing `ModelRuntimeFactory` exists to make cheap —
|
|
9
|
+
* gets the same handling as core's own, with no change to core.
|
|
10
|
+
*
|
|
11
|
+
* ## Why a third classification was needed at all
|
|
12
|
+
*
|
|
13
|
+
* Core's attempt ladder splits every failure two ways — transient conditions
|
|
14
|
+
* throw out so the Workflow step retries the whole round, everything else burns
|
|
15
|
+
* the model slot and hands over to the fallback (see
|
|
16
|
+
* {@link file://./inference.ts isTransientAiError}). An expired credential fits
|
|
17
|
+
* neither: retrying spends the Workflow's budget on a request that can never
|
|
18
|
+
* succeed, and falling back spends the second slot on the *same* rejected token.
|
|
19
|
+
* It has to stop the round and say what a human must do.
|
|
20
|
+
*
|
|
21
|
+
* Note that being non-transient is not enough on its own — "not transient" is
|
|
22
|
+
* precisely the signal that means "try the fallback".
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* A credential on the path to the model was rejected (HTTP 401 / 403).
|
|
26
|
+
*
|
|
27
|
+
* Deliberately not an `APICallError`: the AI SDK's classifier treats those as
|
|
28
|
+
* potentially retryable, and this never is.
|
|
29
|
+
* {@link file://./inference.ts nonRecoverableKind} maps it to one of the
|
|
30
|
+
* credential kinds — which one depends on {@link source} — and that is what
|
|
31
|
+
* stops the round before the fallback slot; `isTransientAiError` additionally
|
|
32
|
+
* returns `false` so the message text can never be mistaken for a rate limit.
|
|
33
|
+
*
|
|
34
|
+
* The round then fails carrying that kind, and the host supplies the
|
|
35
|
+
* operator-facing copy through `HandleTaskDeps.failureCopy` — core owns the
|
|
36
|
+
* signal and the delivery, never the wording.
|
|
37
|
+
*/
|
|
38
|
+
export class CredentialRejectedError extends Error {
|
|
39
|
+
name = "CredentialRejectedError";
|
|
40
|
+
/** The upstream status, when one was available. */
|
|
41
|
+
status;
|
|
42
|
+
/** Which authority rejected it. See {@link CredentialRejectedBy}. */
|
|
43
|
+
source;
|
|
44
|
+
constructor(message, options) {
|
|
45
|
+
super(message, { cause: options?.cause });
|
|
46
|
+
this.status = options?.status;
|
|
47
|
+
this.source = options?.source ?? "unknown";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Structural check rather than `instanceof`.
|
|
51
|
+
*
|
|
52
|
+
* A Worker bundle can end up with two copies of this module (core linked as a
|
|
53
|
+
* tarball while a plugin resolves its own), and `instanceof` fails across
|
|
54
|
+
* them — the same realm hazard `AGENTS.md` calls out for `agents`. The name is
|
|
55
|
+
* a readonly literal, so this is as strong in practice and survives bundling.
|
|
56
|
+
*
|
|
57
|
+
* It is also what lets a provider outside core raise one: anything named
|
|
58
|
+
* `CredentialRejectedError` with a `source` is honoured, no shared class
|
|
59
|
+
* identity required.
|
|
60
|
+
*/
|
|
61
|
+
static isInstance(err) {
|
|
62
|
+
return err instanceof Error && err.name === "CredentialRejectedError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* The `final_reply` tool — the main agent answering the user itself, and the other
|
|
5
|
+
* way a round can end.
|
|
6
|
+
*
|
|
7
|
+
* A round used to end either by calling `delegate` or by stopping with plain text,
|
|
8
|
+
* and that asymmetry was the bug: to the code, "the model answered the user" and
|
|
9
|
+
* "the model narrated an action it never took" were the same outcome — a non-empty
|
|
10
|
+
* string. A model that wrote "I'll start the game" and emitted no call completed the
|
|
11
|
+
* Task successfully, having done nothing.
|
|
12
|
+
*
|
|
13
|
+
* So prose is no longer an outcome. Both endings are now named tools, the round runs
|
|
14
|
+
* with `toolChoice: "required"`, and a round that ends any other way has failed its
|
|
15
|
+
* attempt. The *choice* is still entirely the model's — the point of the design (see
|
|
16
|
+
* {@link file://../round/turn.ts turn.ts}) was never that the model be steered toward
|
|
17
|
+
* delegating, only that it not be forced. Picking between two named tools is also a
|
|
18
|
+
* far easier discrimination for a small model than picking between prose and a tool,
|
|
19
|
+
* which is what the weaker fallback models kept getting wrong.
|
|
20
|
+
*
|
|
21
|
+
* This lives outside `subtasks/` deliberately: replying is not a subtask concept.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FINAL_REPLY_TOOL_NAME = "final_reply";
|
|
24
|
+
/**
|
|
25
|
+
* The call's input, exported as the zod schema rather than only as the tool's
|
|
26
|
+
* `inputSchema`, because the round has to run it itself.
|
|
27
|
+
*
|
|
28
|
+
* A tool with no `execute` never has its input validated by the SDK — the loop
|
|
29
|
+
* halts on the call and nothing checks it. That is what let a blank `text` through
|
|
30
|
+
* to the user despite {@link nonBlank}. The round now parses every control call
|
|
31
|
+
* with the tool's own schema before using it (see
|
|
32
|
+
* {@link file://./control.ts control.ts}), and needs the schema in hand to do it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const finalReplyInputSchema: z.ZodObject<{
|
|
35
|
+
text: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
/**
|
|
38
|
+
* The tool as the model sees it. **Without `execute`**, exactly like
|
|
39
|
+
* {@link file://../subtasks/delegate.ts delegateTool}: the call *is* the round's
|
|
40
|
+
* output, so there is nothing for the SDK to run and the loop halts on it.
|
|
41
|
+
*
|
|
42
|
+
* Unlike `delegate`, this call is never reconstructed in a later round's history —
|
|
43
|
+
* a past reply is stored as, and replayed as, ordinary assistant text (history is
|
|
44
|
+
* text-only by design). The tool exists to constrain *generation*, not to become a
|
|
45
|
+
* new shape in the transcript.
|
|
46
|
+
*/
|
|
47
|
+
export declare const finalReplyTool: Tool<{
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { nonBlank } from "../subtasks/decomposition.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `final_reply` tool — the main agent answering the user itself, and the other
|
|
6
|
+
* way a round can end.
|
|
7
|
+
*
|
|
8
|
+
* A round used to end either by calling `delegate` or by stopping with plain text,
|
|
9
|
+
* and that asymmetry was the bug: to the code, "the model answered the user" and
|
|
10
|
+
* "the model narrated an action it never took" were the same outcome — a non-empty
|
|
11
|
+
* string. A model that wrote "I'll start the game" and emitted no call completed the
|
|
12
|
+
* Task successfully, having done nothing.
|
|
13
|
+
*
|
|
14
|
+
* So prose is no longer an outcome. Both endings are now named tools, the round runs
|
|
15
|
+
* with `toolChoice: "required"`, and a round that ends any other way has failed its
|
|
16
|
+
* attempt. The *choice* is still entirely the model's — the point of the design (see
|
|
17
|
+
* {@link file://../round/turn.ts turn.ts}) was never that the model be steered toward
|
|
18
|
+
* delegating, only that it not be forced. Picking between two named tools is also a
|
|
19
|
+
* far easier discrimination for a small model than picking between prose and a tool,
|
|
20
|
+
* which is what the weaker fallback models kept getting wrong.
|
|
21
|
+
*
|
|
22
|
+
* This lives outside `subtasks/` deliberately: replying is not a subtask concept.
|
|
23
|
+
*/
|
|
24
|
+
export const FINAL_REPLY_TOOL_NAME = "final_reply";
|
|
25
|
+
/**
|
|
26
|
+
* The call's input, exported as the zod schema rather than only as the tool's
|
|
27
|
+
* `inputSchema`, because the round has to run it itself.
|
|
28
|
+
*
|
|
29
|
+
* A tool with no `execute` never has its input validated by the SDK — the loop
|
|
30
|
+
* halts on the call and nothing checks it. That is what let a blank `text` through
|
|
31
|
+
* to the user despite {@link nonBlank}. The round now parses every control call
|
|
32
|
+
* with the tool's own schema before using it (see
|
|
33
|
+
* {@link file://./control.ts control.ts}), and needs the schema in hand to do it.
|
|
34
|
+
*/
|
|
35
|
+
export const finalReplyInputSchema = z.object({
|
|
36
|
+
text: nonBlank("text").describe("Your reply, in your own voice. This is shown to the user verbatim, so it must be the complete answer and must not be blank.")
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The tool as the model sees it. **Without `execute`**, exactly like
|
|
40
|
+
* {@link file://../subtasks/delegate.ts delegateTool}: the call *is* the round's
|
|
41
|
+
* output, so there is nothing for the SDK to run and the loop halts on it.
|
|
42
|
+
*
|
|
43
|
+
* Unlike `delegate`, this call is never reconstructed in a later round's history —
|
|
44
|
+
* a past reply is stored as, and replayed as, ordinary assistant text (history is
|
|
45
|
+
* text-only by design). The tool exists to constrain *generation*, not to become a
|
|
46
|
+
* new shape in the transcript.
|
|
47
|
+
*/
|
|
48
|
+
/*
|
|
49
|
+
* Annotated, not inferred, and the annotation is a **packaging** constraint
|
|
50
|
+
* rather than a style choice.
|
|
51
|
+
*
|
|
52
|
+
* `tool()` returns `Tool<Input, Output, Context>`, and `Context` lives in
|
|
53
|
+
* `@ai-sdk/provider-utils` — an internal of `ai`, which resolves it as a nested
|
|
54
|
+
* copy. Left inferred, `tsc` emits `import("@ai-sdk/provider-utils").Context`
|
|
55
|
+
* into this module's `.d.ts`, so every consumer typechecking
|
|
56
|
+
* `@dynamicagents/core/agent` needs a package this one does not declare and cannot
|
|
57
|
+
* usefully declare: pinning it here installs a *second*, different major
|
|
58
|
+
* alongside `ai`'s own, and TypeScript then refuses the reference outright as
|
|
59
|
+
* unportable.
|
|
60
|
+
*
|
|
61
|
+
* Naming the type through `ai`'s own re-export keeps the emitted declaration
|
|
62
|
+
* pointing at a package that is already a required peer. `verify:exports` walks
|
|
63
|
+
* the declaration graph for exactly this.
|
|
64
|
+
*/
|
|
65
|
+
export const finalReplyTool = tool({
|
|
66
|
+
description: "Answer the user and end this round. Use this whenever the request is yours to answer — anything about this conversation, your own history, memory, or tools, and anything you can settle with the tools available to you here, including work that has already come back to you.",
|
|
67
|
+
inputSchema: finalReplyInputSchema
|
|
68
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
2
|
+
import type { SessionMessage } from "agents/experimental/memory/session";
|
|
3
|
+
/**
|
|
4
|
+
* Session-history glue for the agent runtime: parse the gatekeeper-authored `<turn>`
|
|
5
|
+
* provenance wrapper, and bridge between plain text and the Agents-SDK Sessions
|
|
6
|
+
* store. No A2A types cross this boundary — the {@link file://../a2a/parts.ts
|
|
7
|
+
* A2A adapter} has already reduced the inbound message to a plain string.
|
|
8
|
+
*
|
|
9
|
+
* The gatekeeper inlines a `<turn from="…" id="…" channel="…" at="…">…</turn>` tag
|
|
10
|
+
* into the message text in multi-actor channels so the model — and anything
|
|
11
|
+
* downstream that needs per-message provenance — can attribute "who said what".
|
|
12
|
+
* This agent only *parses* that wrapper; it never authors one.
|
|
13
|
+
*
|
|
14
|
+
* We persist only the user turn and the assistant's final text: intra-turn tool
|
|
15
|
+
* steps stay inside the single `generateText` call, so stored history is plain
|
|
16
|
+
* text messages and the conversion to AI-SDK `ModelMessage`s is trivial.
|
|
17
|
+
*/
|
|
18
|
+
/** The fields recovered from a gatekeeper-rendered `<turn>` wrapper. */
|
|
19
|
+
export interface ParsedTurn {
|
|
20
|
+
from: string;
|
|
21
|
+
/** Slack user id, as rendered. */
|
|
22
|
+
id: string;
|
|
23
|
+
channel: string;
|
|
24
|
+
/** ISO-8601 instant. */
|
|
25
|
+
at: string;
|
|
26
|
+
/** The raw inner body. */
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Recover the structured provenance from a gatekeeper-authored turn. Returns null
|
|
31
|
+
* for any text that isn't a `<turn>` wrapper (plain messages, assistant replies),
|
|
32
|
+
* so callers can treat the provenance as optional.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseTurn(text: string): ParsedTurn | null;
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic Session-message ids for the task round loop.
|
|
37
|
+
*
|
|
38
|
+
* A round runs inside a durable Workflow step that can re-run after a crash, so
|
|
39
|
+
* its Session appends must be exactly-once. `Session.appendMessage` already
|
|
40
|
+
* dedupes on message id (an id that exists is not re-written), so deriving the id
|
|
41
|
+
* from the task id + round makes the append idempotent for free — no
|
|
42
|
+
* read-then-write race, no duplicate turns in history on replay. Pair with
|
|
43
|
+
* {@link file://./session.ts appendOnce}, which reads the durable text back so a
|
|
44
|
+
* retry that re-inferred still returns the *stored* reply.
|
|
45
|
+
*
|
|
46
|
+
* These ids share the Session id-space with random UUIDs (plain turns) and the
|
|
47
|
+
* SDK's `compaction_`-prefixed summaries; the `task:` prefix cannot collide with
|
|
48
|
+
* either.
|
|
49
|
+
*/
|
|
50
|
+
/** Id of the inbound user turn for a task (appended once, by round 0). */
|
|
51
|
+
export declare function taskUserMessageId(taskId: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Id of the acknowledgment a **delegating** round publishes — the message the
|
|
54
|
+
* user sees while that round's Subtasks run. Also the anchor a later round finds
|
|
55
|
+
* to reattach the round's `delegate` call to (see
|
|
56
|
+
* {@link file://../round/turn.ts renderTurnMessages}), which is why it is
|
|
57
|
+
* derived from the round rather than stored.
|
|
58
|
+
*/
|
|
59
|
+
export declare function roundAckMessageId(taskId: string, round: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* Recognize an acknowledgment id — **any** Task's, not just the one being
|
|
62
|
+
* rendered. A Session outlives the Task that wrote it and is shared by every Task
|
|
63
|
+
* from the same caller, so its history accumulates acks the current render has no
|
|
64
|
+
* branches for: earlier Tasks' acks, and (in the window between a round's ack
|
|
65
|
+
* append and its Subtask rows) this Task's own. Both are the agent's scaffolding
|
|
66
|
+
* rather than conversation, so both must stay out of the reference catalog; the
|
|
67
|
+
* `branches`-derived anchor map alone cannot see either.
|
|
68
|
+
*
|
|
69
|
+
* Returns the parsed Task id and round so the caller can tell whose ack it is.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseRoundAckMessageId(id: string): {
|
|
72
|
+
taskId: string;
|
|
73
|
+
round: number;
|
|
74
|
+
} | null;
|
|
75
|
+
/** Id of the terminal reply — the round in which the agent answered the user. */
|
|
76
|
+
export declare function finalReplyMessageId(taskId: string): string;
|
|
77
|
+
/** A Sessions-store message with a caller-chosen (deterministic) id. */
|
|
78
|
+
export declare function deterministicSessionMessage(id: string, role: "user" | "assistant", text: string): SessionMessage;
|
|
79
|
+
/**
|
|
80
|
+
* A Sessions-store message with a fresh random id.
|
|
81
|
+
*
|
|
82
|
+
* The counterpart to {@link deterministicSessionMessage}, and choosing between
|
|
83
|
+
* them is a real decision rather than a style one. A deterministic id makes an
|
|
84
|
+
* append **idempotent**, which is what a durable-Workflow agent needs: its round
|
|
85
|
+
* can re-run after a crash, and the id is what stops the retry from duplicating a
|
|
86
|
+
* turn or rewriting a reply the user already received.
|
|
87
|
+
*
|
|
88
|
+
* A conversational agent that answers inline has no such replay to defend
|
|
89
|
+
* against — every turn is a new message and there is no step to re-enter — so a
|
|
90
|
+
* random id is correct and a synthesized deterministic one would be a lie about
|
|
91
|
+
* what is being deduplicated.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sessionMessage(role: "user" | "assistant", text: string): SessionMessage;
|
|
94
|
+
/** Concatenate the text parts of a stored session message. */
|
|
95
|
+
export declare function sessionText(m: SessionMessage): string;
|
|
96
|
+
/** Convert stored history to AI-SDK model messages (user/assistant text only). */
|
|
97
|
+
export declare function toModelMessages(history: SessionMessage[]): ModelMessage[];
|