@namzu/sdk 4.0.0 → 5.0.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 (84) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +6 -0
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/run-agent.test.d.ts +2 -0
  6. package/dist/agents/__tests__/run-agent.test.d.ts.map +1 -0
  7. package/dist/agents/__tests__/run-agent.test.js +143 -0
  8. package/dist/agents/__tests__/run-agent.test.js.map +1 -0
  9. package/dist/agents/index.d.ts +2 -0
  10. package/dist/agents/index.d.ts.map +1 -1
  11. package/dist/agents/index.js +1 -0
  12. package/dist/agents/index.js.map +1 -1
  13. package/dist/agents/runAgent.d.ts +104 -0
  14. package/dist/agents/runAgent.d.ts.map +1 -0
  15. package/dist/agents/runAgent.js +92 -0
  16. package/dist/agents/runAgent.js.map +1 -0
  17. package/dist/provider/__tests__/thinking-support.test.d.ts +2 -0
  18. package/dist/provider/__tests__/thinking-support.test.d.ts.map +1 -0
  19. package/dist/provider/__tests__/thinking-support.test.js +47 -0
  20. package/dist/provider/__tests__/thinking-support.test.js.map +1 -0
  21. package/dist/provider/thinking-support.d.ts +30 -0
  22. package/dist/provider/thinking-support.d.ts.map +1 -0
  23. package/dist/provider/thinking-support.js +32 -0
  24. package/dist/provider/thinking-support.js.map +1 -0
  25. package/dist/public-runtime.d.ts +4 -1
  26. package/dist/public-runtime.d.ts.map +1 -1
  27. package/dist/public-runtime.js +10 -1
  28. package/dist/public-runtime.js.map +1 -1
  29. package/dist/public-types.d.ts +1 -1
  30. package/dist/public-types.d.ts.map +1 -1
  31. package/dist/runtime/query/__tests__/steering-reaches-the-model.test.d.ts +2 -0
  32. package/dist/runtime/query/__tests__/steering-reaches-the-model.test.d.ts.map +1 -0
  33. package/dist/runtime/query/__tests__/steering-reaches-the-model.test.js +94 -0
  34. package/dist/runtime/query/__tests__/steering-reaches-the-model.test.js.map +1 -0
  35. package/dist/runtime/query/__tests__/steering.test.d.ts +2 -0
  36. package/dist/runtime/query/__tests__/steering.test.d.ts.map +1 -0
  37. package/dist/runtime/query/__tests__/steering.test.js +92 -0
  38. package/dist/runtime/query/__tests__/steering.test.js.map +1 -0
  39. package/dist/runtime/query/index.d.ts +10 -0
  40. package/dist/runtime/query/index.d.ts.map +1 -1
  41. package/dist/runtime/query/index.js +1 -0
  42. package/dist/runtime/query/index.js.map +1 -1
  43. package/dist/runtime/query/iteration/phases/context.d.ts +8 -0
  44. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  45. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  46. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  47. package/dist/runtime/query/iteration/phases/tool-review.js +7 -1
  48. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  49. package/dist/runtime/query/steering.d.ts +81 -0
  50. package/dist/runtime/query/steering.d.ts.map +1 -0
  51. package/dist/runtime/query/steering.js +77 -0
  52. package/dist/runtime/query/steering.js.map +1 -0
  53. package/dist/types/agent/manager.d.ts +11 -4
  54. package/dist/types/agent/manager.d.ts.map +1 -1
  55. package/dist/types/agent/supervisor.d.ts +9 -0
  56. package/dist/types/agent/supervisor.d.ts.map +1 -1
  57. package/dist/types/common/index.d.ts +12 -0
  58. package/dist/types/common/index.d.ts.map +1 -1
  59. package/dist/types/common/index.js +7 -0
  60. package/dist/types/common/index.js.map +1 -1
  61. package/dist/types/provider/chat.d.ts +57 -5
  62. package/dist/types/provider/chat.d.ts.map +1 -1
  63. package/dist/types/provider/index.d.ts +1 -1
  64. package/dist/types/provider/index.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/agents/SupervisorAgent.ts +6 -0
  67. package/src/agents/__tests__/run-agent.test.ts +165 -0
  68. package/src/agents/index.ts +2 -0
  69. package/src/agents/runAgent.ts +179 -0
  70. package/src/provider/__tests__/thinking-support.test.ts +60 -0
  71. package/src/provider/thinking-support.ts +37 -0
  72. package/src/public-runtime.ts +10 -0
  73. package/src/public-types.ts +7 -1
  74. package/src/runtime/query/__tests__/steering-reaches-the-model.test.ts +117 -0
  75. package/src/runtime/query/__tests__/steering.test.ts +121 -0
  76. package/src/runtime/query/index.ts +12 -0
  77. package/src/runtime/query/iteration/phases/context.ts +9 -0
  78. package/src/runtime/query/iteration/phases/tool-review.ts +7 -1
  79. package/src/runtime/query/steering.ts +135 -0
  80. package/src/types/agent/manager.ts +11 -4
  81. package/src/types/agent/supervisor.ts +10 -0
  82. package/src/types/common/index.ts +20 -0
  83. package/src/types/provider/chat.ts +61 -5
  84. package/src/types/provider/index.ts +1 -0
