@dynamicagents/plugins 0.11.1 → 0.13.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.
Files changed (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. package/dist/triage/index.js +0 -302
@@ -1,355 +0,0 @@
1
- import type { LanguageModel } from "ai";
2
- import { z } from "zod";
3
- import type { SessionMessage } from "agents/experimental/memory/session";
4
- import type { AgentPlugin } from "@dynamicagents/core";
5
- /**
6
- * The classifier's output. The booleans are **not** metadata for the caller —
7
- * only `should_reply` is ever read. They exist to shape the generation itself:
8
- * constrained decoding fills properties in schema order, so declaring them ahead
9
- * of `should_reply` forces the model to commit to the sub-judgements that
10
- * determine the answer — *who is this for*, and *do I have something worth
11
- * adding* — before it can name a verdict. Reordering these fields, or dropping
12
- * one, changes what the model reasons about; it is not cosmetic.
13
- *
14
- * `reason` comes last, after the verdict, on purpose: generated afterwards it
15
- * justifies the decision rather than steering it, so a logged reason can never
16
- * contradict the `should_reply` it explains. That is what makes it worth reading
17
- * when a misclassification needs debugging.
18
- */
19
- export declare const triageSchema: z.ZodObject<{
20
- for_other_people: z.ZodBoolean;
21
- for_another_agent: z.ZodBoolean;
22
- can_contribute: z.ZodBoolean;
23
- should_reply: z.ZodBoolean;
24
- reason: z.ZodString;
25
- }, z.core.$strip>;
26
- export type TriageVerdict = z.infer<typeof triageSchema>;
27
- /**
28
- * The tuning half of {@link TriageConfig}: plain values, no bindings.
29
- *
30
- * Declare these once in a config module and spread them at the call site — see
31
- * `RecallTuning` for why enumerating each field is a liability rather than just
32
- * noise.
33
- */
34
- export interface TriageTuning {
35
- /**
36
- * AI Gateway id. Pass the host's **resolved** value
37
- * (`PluginHost.aiGatewayId`), so classify calls stay correlated with chat calls
38
- * in one gateway.
39
- */
40
- aiGatewayId?: string;
41
- /** Workers-AI id for the classifier. Defaults to a small, fast model. */
42
- modelId?: string;
43
- /** Trailing messages the classifier sees. Defaults to 12. */
44
- historyMessages?: number;
45
- /** Per-message char cap in the prompt. Defaults to 500. */
46
- messageMaxChars?: number;
47
- }
48
- export interface TriageConfig extends TriageTuning {
49
- /** The `AI` binding. Read lazily — see {@link triage}. */
50
- ai: Ai;
51
- /** Test override; skips the provider entirely. */
52
- model?: LanguageModel;
53
- }
54
- export interface TriagePromptOptions {
55
- historyMessages?: number;
56
- messageMaxChars?: number;
57
- }
58
- /**
59
- * Build the classifier prompt from the tail of the conversation.
60
- *
61
- * A bare message is frequently unclassifiable — "yes", "thanks", "and the second
62
- * one?" only resolve against what came before — and the drift being fixed is
63
- * *only* visible in context, where the classifier can see the agent has been
64
- * talking and that the speaker has since turned to someone else.
65
- *
66
- * Exported for its own tests: it is pure, so the prompt can be asserted without
67
- * an LLM.
68
- */
69
- export declare function buildTriagePrompt(history: SessionMessage[], options?: TriagePromptOptions): string;
70
- /**
71
- * Classify whether the agent should reply to the newest message in `history`.
72
- *
73
- * **Fails open.** Every error is caught here and answered `true`, because the
74
- * two mistakes are not symmetric: a wrong reply is noise the user can see and
75
- * ignore, while a wrong silence is invisible — the person who needed the agent
76
- * simply never hears back and has no way to tell that anything happened. A
77
- * triage outage must therefore degrade to the previous behaviour (let the loop
78
- * decide), never to a silent agent.
79
- *
80
- * Only `should_reply` is read; the other fields exist to steer the generation
81
- * (see {@link triageSchema}) and are logged, not re-derived in code.
82
- */
83
- export declare function shouldReply(model: LanguageModel, history: SessionMessage[], options?: TriagePromptOptions): Promise<boolean>;
84
- export declare function triage(config: TriageConfig): AgentPlugin;
85
- /** The tool the model calls to end a turn without replying. */
86
- export declare const NO_REPLY_TOOL_NAME = "no_reply";
87
- /**
88
- * Tool result when a `no_reply` call is ignored. This is only ever read by the
89
- * model in one case: it has already sent something this turn (so the tool was
90
- * withdrawn) yet named it anyway — the call is ignored and it must finish with a
91
- * reply. When a `no_reply` call is instead honoured it ends the turn, and this
92
- * result is discarded unread.
93
- */
94
- export declare const NO_REPLY_IGNORED = "Ignored: you have already sent a message this turn \u2014 finish with a reply.";
95
- /**
96
- * End the turn without replying — the *late* counterpart to the pre-turn gate.
97
- * The agent looks something up, concludes there is nothing worth adding, and
98
- * declines.
99
- *
100
- * Deliberately an ordinary tool with an ordinary no-op `execute`: the loop, not
101
- * the tool, decides what a `no_reply` call means (see {@link isNoReplyTurn}).
102
- * Two things here are load-bearing:
103
- *
104
- * - **`execute` must exist.** Once the agent has spoken the tool is withdrawn,
105
- * but the model can still name it (the SDK resolves calls against the
106
- * unfiltered map); that call is ignored and the loop must continue to a real
107
- * reply. `generateText` only continues while every tool call in a step
108
- * produced an output, so without `execute` that step would halt with no reply
109
- * text and surface as a failure. Executing normally keeps the counts equal.
110
- * - **`reason` must stay optional.** A required field the model omits makes the
111
- * SDK mark the call invalid and skip execution, halting the loop the same way.
112
- *
113
- * Wiring is the loop's: offer this tool until the agent has spoken, withdraw it
114
- * after, and stop the run when {@link isNoReplyTurn} holds.
115
- */
116
- export declare const noReplyTool: ({
117
- title?: string;
118
- providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
119
- metadata?: import("@ai-sdk/provider").JSONObject;
120
- inputSchema: import("ai").FlexibleSchema<{
121
- reason?: string | undefined;
122
- }>;
123
- contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
124
- needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
125
- reason?: string | undefined;
126
- }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
127
- onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
128
- onInputDelta?: ((options: {
129
- inputTextDelta: string;
130
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
131
- onInputAvailable?: ((options: {
132
- input: {
133
- reason?: string | undefined;
134
- };
135
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
136
- toModelOutput?: ((options: {
137
- toolCallId: string;
138
- input: {
139
- reason?: string | undefined;
140
- };
141
- output: string;
142
- }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
143
- } & {
144
- outputSchema?: import("ai").FlexibleSchema<string> | undefined;
145
- execute: import("ai").ToolExecuteFunction<{
146
- reason?: string | undefined;
147
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
148
- } & {
149
- description?: string | ((options: {
150
- context: NoInfer<import("@ai-sdk/provider-utils").Context>;
151
- experimental_sandbox?: import("ai").Experimental_SandboxSession;
152
- }) => string) | undefined;
153
- strict?: boolean;
154
- inputExamples?: {
155
- input: NoInfer<{
156
- reason?: string | undefined;
157
- }>;
158
- }[] | undefined;
159
- id?: never;
160
- isProviderExecuted?: never;
161
- args?: never;
162
- supportsDeferredResults?: never;
163
- } & {
164
- type?: undefined | "function";
165
- } & {
166
- execute: import("ai").ToolExecuteFunction<{
167
- reason?: string | undefined;
168
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
169
- }) | ({
170
- title?: string;
171
- providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
172
- metadata?: import("@ai-sdk/provider").JSONObject;
173
- inputSchema: import("ai").FlexibleSchema<{
174
- reason?: string | undefined;
175
- }>;
176
- contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
177
- needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
178
- reason?: string | undefined;
179
- }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
180
- onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
181
- onInputDelta?: ((options: {
182
- inputTextDelta: string;
183
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
184
- onInputAvailable?: ((options: {
185
- input: {
186
- reason?: string | undefined;
187
- };
188
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
189
- toModelOutput?: ((options: {
190
- toolCallId: string;
191
- input: {
192
- reason?: string | undefined;
193
- };
194
- output: string;
195
- }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
196
- } & {
197
- outputSchema?: import("ai").FlexibleSchema<string> | undefined;
198
- execute: import("ai").ToolExecuteFunction<{
199
- reason?: string | undefined;
200
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
201
- } & {
202
- description?: string | ((options: {
203
- context: NoInfer<import("@ai-sdk/provider-utils").Context>;
204
- experimental_sandbox?: import("ai").Experimental_SandboxSession;
205
- }) => string) | undefined;
206
- strict?: boolean;
207
- inputExamples?: {
208
- input: NoInfer<{
209
- reason?: string | undefined;
210
- }>;
211
- }[] | undefined;
212
- id?: never;
213
- isProviderExecuted?: never;
214
- args?: never;
215
- supportsDeferredResults?: never;
216
- } & {
217
- type: "dynamic";
218
- } & {
219
- execute: import("ai").ToolExecuteFunction<{
220
- reason?: string | undefined;
221
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
222
- }) | ({
223
- title?: string;
224
- providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
225
- metadata?: import("@ai-sdk/provider").JSONObject;
226
- inputSchema: import("ai").FlexibleSchema<{
227
- reason?: string | undefined;
228
- }>;
229
- contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
230
- needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
231
- reason?: string | undefined;
232
- }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
233
- onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
234
- onInputDelta?: ((options: {
235
- inputTextDelta: string;
236
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
237
- onInputAvailable?: ((options: {
238
- input: {
239
- reason?: string | undefined;
240
- };
241
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
242
- toModelOutput?: ((options: {
243
- toolCallId: string;
244
- input: {
245
- reason?: string | undefined;
246
- };
247
- output: string;
248
- }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
249
- } & {
250
- outputSchema?: import("ai").FlexibleSchema<string> | undefined;
251
- execute: import("ai").ToolExecuteFunction<{
252
- reason?: string | undefined;
253
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
254
- } & {
255
- type: "provider";
256
- id: `${string}.${string}`;
257
- args: Record<string, unknown>;
258
- description?: never;
259
- strict?: never;
260
- inputExamples?: never;
261
- } & {
262
- isProviderExecuted: false;
263
- supportsDeferredResults?: never;
264
- } & {
265
- execute: import("ai").ToolExecuteFunction<{
266
- reason?: string | undefined;
267
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
268
- }) | ({
269
- title?: string;
270
- providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
271
- metadata?: import("@ai-sdk/provider").JSONObject;
272
- inputSchema: import("ai").FlexibleSchema<{
273
- reason?: string | undefined;
274
- }>;
275
- contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
276
- needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
277
- reason?: string | undefined;
278
- }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
279
- onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
280
- onInputDelta?: ((options: {
281
- inputTextDelta: string;
282
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
283
- onInputAvailable?: ((options: {
284
- input: {
285
- reason?: string | undefined;
286
- };
287
- } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
288
- toModelOutput?: ((options: {
289
- toolCallId: string;
290
- input: {
291
- reason?: string | undefined;
292
- };
293
- output: string;
294
- }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
295
- } & {
296
- outputSchema?: import("ai").FlexibleSchema<string> | undefined;
297
- execute: import("ai").ToolExecuteFunction<{
298
- reason?: string | undefined;
299
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
300
- } & {
301
- type: "provider";
302
- id: `${string}.${string}`;
303
- args: Record<string, unknown>;
304
- description?: never;
305
- strict?: never;
306
- inputExamples?: never;
307
- } & {
308
- isProviderExecuted: true;
309
- supportsDeferredResults?: boolean;
310
- } & {
311
- execute: import("ai").ToolExecuteFunction<{
312
- reason?: string | undefined;
313
- }, string, NoInfer<import("@ai-sdk/provider-utils").Context>>;
314
- });
315
- /**
316
- * What the agent is told about {@link noReplyTool}, as a system-prompt suffix.
317
- *
318
- * It lives here rather than in a host's prompt file for the same reason a subtask
319
- * type carries its own `capability`: everything the model is told about a
320
- * capability is declared by whatever owns that capability, so uninstalling the
321
- * plugin takes its advice with it. A host describing a `no_reply` tool it no
322
- * longer installs is the exact drift those fields exist to end.
323
- *
324
- * Not a `capability` on the plugin, though, and the distinction is the design.
325
- * `capability` is rendered into the soul once and is true for the whole
326
- * conversation; this has to be **withdrawn mid-turn**, the moment the agent
327
- * speaks, in the same `prepareStep` that withdraws the tool. Advice for a tool
328
- * that is no longer on the call is worse than no advice — so the loop injects it,
329
- * and the loop is what knows when.
330
- */
331
- export declare const NO_REPLY_GUIDANCE: string;
332
- /** The only part of a `StepResult` the no-reply predicates read. */
333
- type ToolCallingStep = {
334
- toolCalls: ReadonlyArray<{
335
- toolName: string;
336
- }>;
337
- };
338
- /** Whether a step calls `no_reply` at all — on its own or beside other tools. */
339
- export declare function stepCallsNoReply(step: ToolCallingStep): boolean;
340
- /**
341
- * Whether a run declined to reply: the agent has **not sent anything this turn**
342
- * (`!repliedAny`) and its final step called `no_reply`. A `no_reply` call ends
343
- * the turn the moment it appears — even beside another tool, which still runs
344
- * but whose result is discarded — so the first step to call it is the last. A
345
- * `no_reply` after the agent already streamed content is the one thing ignored.
346
- *
347
- * `!repliedAny` is the runtime guard that lets `no_reply` be a *late* decision
348
- * while still forbidding it once the agent has spoken. Hiding the tool from the
349
- * model merely hides it; the SDK still resolves a tool call against the full,
350
- * unfiltered map, so a model that names `no_reply` after speaking executes it
351
- * happily — this flag is what stops that from discarding a turn the user has
352
- * already seen.
353
- */
354
- export declare function isNoReplyTurn(repliedAny: boolean, steps: readonly ToolCallingStep[]): boolean;
355
- export {};
@@ -1,302 +0,0 @@
1
- import { generateText, Output, tool } from "ai";
2
- import { z } from "zod";
3
- import { createWorkersAI } from "workers-ai-provider";
4
- import { definePlugin } from "@dynamicagents/core";
5
- import { parseTurn, sessionText } from "@dynamicagents/core/agent";
6
- /**
7
- * `@dynamicagents/plugins/triage` — decide whether an inbound message deserves a
8
- * reply at all, *before* the tool loop runs.
9
- *
10
- * An agent in a shared channel sees every message, and most are not for it. Left
11
- * to the main loop, that judgement is made by a model simultaneously trying to
12
- * be helpful, with memory, history and half a dozen tools in view — and it
13
- * degrades exactly there, and degrades **invisibly**: failing to call a
14
- * decline-tool looks identical to deciding not to.
15
- *
16
- * Triage moves the decision somewhere it cannot be skipped. A structured-output
17
- * call has no "just answer instead" escape hatch — the schema must be filled —
18
- * so the question is always actually asked.
19
- *
20
- * Two moments, and both are needed. The gate here judges the *message*; the
21
- * `no_reply` tool below lets the agent decline *late*, after looking something
22
- * up and concluding there was nothing worth adding. Wiring `no_reply` into a
23
- * loop is the loop's business (see {@link noReplyTool}); this module ships the
24
- * pieces rather than the loop.
25
- */
26
- /** Small, fast classifier. High-volume and narrow — not the chat model. */
27
- const DEFAULT_TRIAGE_MODEL_ID = "@cf/qwen/qwen3-30b-a3b-fp8";
28
- /** Trailing history the classifier sees, including the message being judged. */
29
- const DEFAULT_HISTORY_MESSAGES = 12;
30
- /** Per-message char cap in the prompt — keeps the classify call small. */
31
- const DEFAULT_MESSAGE_MAX_CHARS = 500;
32
- /**
33
- * The classifier's output. The booleans are **not** metadata for the caller —
34
- * only `should_reply` is ever read. They exist to shape the generation itself:
35
- * constrained decoding fills properties in schema order, so declaring them ahead
36
- * of `should_reply` forces the model to commit to the sub-judgements that
37
- * determine the answer — *who is this for*, and *do I have something worth
38
- * adding* — before it can name a verdict. Reordering these fields, or dropping
39
- * one, changes what the model reasons about; it is not cosmetic.
40
- *
41
- * `reason` comes last, after the verdict, on purpose: generated afterwards it
42
- * justifies the decision rather than steering it, so a logged reason can never
43
- * contradict the `should_reply` it explains. That is what makes it worth reading
44
- * when a misclassification needs debugging.
45
- */
46
- export const triageSchema = z.object({
47
- for_other_people: z
48
- .boolean()
49
- .describe("Is this message aimed at other people in the channel rather than at you?"),
50
- for_another_agent: z
51
- .boolean()
52
- .describe("Is this message aimed at a different bot or agent, not at you?"),
53
- can_contribute: z
54
- .boolean()
55
- .describe("Is there something this conversation clearly needs right now that you can add?"),
56
- should_reply: z
57
- .boolean()
58
- .describe("Given all of the above, should you reply at all?"),
59
- reason: z.string().describe("One short sentence justifying the verdict.")
60
- });
61
- /**
62
- * The classifier's instructions. Aimed squarely at the failure this exists to
63
- * fix: the agent joins a conversation legitimately, then keeps answering
64
- * afterwards as unrelated messages go by. Every rule here is about *audience*
65
- * and *value* — the hard, per-message calls — rather than about obeying
66
- * instructions, which was never the part that broke.
67
- */
68
- const TRIAGE_RULES = [
69
- "You are the assistant in a shared chat channel, and you see every message — including the many that are not for you.",
70
- "Decide one thing only: should you reply to the LAST message?",
71
- "",
72
- "Rules:",
73
- "- People talking to each other, chatter, and acknowledgements need no reply from you. In a busy channel this is the common case.",
74
- "- Having replied recently does not entitle you to reply now. Judge the last message on its own audience; being part of the conversation a moment ago is not a licence to answer whatever follows.",
75
- "- Reply when you are directly asked something, addressed by name or @mention, or when the previous message was your own and this one answers or follows up on it.",
76
- "- `can_contribute` is a high bar: something the conversation clearly needs, not merely something true you could say. Restating, agreeing, praising, or acknowledging is not contributing.",
77
- "- These rules apply in every language, including when the channel is not in English.",
78
- "- When you are genuinely unsure, reply. A wrong silence is invisible to the person who needed you; a wrong reply is only noise."
79
- ].join("\n");
80
- /** Trim a message body to the triage char cap, marking it when cut. */
81
- function truncate(text, max) {
82
- const flat = text.trim();
83
- return flat.length > max ? `${flat.slice(0, max)}…` : flat;
84
- }
85
- /**
86
- * Render one stored message as a transcript line. User turns carry the gatekeeper's
87
- * `<turn from="…">` provenance, so the speaker's name is surfaced rather than a
88
- * flat "user" — without it the classifier cannot tell one participant from
89
- * another, which is most of what it has to reason about. Assistant turns are
90
- * labelled "you" so "the previous message was your own" is legible.
91
- */
92
- function transcriptLine(message, max) {
93
- const text = sessionText(message);
94
- if (message.role === "assistant")
95
- return `you: ${truncate(text, max)}`;
96
- const turn = parseTurn(text);
97
- const speaker = turn ? turn.from : "someone";
98
- return `${speaker}: ${truncate(turn ? turn.body : text, max)}`;
99
- }
100
- /**
101
- * Build the classifier prompt from the tail of the conversation.
102
- *
103
- * A bare message is frequently unclassifiable — "yes", "thanks", "and the second
104
- * one?" only resolve against what came before — and the drift being fixed is
105
- * *only* visible in context, where the classifier can see the agent has been
106
- * talking and that the speaker has since turned to someone else.
107
- *
108
- * Exported for its own tests: it is pure, so the prompt can be asserted without
109
- * an LLM.
110
- */
111
- export function buildTriagePrompt(history, options = {}) {
112
- const { historyMessages = DEFAULT_HISTORY_MESSAGES, messageMaxChars = DEFAULT_MESSAGE_MAX_CHARS } = options;
113
- const recent = history
114
- .filter((m) => m.role === "user" || m.role === "assistant")
115
- .slice(-historyMessages);
116
- const transcript = recent
117
- .map((m) => transcriptLine(m, messageMaxChars))
118
- .join("\n");
119
- return [
120
- TRIAGE_RULES,
121
- "",
122
- "Recent messages, oldest first. The last line is the message you are judging:",
123
- transcript
124
- ].join("\n");
125
- }
126
- /**
127
- * Classify whether the agent should reply to the newest message in `history`.
128
- *
129
- * **Fails open.** Every error is caught here and answered `true`, because the
130
- * two mistakes are not symmetric: a wrong reply is noise the user can see and
131
- * ignore, while a wrong silence is invisible — the person who needed the agent
132
- * simply never hears back and has no way to tell that anything happened. A
133
- * triage outage must therefore degrade to the previous behaviour (let the loop
134
- * decide), never to a silent agent.
135
- *
136
- * Only `should_reply` is read; the other fields exist to steer the generation
137
- * (see {@link triageSchema}) and are logged, not re-derived in code.
138
- */
139
- export async function shouldReply(model, history, options = {}) {
140
- try {
141
- // `generateText` + `Output.object` rather than `generateObject`, which is
142
- // deprecated in AI SDK 7. Same effect: the provider is sent a json_schema
143
- // response format, so the model cannot answer with anything but the schema.
144
- const { output } = await generateText({
145
- model,
146
- output: Output.object({ schema: triageSchema }),
147
- prompt: buildTriagePrompt(history, options)
148
- });
149
- // Log the whole verdict, not just the outcome. The booleans are the
150
- // reasoning — a correct verdict reached through wrong booleans means the
151
- // decomposition is not carrying the decision, and will drift again on
152
- // untested cases.
153
- console.debug("[triage] verdict", output);
154
- return output.should_reply;
155
- }
156
- catch (err) {
157
- console.warn("[triage] classify failed, replying by default", {
158
- error: String(err)
159
- });
160
- return true;
161
- }
162
- }
163
- export function triage(config) {
164
- const { ai, aiGatewayId, modelId = DEFAULT_TRIAGE_MODEL_ID, historyMessages, messageMaxChars } = config;
165
- /**
166
- * Built on first use, never at module scope. Cloudflare evaluates module scope
167
- * during `wrangler deploy` to validate the new version, and bindings are not
168
- * populated at that point — constructing eagerly makes `createWorkersAI` throw
169
- * "you must provide either a binding or credentials". The same laziness
170
- * protects a host that builds its runtime early.
171
- *
172
- * No fallback model: triage fails open, so a second attempt on a different
173
- * model buys nothing a `true` does not already buy, at twice the latency in
174
- * front of every turn.
175
- */
176
- let model;
177
- const classifier = () => (model ??=
178
- config.model ??
179
- createWorkersAI({
180
- binding: ai,
181
- ...(aiGatewayId ? { gateway: { id: aiGatewayId } } : {})
182
- })(modelId));
183
- return definePlugin({
184
- key: "triage",
185
- /**
186
- * Building the classifier is inside the guarantee, not in front of it.
187
- *
188
- * `shouldReply` catches its own failures, but it can only catch what happens
189
- * after it is entered — and `createWorkersAI` throws synchronously when the
190
- * binding is missing, before the call is ever made. That threw straight out
191
- * of this hook, which is the one outcome the whole design forbids: a triage
192
- * outage must degrade to *running the turn*, never to a silent agent.
193
- * (`createAgentRuntime` also treats a rejected gate as `true`, so the agent
194
- * stayed correct — but a plugin that documents fail-open should not be
195
- * relying on its host to make that true.)
196
- */
197
- shouldHandleTurn: async ({ history }) => {
198
- let model;
199
- try {
200
- model = classifier();
201
- }
202
- catch (error) {
203
- console.warn("[triage] classifier unavailable, replying by default", {
204
- error: String(error)
205
- });
206
- return true;
207
- }
208
- return shouldReply(model, history, { historyMessages, messageMaxChars });
209
- }
210
- // No `requires`. `AI` is core's own mandatory binding, not this plugin's to
211
- // demand — declaring it would report a core misconfiguration under this
212
- // plugin's name.
213
- });
214
- }
215
- // --- declining late: the `no_reply` tool ------------------------------------
216
- /** The tool the model calls to end a turn without replying. */
217
- export const NO_REPLY_TOOL_NAME = "no_reply";
218
- /**
219
- * Tool result when a `no_reply` call is ignored. This is only ever read by the
220
- * model in one case: it has already sent something this turn (so the tool was
221
- * withdrawn) yet named it anyway — the call is ignored and it must finish with a
222
- * reply. When a `no_reply` call is instead honoured it ends the turn, and this
223
- * result is discarded unread.
224
- */
225
- export const NO_REPLY_IGNORED = "Ignored: you have already sent a message this turn — finish with a reply.";
226
- /**
227
- * End the turn without replying — the *late* counterpart to the pre-turn gate.
228
- * The agent looks something up, concludes there is nothing worth adding, and
229
- * declines.
230
- *
231
- * Deliberately an ordinary tool with an ordinary no-op `execute`: the loop, not
232
- * the tool, decides what a `no_reply` call means (see {@link isNoReplyTurn}).
233
- * Two things here are load-bearing:
234
- *
235
- * - **`execute` must exist.** Once the agent has spoken the tool is withdrawn,
236
- * but the model can still name it (the SDK resolves calls against the
237
- * unfiltered map); that call is ignored and the loop must continue to a real
238
- * reply. `generateText` only continues while every tool call in a step
239
- * produced an output, so without `execute` that step would halt with no reply
240
- * text and surface as a failure. Executing normally keeps the counts equal.
241
- * - **`reason` must stay optional.** A required field the model omits makes the
242
- * SDK mark the call invalid and skip execution, halting the loop the same way.
243
- *
244
- * Wiring is the loop's: offer this tool until the agent has spoken, withdraw it
245
- * after, and stop the run when {@link isNoReplyTurn} holds.
246
- */
247
- export const noReplyTool = tool({
248
- description: "End this turn without replying at all, when the message needs no answer from you (chatter, people talking to each other, anything not addressed to you) — or when you looked into it and there is genuinely nothing worth adding. Calling this ends the turn immediately, so do not pair it with a tool whose result you still need. Only available until you have sent anything this turn.",
249
- inputSchema: z.object({
250
- reason: z
251
- .string()
252
- .optional()
253
- .describe("One short phrase: why this message needs no reply.")
254
- }),
255
- execute: async () => NO_REPLY_IGNORED
256
- });
257
- /**
258
- * What the agent is told about {@link noReplyTool}, as a system-prompt suffix.
259
- *
260
- * It lives here rather than in a host's prompt file for the same reason a subtask
261
- * type carries its own `capability`: everything the model is told about a
262
- * capability is declared by whatever owns that capability, so uninstalling the
263
- * plugin takes its advice with it. A host describing a `no_reply` tool it no
264
- * longer installs is the exact drift those fields exist to end.
265
- *
266
- * Not a `capability` on the plugin, though, and the distinction is the design.
267
- * `capability` is rendered into the soul once and is true for the whole
268
- * conversation; this has to be **withdrawn mid-turn**, the moment the agent
269
- * speaks, in the same `prepareStep` that withdraws the tool. Advice for a tool
270
- * that is no longer on the call is worse than no advice — so the loop injects it,
271
- * and the loop is what knows when.
272
- */
273
- export const NO_REPLY_GUIDANCE = [
274
- "",
275
- "",
276
- "You see every message in this channel, including the many that are not for you.",
277
- "Call the `no_reply` tool to end your turn without replying whenever the message needs no answer from you: people talking to each other, chatter, acknowledgements, or anything you were not asked about. You may also look something up first and then call `no_reply` if, having looked, there is genuinely nothing worth adding.",
278
- "Staying silent is the right default in a busy channel. Reply only when you are addressed, asked a question, or can add something the conversation clearly needs.",
279
- "Calling `no_reply` ends your turn at once — don't pair it with a tool whose result you still need, and don't write any text with it (that text is discarded). It is unavailable once you have already sent something this turn."
280
- ].join("\n");
281
- /** Whether a step calls `no_reply` at all — on its own or beside other tools. */
282
- export function stepCallsNoReply(step) {
283
- return step.toolCalls.some((c) => c.toolName === NO_REPLY_TOOL_NAME);
284
- }
285
- /**
286
- * Whether a run declined to reply: the agent has **not sent anything this turn**
287
- * (`!repliedAny`) and its final step called `no_reply`. A `no_reply` call ends
288
- * the turn the moment it appears — even beside another tool, which still runs
289
- * but whose result is discarded — so the first step to call it is the last. A
290
- * `no_reply` after the agent already streamed content is the one thing ignored.
291
- *
292
- * `!repliedAny` is the runtime guard that lets `no_reply` be a *late* decision
293
- * while still forbidding it once the agent has spoken. Hiding the tool from the
294
- * model merely hides it; the SDK still resolves a tool call against the full,
295
- * unfiltered map, so a model that names `no_reply` after speaking executes it
296
- * happily — this flag is what stops that from discarding a turn the user has
297
- * already seen.
298
- */
299
- export function isNoReplyTurn(repliedAny, steps) {
300
- const last = steps.at(-1);
301
- return !repliedAny && last !== undefined && stepCallsNoReply(last);
302
- }