@@ -67,17 +67,69 @@ export interface ChatCompletionParams {
67
67
  * Drivers that do not support it ignore the field.
68
68
  */
69
69
  thinking?: ThinkingConfig;
70
+ /**
71
+ * How much work to put into the response. See {@link ReasoningEffort}.
72
+ *
73
+ * Drivers that do not support it ignore the field.
74
+ */
75
+ effort?: ReasoningEffort;
70
76
  }
71
77
  export interface ThinkingConfig {
72
- type: 'enabled' | 'disabled';
73
- /** Token allowance for the thinking pass. */
78
+ /**
79
+ * Which thinking mode to ask for.
80
+ *
81
+ * `'adaptive'` lets the model decide whether and how deeply to think per
82
+ * request; depth is steered by {@link ChatCompletionParams.effort} rather
83
+ * than a token budget. `'enabled'` is the older manual mode, where
84
+ * {@link budgetTokens} fixes the depth and the model thinks on every
85
+ * request.
86
+ *
87
+ * **These are not interchangeable, and a driver must not guess.** Vendors
88
+ * reject the wrong one for a given model outright rather than degrading:
89
+ * newer models refuse `'enabled'`, older ones refuse `'adaptive'`, and
90
+ * some refuse `'disabled'` because they cannot stop thinking at all. A
91
+ * driver that sends a mode the model does not accept produces a failed
92
+ * request, not a worse answer — which is why this is a declared intent
93
+ * that each driver resolves against the model it is about to call.
94
+ */
95
+ type: 'adaptive' | 'enabled' | 'disabled';
96
+ /**
97
+ * Token allowance for the thinking pass. Manual mode only — the adaptive
98
+ * mode has no budget, and depth is set by `effort`.
99
+ */
74
100
  budgetTokens?: number;
75
101
  /**
76
- * Whether the provider should return full thinking text or a summary.
77
- * Purely a request hint; the runtime stores whatever comes back.
102
+ * Whether the thinking text comes back or only its signature.
103
+ *
104
+ * `'omitted'` returns the blocks with an empty body and a signature, which
105
+ * is enough to replay them on the next turn (see `replayReasoning`) while
106
+ * keeping the text out of the response. `'summarized'` returns a summary
107
+ * of the reasoning.
108
+ *
109
+ * Worth setting explicitly. This defaults to `'omitted'` on newer models,
110
+ * so a caller that wants to show reasoning and does not ask for it gets
111
+ * thinking blocks whose text is empty and no indication why.
112
+ *
113
+ * The values were `'full' | 'summarized'` here, and `'full'` was never a
114
+ * value any vendor accepted — a declared option that could only ever have
115
+ * been rejected, next to a real one that was missing.
78
116
  */
79
- display?: 'full' | 'summarized';
117
+ display?: 'summarized' | 'omitted';
80
118
  }
119
+ /**
120
+ * How much work the model should put into a response.
121
+ *
122
+ * A sibling of {@link ChatCompletionParams.thinking}, not a field inside it,
123
+ * because it is not exclusively a thinking control: it shapes the whole
124
+ * response, and at least one manual-mode model accepts it alongside a token
125
+ * budget, where effort shapes the answer and the budget sets thinking depth.
126
+ * Nesting it under `thinking` would have made that combination unsayable.
127
+ *
128
+ * In adaptive mode it is the primary depth lever — low effort may skip
129
+ * thinking entirely on easy input. In manual mode `budgetTokens` sets depth
130
+ * and effort does not move it.
131
+ */
132
+ export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
81
133
  export interface ChatCompletionResponse {
82
134
  id: string;
83
135
  model: string;
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/types/provider/chat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,MAAM,UAAU,GACnB,MAAM,GACN,MAAM,GACN,UAAU,GACV;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAEnD,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IACA,IAAI,EAAE,aAAa,CAAA;IACnB,WAAW,EAAE;QACZ,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC/B,MAAM,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;CACA,CAAA;AAEJ,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;IACvB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IAEf;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IAEpB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAE3B,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B,cAAc,CAAC,EAAE,cAAc,CAAA;IAE/B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,SAAS,GAAG,UAAU,CAAA;IAC5B,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE;QACR,IAAI,EAAE,WAAW,CAAA;QACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;QACtB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;QACtB,mEAAmE;QACnE,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;QACrC,kEAAkE;QAClE,SAAS,CAAC,EAAE,SAAS,OAAO,qBAAqB,EAAE,QAAQ,EAAE,CAAA;KAC7D,CAAA;IACD,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,GAAG,gBAAgB,CAAA;IACjE,KAAK,EAAE,UAAU,CAAA;CACjB"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/types/provider/chat.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export type { ToolChoice, ResponseFormat, CacheControl, ChatCompletionParams, ChatCompletionResponse, ThinkingConfig, } from './chat.js';
1
+ export type { ToolChoice, ResponseFormat, CacheControl, ChatCompletionParams, ChatCompletionResponse, ReasoningEffort, ThinkingConfig, } from './chat.js';
2
2
  export type { StreamChunk } from './stream.js';
3
3
  export type { ModelInfo } from './model.js';
4
4
  export type { LLMProvider } from './interface.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/provider/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,GACd,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,YAAY,EACX,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,GACxB,MAAM,YAAY,CAAA;AACnB,YAAY,EACX,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,GACtB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/provider/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,cAAc,GACd,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,YAAY,EACX,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,GACxB,MAAM,YAAY,CAAA;AACnB,YAAY,EACX,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,GACtB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -300,6 +300,12 @@ export class SupervisorAgent extends AbstractAgent<SupervisorAgentConfig, Superv
300
300
  // auto-approve. drainQuery falls back to autoApproveHandler
301
301
  // when resumeHandler is omitted (= same behaviour as before).
302
302
  ...(config.resumeHandler ? { resumeHandler: config.resumeHandler } : {}),
303
+ // Forwarded for the same reason the handler is. A capability the
304
+ // kernel honours in `drainQuery` but that never reaches the
305
+ // surface a host actually constructs is a capability nobody can
306
+ // use — which is the shape of defect this file has already been
307
+ // corrected for twice.
308
+ ...(config.steering ? { steering: config.steering } : {}),
303
309
  ...(config.verificationGate ? { verificationGate: config.verificationGate } : {}),
304
310
  ...(config.sandboxProvider ? { sandboxProvider: config.sandboxProvider } : {}),
305
311
  // Working-memory / compaction seam (optional; absent => unchanged
@@ -0,0 +1,165 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ import { MockLLMProvider, registerMock } from '../../provider/index.js'
5
+ import { ToolRegistry } from '../../registry/index.js'
6
+ import { runAgent } from '../runAgent.js'
7
+
8
+ /**
9
+ * `drainQuery` takes eleven required parameters, four of which throw when
10
+ * missing. That is right for a kernel and wrong for the first thing anybody
11
+ * writes — and the proof was in this repo, where the eval suites, the test
12
+ * files and the CLI each hand-assembled the same block.
13
+ *
14
+ * These pin the two things a front door has to get right: that the short form
15
+ * works at all, and that the identity it invents comes back, because a
16
+ * generated session that a caller cannot recover is a conversation that
17
+ * silently restarts on turn two.
18
+ */
19
+
20
+ registerMock()
21
+
22
+ describe('running an agent through the front door', () => {
23
+ it('runs from a provider, a model and a prompt', async () => {
24
+ const { output, run } = await runAgent({
25
+ provider: new MockLLMProvider({ turns: [{ text: 'four' }] }),
26
+ model: 'mock-model',
27
+ prompt: 'What is 2 + 2?',
28
+ })
29
+
30
+ expect(output).toBe('four')
31
+ expect(run.status).toBe('completed')
32
+ expect(run.stopReason).toBe('end_turn')
33
+ })
34
+
35
+ it('hands back the identity it generated, so a second turn can continue', async () => {
36
+ const first = await runAgent({
37
+ provider: new MockLLMProvider({ turns: [{ text: 'noted' }] }),
38
+ model: 'mock-model',
39
+ prompt: 'My name is Ada.',
40
+ })
41
+
42
+ expect(first.identity.sessionId).toBeTruthy()
43
+ expect(first.identity.tenantId).toBeTruthy()
44
+
45
+ const second = await runAgent({
46
+ provider: new MockLLMProvider({ turns: [{ text: 'Ada' }] }),
47
+ model: 'mock-model',
48
+ prompt: 'What is my name?',
49
+ ...first.identity,
50
+ })
51
+
52
+ // The same session, not a new one that happens to work.
53
+ expect(second.identity).toEqual(first.identity)
54
+ })
55
+
56
+ it('generates a distinct identity per run when none is given', async () => {
57
+ const a = await runAgent({
58
+ provider: new MockLLMProvider({ turns: [{ text: 'a' }] }),
59
+ model: 'mock-model',
60
+ prompt: 'x',
61
+ })
62
+ const b = await runAgent({
63
+ provider: new MockLLMProvider({ turns: [{ text: 'b' }] }),
64
+ model: 'mock-model',
65
+ prompt: 'y',
66
+ })
67
+
68
+ expect(a.identity.sessionId).not.toBe(b.identity.sessionId)
69
+ })
70
+
71
+ it('carries prior messages when the prompt is a history', async () => {
72
+ const provider = new MockLLMProvider({ turns: [{ text: 'Ada' }] })
73
+
74
+ await runAgent({
75
+ provider,
76
+ model: 'mock-model',
77
+ prompt: [
78
+ { role: 'user', content: 'My name is Ada.' },
79
+ { role: 'assistant', content: 'Noted.' },
80
+ { role: 'user', content: 'What is my name?' },
81
+ ] as never,
82
+ })
83
+
84
+ const sent = provider.requests[0]?.messages.map((m) => m.content) ?? []
85
+ expect(sent).toContain('My name is Ada.')
86
+ expect(sent).toContain('What is my name?')
87
+ })
88
+
89
+ it('passes instructions through as the system prompt', async () => {
90
+ const provider = new MockLLMProvider({ turns: [{ text: 'ok' }] })
91
+
92
+ await runAgent({
93
+ provider,
94
+ model: 'mock-model',
95
+ prompt: 'hello',
96
+ instructions: 'You only answer in haiku.',
97
+ })
98
+
99
+ const system = JSON.stringify(provider.requests[0]?.messages ?? [])
100
+ expect(system).toContain('You only answer in haiku.')
101
+ })
102
+
103
+ it('runs tools when given a registry', async () => {
104
+ const tools = new ToolRegistry()
105
+ let ran = false
106
+ tools.register({
107
+ name: 'ping',
108
+ description: 'pings',
109
+ inputSchema: z.object({}),
110
+ category: 'custom',
111
+ permissions: [],
112
+ readOnly: true,
113
+ destructive: false,
114
+ concurrencySafe: true,
115
+ execute: async () => {
116
+ ran = true
117
+ return { success: true, output: 'pong' }
118
+ },
119
+ } as never)
120
+
121
+ const { output } = await runAgent({
122
+ provider: new MockLLMProvider({
123
+ turns: [{ toolCalls: [{ id: 'c1', name: 'ping', rawArguments: '{}' }] }, { text: 'done' }],
124
+ }),
125
+ model: 'mock-model',
126
+ prompt: 'ping it',
127
+ tools,
128
+ })
129
+
130
+ expect(ran).toBe(true)
131
+ expect(output).toBe('done')
132
+ })
133
+
134
+ it('caps a runaway loop on its own default', async () => {
135
+ const tools = new ToolRegistry()
136
+ tools.register({
137
+ name: 'again',
138
+ description: 'always asks for more',
139
+ inputSchema: z.object({}),
140
+ category: 'custom',
141
+ permissions: [],
142
+ readOnly: true,
143
+ destructive: false,
144
+ concurrencySafe: true,
145
+ execute: async () => ({ success: true, output: 'and again' }),
146
+ } as never)
147
+
148
+ // The point of a default budget is that a caller who set none is still
149
+ // protected. Two iterations here rather than the default sixteen, so
150
+ // the test pins the mechanism without paying for it.
151
+ const { run } = await runAgent({
152
+ provider: new MockLLMProvider({
153
+ turns: Array.from({ length: 10 }, () => ({
154
+ toolCalls: [{ id: 'c', name: 'again', rawArguments: '{}' }],
155
+ })),
156
+ }),
157
+ model: 'mock-model',
158
+ prompt: 'loop',
159
+ tools,
160
+ maxIterations: 2,
161
+ })
162
+
163
+ expect(run.currentIteration).toBeLessThanOrEqual(2)
164
+ })
165
+ })
@@ -7,3 +7,5 @@ export { defineAgent } from './defineAgent.js'
7
7
  export type { DefineAgentOptions } from './defineAgent.js'
8
8
  export { InvocationLock, ConcurrentInvocationError } from './lock.js'
9
9
  export type { Disposable } from './lock.js'
10
+ export { runAgent } from './runAgent.js'
11
+ export type { AgentIdentity, RunAgentOptions, RunAgentResult } from './runAgent.js'
@@ -0,0 +1,179 @@
1
+ import { ToolRegistry } from '../registry/tool/execute.js'
2
+ import { drainQuery } from '../runtime/query/index.js'
3
+ import type { ProjectId, SessionId, TenantId, ThreadId } from '../types/ids/index.js'
4
+ import type { Message } from '../types/message/index.js'
5
+ import type { LLMProvider } from '../types/provider/index.js'
6
+ import type { Run, RunEventListener } from '../types/run/index.js'
7
+ import type { ToolRegistryContract } from '../types/tool/index.js'
8
+ import {
9
+ generateProjectId,
10
+ generateSessionId,
11
+ generateTenantId,
12
+ generateThreadId,
13
+ } from '../utils/id.js'
14
+
15
+ /**
16
+ * The session a run belongs to.
17
+ *
18
+ * Every field is generated when absent, and the generated values come back on
19
+ * the result so a second turn can be handed the same ones. That pairing is the
20
+ * point: auto-generating alone would make each call its own session, which is
21
+ * right for a one-shot and silently wrong for a conversation — the second turn
22
+ * would start with no history and no shared budget, and nothing would say so.
23
+ */
24
+ export interface AgentIdentity {
25
+ sessionId?: SessionId
26
+ threadId?: ThreadId
27
+ projectId?: ProjectId
28
+ tenantId?: TenantId
29
+ }
30
+
31
+ export interface RunAgentOptions extends AgentIdentity {
32
+ /** The model driver. The one thing with no sensible default. */
33
+ provider: LLMProvider
34
+
35
+ /** What to ask. A string is turned into a single user message. */
36
+ prompt: string | Message[]
37
+
38
+ /** The system prompt. */
39
+ instructions?: string
40
+
41
+ /**
42
+ * Model id.
43
+ *
44
+ * Required, and not defaulted from the provider, because `LLMProvider`
45
+ * carries no model — a driver may have been constructed with one, but the
46
+ * interface does not expose it, so anything this function picked would be
47
+ * a guess billed to the caller. Two required options is a shape someone
48
+ * can hold in their head; a wrong model quietly used is not.
49
+ */
50
+ model: string
51
+
52
+ tools?: ToolRegistryContract
53
+
54
+ /** Defaults to the current working directory. */
55
+ workingDirectory?: string
56
+
57
+ maxIterations?: number
58
+ tokenBudget?: number
59
+ timeoutMs?: number
60
+ temperature?: number
61
+
62
+ /** Names the agent in traces and events. Defaults to `Agent`. */
63
+ name?: string
64
+
65
+ signal?: AbortSignal
66
+ listener?: RunEventListener
67
+ }
68
+
69
+ export interface RunAgentResult {
70
+ /** The model's final text, or `undefined` if it produced none. */
71
+ readonly output: string | undefined
72
+
73
+ /** The full run — usage, cost, steps, stop reason, every message. */
74
+ readonly run: Run
75
+
76
+ /**
77
+ * The identity this run used, with anything generated filled in.
78
+ *
79
+ * Pass it straight back into the next call to continue the same session.
80
+ */
81
+ readonly identity: Required<AgentIdentity>
82
+ }
83
+
84
+ /**
85
+ * Defaults chosen to be safe rather than generous.
86
+ *
87
+ * A front door exists so a first run works without a decision, and the cost of
88
+ * that convenience is that nobody reads these numbers before their first
89
+ * runaway loop. So: a budget that ends a stuck run in seconds rather than
90
+ * dollars, and an iteration cap that stops a tool-calling loop well before a
91
+ * context window does. Every one is overridable and named on the option.
92
+ */
93
+ const DEFAULT_MAX_ITERATIONS = 16
94
+ const DEFAULT_TOKEN_BUDGET = 200_000
95
+ const DEFAULT_TIMEOUT_MS = 300_000
96
+
97
+ /**
98
+ * Run an agent, without assembling a kernel by hand.
99
+ *
100
+ * `drainQuery` is the kernel's real entry point and takes eleven required
101
+ * parameters, four of which are identity fields that throw when missing. That
102
+ * is the correct shape for a kernel — a run with no tenant is a run no auditor
103
+ * can attribute — and it is the wrong shape for the first thing anybody
104
+ * writes. The proof was in this repo: the eval suites, the test files and the
105
+ * CLI each hand-assemble the same block, which is what a missing front door
106
+ * looks like from the inside.
107
+ *
108
+ * So this supplies an environment rather than a new engine. It generates the
109
+ * identity a single-tenant local run has no opinion about, defaults the
110
+ * budgets, points the working directory at the process's own, and hands back
111
+ * both the answer and the identity it used. Everything it fills in is a normal
112
+ * `drainQuery` parameter; there is no second code path, and a caller who
113
+ * outgrows it passes more options until they are calling `drainQuery` in all
114
+ * but name.
115
+ *
116
+ * ```ts
117
+ * const { output } = await runAgent({
118
+ * provider,
119
+ * model: 'claude-sonnet-4-5',
120
+ * prompt: 'What is 2 + 2?',
121
+ * })
122
+ * ```
123
+ *
124
+ * A second turn in the same session is the identity handed back, and the
125
+ * previous messages carried forward:
126
+ *
127
+ * ```ts
128
+ * const first = await runAgent({ provider, model, prompt: 'My name is Ada.' })
129
+ *
130
+ * const second = await runAgent({
131
+ * provider,
132
+ * model,
133
+ * ...first.identity,
134
+ * prompt: [...first.run.messages, createUserMessage('What is my name?')],
135
+ * })
136
+ * ```
137
+ */
138
+ export async function runAgent(options: RunAgentOptions): Promise<RunAgentResult> {
139
+ const identity: Required<AgentIdentity> = {
140
+ sessionId: options.sessionId ?? generateSessionId(),
141
+ threadId: options.threadId ?? generateThreadId(),
142
+ projectId: options.projectId ?? generateProjectId(),
143
+ tenantId: options.tenantId ?? generateTenantId(),
144
+ }
145
+
146
+ const messages: Message[] =
147
+ typeof options.prompt === 'string'
148
+ ? [{ role: 'user', content: options.prompt, timestamp: Date.now() } as Message]
149
+ : options.prompt
150
+
151
+ const run = await drainQuery(
152
+ {
153
+ provider: options.provider,
154
+ tools: options.tools ?? new ToolRegistry(),
155
+ messages,
156
+ workingDirectory: options.workingDirectory ?? process.cwd(),
157
+ runConfig: {
158
+ model: options.model,
159
+ maxIterations: options.maxIterations ?? DEFAULT_MAX_ITERATIONS,
160
+ tokenBudget: options.tokenBudget ?? DEFAULT_TOKEN_BUDGET,
161
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
162
+ ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
163
+ },
164
+ // One option covers both. `drainQuery` separates the id from the
165
+ // display name because a fleet needs a stable key and a readable
166
+ // label; a single agent has no such tension, and asking for two
167
+ // strings that will always be the same is the kind of ceremony this
168
+ // function exists to remove.
169
+ agentId: options.name ?? 'agent',
170
+ agentName: options.name ?? 'Agent',
171
+ ...(options.instructions ? { systemPrompt: options.instructions } : {}),
172
+ ...(options.signal ? { signal: options.signal } : {}),
173
+ ...identity,
174
+ } as never,
175
+ options.listener,
176
+ )
177
+
178
+ return { output: run.result, run, identity }
179
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { assertThinkingUnsupported } from '../thinking-support.js'
4
+
5
+ /**
6
+ * `thinking` sits on `ChatCompletionParams`, so every driver accepts it. Six
7
+ * of the seven in this repo did not implement it, and five of those six simply
8
+ * dropped the field: the caller got an ordinary completion with an empty
9
+ * `reasoning` array, indistinguishable from a model that chose not to reason.
10
+ *
11
+ * One driver already refused instead, with the reasoning written out. The rule
12
+ * was decided once and applied once — so it moved here, where a new driver
13
+ * inherits it rather than re-deciding it.
14
+ */
15
+ describe('a driver that cannot think says so', () => {
16
+ it('refuses a manual thinking request', () => {
17
+ expect(() =>
18
+ assertThinkingUnsupported('TestProvider', { thinking: { type: 'enabled' } }),
19
+ ).toThrow(/TestProvider does not implement thinking/)
20
+ })
21
+
22
+ it('refuses an adaptive one too', () => {
23
+ // Both are a request to think. Refusing one and dropping the other
24
+ // would leave exactly the silence this exists to remove.
25
+ expect(() =>
26
+ assertThinkingUnsupported('TestProvider', { thinking: { type: 'adaptive' } }),
27
+ ).toThrow(/does not implement thinking/)
28
+ })
29
+
30
+ it('names the driver, not just the problem', () => {
31
+ // In a multi-provider setup this is the difference between a bug
32
+ // report about the model and a one-line config fix.
33
+ expect(() =>
34
+ assertThinkingUnsupported('BedrockProvider', { thinking: { type: 'adaptive' } }),
35
+ ).toThrow(/BedrockProvider/)
36
+ })
37
+
38
+ it('says what silence would have looked like', () => {
39
+ let message = ''
40
+ try {
41
+ assertThinkingUnsupported('TestProvider', { thinking: { type: 'enabled' } })
42
+ } catch (err) {
43
+ message = (err as Error).message
44
+ }
45
+ expect(message).toContain('empty reasoning list')
46
+ expect(message).toContain('Drop `thinking`')
47
+ })
48
+
49
+ it('honours an explicit disable as a no-op', () => {
50
+ // A config shared across providers that says "do not think" should not
51
+ // fail on the ones that were never going to.
52
+ expect(() =>
53
+ assertThinkingUnsupported('TestProvider', { thinking: { type: 'disabled' } }),
54
+ ).not.toThrow()
55
+ })
56
+
57
+ it('does nothing when the caller said nothing', () => {
58
+ expect(() => assertThinkingUnsupported('TestProvider', {})).not.toThrow()
59
+ })
60
+ })
@@ -0,0 +1,37 @@
1
+ import type { ThinkingConfig } from '../types/provider/index.js'
2
+
3
+ /**
4
+ * Refuse a thinking request a driver does not implement.
5
+ *
6
+ * The failure this prevents is silence. `thinking` sits on
7
+ * `ChatCompletionParams`, so every driver accepts it; a driver that does not
8
+ * implement it drops the field and returns an ordinary completion with an
9
+ * empty `reasoning` array. The caller cannot tell that apart from a model that
10
+ * simply chose not to reason — the request looks honoured and the answer looks
11
+ * like an answer.
12
+ *
13
+ * Refusing names the driver instead, which is the difference between a bug
14
+ * report about the model and a one-line configuration fix.
15
+ *
16
+ * **Turning thinking OFF is honoured as a no-op**, because that is the state a
17
+ * driver without thinking is already in. A config shared across providers that
18
+ * says `{type: 'disabled'}` should not fail on the ones that were never going
19
+ * to think.
20
+ *
21
+ * This lived as a private copy inside one driver while five others dropped the
22
+ * field silently. It is here so a new driver inherits the rule instead of
23
+ * re-deciding it.
24
+ *
25
+ * @param driverName Named in the error, so the reader knows which provider in
26
+ * a multi-provider setup refused.
27
+ */
28
+ export function assertThinkingUnsupported(
29
+ driverName: string,
30
+ params: { thinking?: ThinkingConfig },
31
+ ): void {
32
+ const type = params.thinking?.type
33
+ if (type !== 'enabled' && type !== 'adaptive') return
34
+ throw new Error(
35
+ `${driverName} does not implement thinking. Silently ignoring the request would return an ordinary completion with an empty reasoning list, which reads as "the model did not reason" rather than "this driver cannot ask it to". Drop \`thinking\`, or use a driver that implements it.`,
36
+ )
37
+ }
@@ -67,7 +67,14 @@ export { extractFinalResponse } from './utils/conversation.js'
67
67
  // ─── router, runtime, run ────────────────────────────────────────────────
68
68
 
69
69
  export { resolveTaskModel } from './router/task-router.js'
70
+ // Every driver accepts `thinking`; one that does not implement it must
71
+ // refuse rather than drop it. Shared so a new driver inherits the rule.
72
+ export { assertThinkingUnsupported } from './provider/thinking-support.js'
70
73
  export { drainQuery, query } from './runtime/query/index.js'
74
+ // Mid-run guidance. A host holds the channel and the loop drains it at the
75
+ // tool-result boundary; see the module for why that is the only legal slot.
76
+ export { SteeringBinding, attachSteering, formatSteeringNote } from './runtime/query/steering.js'
77
+ export type { SteeringChannel } from './runtime/query/steering.js'
71
78
  export { createMockBidiProvider, startBidiRun } from './runtime/bidi/index.js'
72
79
  export { ContextCache } from './runtime/query/context-cache.js'
73
80
  export {
@@ -135,6 +142,9 @@ export {
135
142
  PipelineAgent,
136
143
  ReactiveAgent,
137
144
  RouterAgent,
145
+ // The short path: provider + model + prompt. Assembles the identity and
146
+ // budgets `drainQuery` requires and hands the generated identity back.
147
+ runAgent,
138
148
  SupervisorAgent,
139
149
  } from './agents/index.js'
140
150
 
@@ -99,7 +99,13 @@ export type { ContextCacheConfig, PromptCacheInput } from './runtime/query/conte
99
99
 
100
100
  export type { LimitCheckResult, LimitCheckerState, RunReporter } from './run/index.js'
101
101
 
102
- export type { DefineAgentOptions, Disposable } from './agents/index.js'
102
+ export type {
103
+ AgentIdentity,
104
+ DefineAgentOptions,
105
+ Disposable,
106
+ RunAgentOptions,
107
+ RunAgentResult,
108
+ } from './agents/index.js'
103
109
 
104
110
  export type {
105
111
  ActivityEvent,