@namzu/sdk 50.1.0 → 51.1.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 (161) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  3. package/dist/bridge/sse/mapper.js +1 -0
  4. package/dist/bridge/sse/mapper.js.map +1 -1
  5. package/dist/execution/code-runtime/types.d.ts +35 -0
  6. package/dist/execution/code-runtime/types.d.ts.map +1 -1
  7. package/dist/execution/code-runtime/types.js.map +1 -1
  8. package/dist/execution/code-runtime/worker-program.d.ts +1 -1
  9. package/dist/execution/code-runtime/worker-program.d.ts.map +1 -1
  10. package/dist/execution/code-runtime/worker-program.js +72 -13
  11. package/dist/execution/code-runtime/worker-program.js.map +1 -1
  12. package/dist/execution/code-runtime/worker.d.ts.map +1 -1
  13. package/dist/execution/code-runtime/worker.js +35 -1
  14. package/dist/execution/code-runtime/worker.js.map +1 -1
  15. package/dist/execution/workflow/errors.d.ts +5 -0
  16. package/dist/execution/workflow/errors.d.ts.map +1 -0
  17. package/dist/execution/workflow/errors.js +8 -0
  18. package/dist/execution/workflow/errors.js.map +1 -0
  19. package/dist/execution/workflow/index.d.ts +9 -0
  20. package/dist/execution/workflow/index.d.ts.map +1 -0
  21. package/dist/execution/workflow/index.js +6 -0
  22. package/dist/execution/workflow/index.js.map +1 -0
  23. package/dist/execution/workflow/journal.d.ts +18 -0
  24. package/dist/execution/workflow/journal.d.ts.map +1 -0
  25. package/dist/execution/workflow/journal.js +43 -0
  26. package/dist/execution/workflow/journal.js.map +1 -0
  27. package/dist/execution/workflow/meta.d.ts +14 -0
  28. package/dist/execution/workflow/meta.d.ts.map +1 -0
  29. package/dist/execution/workflow/meta.js +314 -0
  30. package/dist/execution/workflow/meta.js.map +1 -0
  31. package/dist/execution/workflow/phases.d.ts +32 -0
  32. package/dist/execution/workflow/phases.d.ts.map +1 -0
  33. package/dist/execution/workflow/phases.js +57 -0
  34. package/dist/execution/workflow/phases.js.map +1 -0
  35. package/dist/execution/workflow/prelude.d.ts +15 -0
  36. package/dist/execution/workflow/prelude.d.ts.map +1 -0
  37. package/dist/execution/workflow/prelude.js +128 -0
  38. package/dist/execution/workflow/prelude.js.map +1 -0
  39. package/dist/execution/workflow/runtime.d.ts +31 -0
  40. package/dist/execution/workflow/runtime.d.ts.map +1 -0
  41. package/dist/execution/workflow/runtime.js +682 -0
  42. package/dist/execution/workflow/runtime.js.map +1 -0
  43. package/dist/execution/workflow/schema.d.ts +10 -0
  44. package/dist/execution/workflow/schema.d.ts.map +1 -0
  45. package/dist/execution/workflow/schema.js +145 -0
  46. package/dist/execution/workflow/schema.js.map +1 -0
  47. package/dist/execution/workflow/types.d.ts +267 -0
  48. package/dist/execution/workflow/types.d.ts.map +1 -0
  49. package/dist/execution/workflow/types.js +7 -0
  50. package/dist/execution/workflow/types.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/plugin/shell-hook.d.ts.map +1 -1
  56. package/dist/plugin/shell-hook.js +2 -1
  57. package/dist/plugin/shell-hook.js.map +1 -1
  58. package/dist/process/kill-tree.d.ts +7 -0
  59. package/dist/process/kill-tree.d.ts.map +1 -1
  60. package/dist/process/kill-tree.js +41 -0
  61. package/dist/process/kill-tree.js.map +1 -1
  62. package/dist/provider/errors.d.ts +10 -0
  63. package/dist/provider/errors.d.ts.map +1 -1
  64. package/dist/provider/errors.js +16 -0
  65. package/dist/provider/errors.js.map +1 -1
  66. package/dist/provider/retry.d.ts +27 -0
  67. package/dist/provider/retry.d.ts.map +1 -1
  68. package/dist/provider/retry.js +19 -3
  69. package/dist/provider/retry.js.map +1 -1
  70. package/dist/provider/token-budget.d.ts.map +1 -1
  71. package/dist/provider/token-budget.js +31 -2
  72. package/dist/provider/token-budget.js.map +1 -1
  73. package/dist/public-runtime.d.ts +1 -0
  74. package/dist/public-runtime.d.ts.map +1 -1
  75. package/dist/public-runtime.js +3 -0
  76. package/dist/public-runtime.js.map +1 -1
  77. package/dist/public-tools.d.ts +1 -1
  78. package/dist/public-tools.d.ts.map +1 -1
  79. package/dist/public-tools.js +1 -1
  80. package/dist/public-tools.js.map +1 -1
  81. package/dist/public-types.d.ts +3 -0
  82. package/dist/public-types.d.ts.map +1 -1
  83. package/dist/runtime/jobs/job-id.d.ts +21 -0
  84. package/dist/runtime/jobs/job-id.d.ts.map +1 -0
  85. package/dist/runtime/jobs/job-id.js +66 -0
  86. package/dist/runtime/jobs/job-id.js.map +1 -0
  87. package/dist/runtime/jobs/registry.d.ts +0 -1
  88. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  89. package/dist/runtime/jobs/registry.js +2 -3
  90. package/dist/runtime/jobs/registry.js.map +1 -1
  91. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/stream-turn.js +24 -1
  93. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  94. package/dist/runtime/query/prompt.d.ts.map +1 -1
  95. package/dist/runtime/query/prompt.js +5 -1
  96. package/dist/runtime/query/prompt.js.map +1 -1
  97. package/dist/tools/builtins/bash.d.ts +8 -1
  98. package/dist/tools/builtins/bash.d.ts.map +1 -1
  99. package/dist/tools/builtins/bash.js +20 -3
  100. package/dist/tools/builtins/bash.js.map +1 -1
  101. package/dist/tools/builtins/job.d.ts.map +1 -1
  102. package/dist/tools/builtins/job.js +4 -0
  103. package/dist/tools/builtins/job.js.map +1 -1
  104. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  105. package/dist/tools/builtins/wait-for-job.js +4 -0
  106. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  107. package/dist/tools/command-shell.d.ts +55 -0
  108. package/dist/tools/command-shell.d.ts.map +1 -1
  109. package/dist/tools/command-shell.js +174 -13
  110. package/dist/tools/command-shell.js.map +1 -1
  111. package/dist/types/provider/error.d.ts +6 -0
  112. package/dist/types/provider/error.d.ts.map +1 -1
  113. package/dist/types/provider/errors.d.ts.map +1 -1
  114. package/dist/types/provider/errors.js +15 -1
  115. package/dist/types/provider/errors.js.map +1 -1
  116. package/dist/types/provider/stream.d.ts +6 -0
  117. package/dist/types/provider/stream.d.ts.map +1 -1
  118. package/dist/types/session/events.d.ts +5 -0
  119. package/dist/types/session/events.d.ts.map +1 -1
  120. package/dist/types/session/events.js.map +1 -1
  121. package/dist/utils/backoff.d.ts +12 -0
  122. package/dist/utils/backoff.d.ts.map +1 -1
  123. package/dist/utils/backoff.js +15 -0
  124. package/dist/utils/backoff.js.map +1 -1
  125. package/package.json +1 -1
  126. package/src/agents/AGENTS.md +134 -46
  127. package/src/bridge/sse/mapper.ts +1 -0
  128. package/src/execution/code-runtime/types.ts +37 -1
  129. package/src/execution/code-runtime/worker-program.ts +72 -13
  130. package/src/execution/code-runtime/worker.ts +40 -1
  131. package/src/execution/workflow/errors.ts +7 -0
  132. package/src/execution/workflow/index.ts +8 -0
  133. package/src/execution/workflow/journal.ts +46 -0
  134. package/src/execution/workflow/meta.ts +316 -0
  135. package/src/execution/workflow/phases.ts +91 -0
  136. package/src/execution/workflow/prelude.ts +135 -0
  137. package/src/execution/workflow/runtime.ts +786 -0
  138. package/src/execution/workflow/schema.ts +137 -0
  139. package/src/execution/workflow/types.ts +300 -0
  140. package/src/index.ts +4 -0
  141. package/src/plugin/shell-hook.ts +2 -1
  142. package/src/process/kill-tree.ts +43 -0
  143. package/src/provider/errors.ts +18 -0
  144. package/src/provider/retry.ts +53 -3
  145. package/src/provider/token-budget.ts +31 -1
  146. package/src/public-runtime.ts +11 -0
  147. package/src/public-tools.ts +6 -0
  148. package/src/public-types.ts +3 -0
  149. package/src/runtime/jobs/job-id.ts +66 -0
  150. package/src/runtime/jobs/registry.ts +2 -3
  151. package/src/runtime/query/iteration/stream-turn.ts +24 -1
  152. package/src/runtime/query/prompt.ts +5 -1
  153. package/src/tools/builtins/bash.ts +33 -3
  154. package/src/tools/builtins/job.ts +4 -0
  155. package/src/tools/builtins/wait-for-job.ts +4 -0
  156. package/src/tools/command-shell.ts +194 -11
  157. package/src/types/provider/error.ts +6 -0
  158. package/src/types/provider/errors.ts +16 -1
  159. package/src/types/provider/stream.ts +6 -0
  160. package/src/types/session/events.ts +5 -0
  161. package/src/utils/backoff.ts +21 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A small JSON Schema validator for `agent(..., { schema })`.
3
+ *
4
+ * Supported keywords: `type` (string or list), `enum`, `const`, `properties`,
5
+ * `required`, `additionalProperties` (boolean or schema), `items` (one
6
+ * schema), `minItems`, `maxItems`, `minLength`, `maxLength`, `minimum`,
7
+ * `maximum`, `anyOf`, `oneOf`, `allOf`. Any other keyword is ignored, and the
8
+ * schema is also handed to the host so a model-side structured-output
9
+ * mechanism can enforce more. `pattern` is deliberately not supported: the
10
+ * schema is model-authored and a regular expression is a CPU risk.
11
+ */
12
+ const MAX_DEPTH = 32
13
+
14
+ function typeOf(value: unknown): string {
15
+ if (value === null) return 'null'
16
+ if (Array.isArray(value)) return 'array'
17
+ if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number'
18
+ return typeof value
19
+ }
20
+
21
+ function matchesType(value: unknown, type: string): boolean {
22
+ const actual = typeOf(value)
23
+ return actual === type || (type === 'number' && actual === 'integer')
24
+ }
25
+
26
+ function same(a: unknown, b: unknown): boolean {
27
+ return JSON.stringify(a) === JSON.stringify(b)
28
+ }
29
+
30
+ function isSchema(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
32
+ }
33
+
34
+ function check(value: unknown, schema: unknown, path: string, depth: number): string | undefined {
35
+ if (schema === true || schema === undefined) return undefined
36
+ if (schema === false) return `${path}: nothing is allowed here`
37
+ if (!isSchema(schema)) return undefined
38
+ if (depth > MAX_DEPTH) return `${path}: schema is nested too deeply`
39
+ const here = path || 'the result'
40
+
41
+ const type = schema.type
42
+ if (typeof type === 'string' && !matchesType(value, type)) {
43
+ return `${here} must be ${type}, got ${typeOf(value)}`
44
+ }
45
+ if (Array.isArray(type) && !type.some((t) => typeof t === 'string' && matchesType(value, t))) {
46
+ return `${here} must be one of ${type.join(', ')}, got ${typeOf(value)}`
47
+ }
48
+ if (Array.isArray(schema.enum) && !schema.enum.some((e) => same(e, value))) {
49
+ return `${here} must be one of ${JSON.stringify(schema.enum)}`
50
+ }
51
+ if ('const' in schema && !same(schema.const, value)) {
52
+ return `${here} must equal ${JSON.stringify(schema.const)}`
53
+ }
54
+ if (typeof value === 'string') {
55
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
56
+ return `${here} is shorter than ${schema.minLength} characters`
57
+ }
58
+ if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
59
+ return `${here} is longer than ${schema.maxLength} characters`
60
+ }
61
+ }
62
+ if (typeof value === 'number') {
63
+ if (typeof schema.minimum === 'number' && value < schema.minimum) {
64
+ return `${here} is below ${schema.minimum}`
65
+ }
66
+ if (typeof schema.maximum === 'number' && value > schema.maximum) {
67
+ return `${here} is above ${schema.maximum}`
68
+ }
69
+ }
70
+ if (Array.isArray(value)) {
71
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
72
+ return `${here} has fewer than ${schema.minItems} items`
73
+ }
74
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
75
+ return `${here} has more than ${schema.maxItems} items`
76
+ }
77
+ if (schema.items !== undefined) {
78
+ for (const [i, item] of value.entries()) {
79
+ const problem = check(item, schema.items, `${path}[${i}]`, depth + 1)
80
+ if (problem) return problem
81
+ }
82
+ }
83
+ }
84
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
85
+ const object = value as Record<string, unknown>
86
+ if (Array.isArray(schema.required)) {
87
+ for (const key of schema.required) {
88
+ if (typeof key === 'string' && !Object.hasOwn(object, key)) {
89
+ return `${here} is missing "${key}"`
90
+ }
91
+ }
92
+ }
93
+ const properties = isSchema(schema.properties) ? schema.properties : {}
94
+ for (const [key, child] of Object.entries(object)) {
95
+ const childPath = path ? `${path}.${key}` : key
96
+ if (Object.hasOwn(properties, key)) {
97
+ const problem = check(child, properties[key], childPath, depth + 1)
98
+ if (problem) return problem
99
+ } else if (schema.additionalProperties === false) {
100
+ return `${here} has an unexpected property "${key}"`
101
+ } else if (isSchema(schema.additionalProperties)) {
102
+ const problem = check(child, schema.additionalProperties, childPath, depth + 1)
103
+ if (problem) return problem
104
+ }
105
+ }
106
+ }
107
+ if (Array.isArray(schema.allOf)) {
108
+ for (const part of schema.allOf) {
109
+ const problem = check(value, part, path, depth + 1)
110
+ if (problem) return problem
111
+ }
112
+ }
113
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((p) => !check(value, p, path, depth + 1))) {
114
+ return `${here} matches none of the allowed shapes`
115
+ }
116
+ if (Array.isArray(schema.oneOf)) {
117
+ const matching = schema.oneOf.filter((p) => !check(value, p, path, depth + 1)).length
118
+ if (matching !== 1) return `${here} must match exactly one allowed shape, matches ${matching}`
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ /** `undefined` when `value` satisfies `schema`, otherwise a plain sentence naming the first problem. */
124
+ export function validateAgainstSchema(value: unknown, schema: unknown): string | undefined {
125
+ return check(value, schema, '', 0)
126
+ }
127
+
128
+ /** Parse a model's text as JSON, tolerating one surrounding code fence. */
129
+ export function parseJsonText(text: string): { ok: true; value: unknown } | { ok: false } {
130
+ const trimmed = text.trim()
131
+ const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/.exec(trimmed)
132
+ try {
133
+ return { ok: true, value: JSON.parse(fenced ? (fenced[1] as string) : trimmed) }
134
+ } catch {
135
+ return { ok: false }
136
+ }
137
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Types of the workflow runtime: a script the model wrote once decides how
3
+ * many subagents run, with which prompts and under which phase titles; the
4
+ * runtime schedules them. See `docs/sdk/workflows.md`.
5
+ */
6
+
7
+ /** One phase declared in `meta.phases`. */
8
+ export interface WorkflowPhaseMeta {
9
+ readonly title: string
10
+ readonly detail?: string
11
+ readonly model?: string
12
+ }
13
+
14
+ /** The pure literal a script exports as `meta`. */
15
+ export interface WorkflowMeta {
16
+ readonly name: string
17
+ readonly description: string
18
+ readonly whenToUse?: string
19
+ readonly phases: readonly WorkflowPhaseMeta[]
20
+ }
21
+
22
+ /** What the host is asked to run for one `agent()` call. */
23
+ export interface WorkflowAgentRequest {
24
+ /** 1-based, in the order the script asked. */
25
+ readonly index: number
26
+ readonly label: string
27
+ readonly prompt: string
28
+ readonly phaseIndex?: number
29
+ readonly phaseTitle?: string
30
+ /** JSON Schema the result must satisfy; the runtime validates `data`. */
31
+ readonly schema?: Readonly<Record<string, unknown>>
32
+ readonly agentType?: string
33
+ readonly model?: string
34
+ readonly effort?: string
35
+ /** Report live progress; the runtime turns it into `workflow_agent` events. */
36
+ readonly onProgress: (progress: WorkflowAgentProgress) => void
37
+ }
38
+
39
+ export interface WorkflowAgentProgress {
40
+ readonly tokens?: number
41
+ readonly toolCalls?: number
42
+ readonly lastToolName?: string
43
+ readonly lastToolSummary?: string
44
+ }
45
+
46
+ /** What the host's agent run came to. */
47
+ export interface WorkflowAgentOutcome {
48
+ readonly state: 'done' | 'error' | 'skipped'
49
+ /** The agent's final text. */
50
+ readonly text?: string
51
+ /** A structured result, when the host produced one for the schema. */
52
+ readonly data?: unknown
53
+ readonly tokens: number
54
+ readonly toolCalls: number
55
+ readonly error?: string
56
+ }
57
+
58
+ /**
59
+ * Starts one subagent through the host's own delegation path (the same
60
+ * policy, sandbox, accounting and cancellation as the `Agent` tool).
61
+ * Must honour `signal`.
62
+ */
63
+ export type WorkflowSpawnAgent = (
64
+ request: WorkflowAgentRequest,
65
+ signal: AbortSignal,
66
+ ) => Promise<WorkflowAgentOutcome>
67
+
68
+ export type WorkflowAgentState = 'start' | 'progress' | 'done' | 'error'
69
+
70
+ export interface WorkflowStartedEvent {
71
+ readonly type: 'workflow_started'
72
+ readonly runId: string
73
+ readonly name: string
74
+ readonly description: string
75
+ readonly phases: readonly WorkflowPhaseMeta[]
76
+ readonly at: number
77
+ }
78
+
79
+ export interface WorkflowPhaseEvent {
80
+ readonly type: 'workflow_phase'
81
+ readonly runId: string
82
+ /** 1-based first-seen order; declared titles come first. */
83
+ readonly index: number
84
+ readonly title: string
85
+ readonly declared: boolean
86
+ readonly at: number
87
+ }
88
+
89
+ /**
90
+ * One agent changed state. `start` without `startedAt` means queued;
91
+ * `start` with `startedAt` means running. A `done` agent that was skipped
92
+ * (by the host, the operator or the token budget) carries `skipped: true`.
93
+ */
94
+ export interface WorkflowAgentEvent {
95
+ readonly type: 'workflow_agent'
96
+ readonly runId: string
97
+ readonly state: WorkflowAgentState
98
+ readonly index: number
99
+ readonly label: string
100
+ readonly phaseIndex?: number
101
+ readonly phaseTitle?: string
102
+ readonly agentType?: string
103
+ readonly model?: string
104
+ readonly queuedAt: number
105
+ readonly startedAt?: number
106
+ readonly finishedAt?: number
107
+ readonly lastProgressAt: number
108
+ readonly tokens: number
109
+ readonly toolCalls: number
110
+ readonly lastToolName?: string
111
+ readonly lastToolSummary?: string
112
+ readonly promptPreview: string
113
+ readonly resultPreview?: string
114
+ readonly error?: string
115
+ readonly skipped?: boolean
116
+ }
117
+
118
+ export interface WorkflowLogEvent {
119
+ readonly type: 'workflow_log'
120
+ readonly runId: string
121
+ readonly message: string
122
+ readonly at: number
123
+ }
124
+
125
+ export type WorkflowFinishStatus = 'completed' | 'failed' | 'stopped'
126
+
127
+ export interface WorkflowUsage {
128
+ /** Agents the script asked for and the runtime accepted. */
129
+ readonly agents: number
130
+ readonly agentsDone: number
131
+ readonly agentsFailed: number
132
+ readonly agentsSkipped: number
133
+ readonly tokens: number
134
+ readonly toolCalls: number
135
+ readonly elapsedMs: number
136
+ }
137
+
138
+ export interface WorkflowFinishedEvent {
139
+ readonly type: 'workflow_finished'
140
+ readonly runId: string
141
+ readonly status: WorkflowFinishStatus
142
+ readonly result?: unknown
143
+ readonly error?: string
144
+ readonly usage: WorkflowUsage
145
+ readonly at: number
146
+ }
147
+
148
+ export type WorkflowEvent =
149
+ | WorkflowStartedEvent
150
+ | WorkflowPhaseEvent
151
+ | WorkflowAgentEvent
152
+ | WorkflowLogEvent
153
+ | WorkflowFinishedEvent
154
+
155
+ export type WorkflowPhaseStatus = 'not-started' | 'running' | 'done' | 'failed'
156
+
157
+ export interface WorkflowPhaseSummary {
158
+ /** 0 for the derived "Agents" group, which has no phase index. */
159
+ readonly index: number
160
+ readonly title: string
161
+ readonly detail?: string
162
+ readonly status: WorkflowPhaseStatus
163
+ readonly agents: number
164
+ readonly done: number
165
+ readonly failed: number
166
+ readonly tokens: number
167
+ }
168
+
169
+ /** Append-only audit records, one JSON object per line. */
170
+ export type WorkflowJournalRecord =
171
+ | {
172
+ readonly type: 'run_started'
173
+ readonly at: number
174
+ readonly runId: string
175
+ readonly scriptSha256: string
176
+ readonly scriptBytes: number
177
+ readonly meta: WorkflowMeta
178
+ readonly args: unknown
179
+ readonly limits: WorkflowResolvedLimits
180
+ }
181
+ | {
182
+ readonly type: 'phase'
183
+ readonly at: number
184
+ readonly index: number
185
+ readonly title: string
186
+ readonly declared: boolean
187
+ }
188
+ | {
189
+ readonly type: 'agent_request'
190
+ readonly at: number
191
+ readonly index: number
192
+ readonly label: string
193
+ readonly phaseIndex?: number
194
+ readonly phaseTitle?: string
195
+ readonly promptPreview: string
196
+ readonly promptSha256: string
197
+ readonly options: {
198
+ readonly agentType?: string
199
+ readonly model?: string
200
+ readonly effort?: string
201
+ readonly schema: boolean
202
+ }
203
+ }
204
+ | {
205
+ readonly type: 'agent_outcome'
206
+ readonly at: number
207
+ readonly index: number
208
+ readonly state: 'done' | 'error' | 'skipped'
209
+ readonly tokens: number
210
+ readonly toolCalls: number
211
+ readonly queuedAt: number
212
+ readonly startedAt?: number
213
+ readonly finishedAt: number
214
+ readonly resultPreview?: string
215
+ readonly error?: string
216
+ }
217
+ | { readonly type: 'log'; readonly at: number; readonly message: string }
218
+ | {
219
+ readonly type: 'run_finished'
220
+ readonly at: number
221
+ readonly status: WorkflowFinishStatus
222
+ readonly result?: unknown
223
+ readonly error?: string
224
+ readonly usage: WorkflowUsage
225
+ }
226
+
227
+ /** Where a run's audit trail goes. Injected so the SDK stays host-agnostic. */
228
+ export interface WorkflowJournal {
229
+ /** Called once, before the first record, with the exact script text. */
230
+ writeScript?(source: string, sha256: string): Promise<void> | void
231
+ append(record: WorkflowJournalRecord): Promise<void> | void
232
+ }
233
+
234
+ export interface WorkflowLimits {
235
+ /** Agents running at once. Default `min(8, cpus - 2)`, at least 1; at most 16. */
236
+ readonly maxConcurrency?: number
237
+ /** Agents one run may start. Default 200; at most 1000. */
238
+ readonly maxAgents?: number
239
+ /** UTF-8 script size. Default 256 KiB. */
240
+ readonly maxSourceBytes?: number
241
+ /** Interpreter memory. Default 64 MiB. */
242
+ readonly memoryLimitBytes?: number
243
+ /** One value crossing the bridge, either way. Default 1 MiB. */
244
+ readonly maxValueBytes?: number
245
+ /** Stop admitting new agents once this many tokens are spent. Default: none. */
246
+ readonly tokenBudget?: number
247
+ /** Wall clock for the whole run. Default 6 hours. */
248
+ readonly maxDurationMs?: number
249
+ /** One synchronous stretch of script execution. Default 2 seconds. */
250
+ readonly cpuSliceMs?: number
251
+ /** All synchronous script execution together. Default 60 seconds. */
252
+ readonly cpuTotalMs?: number
253
+ }
254
+
255
+ export interface WorkflowResolvedLimits {
256
+ readonly maxConcurrency: number
257
+ readonly maxAgents: number
258
+ readonly maxSourceBytes: number
259
+ readonly memoryLimitBytes: number
260
+ readonly maxValueBytes: number
261
+ readonly tokenBudget?: number
262
+ readonly maxDurationMs: number
263
+ readonly cpuSliceMs: number
264
+ readonly cpuTotalMs: number
265
+ }
266
+
267
+ /** Lets the host act on one running agent. Handed to `onControl`. */
268
+ export interface WorkflowControl {
269
+ /** Skip an agent: queued ones never start, running ones are aborted. Its `agent()` resolves `null`. */
270
+ skipAgent(index: number): boolean
271
+ }
272
+
273
+ export interface WorkflowRunOptions {
274
+ /** The script, including its `export const meta` literal. Untrusted. */
275
+ readonly script: string
276
+ /** JSON handed to the script as `args`. */
277
+ readonly args?: unknown
278
+ readonly limits?: WorkflowLimits
279
+ readonly spawnAgent: WorkflowSpawnAgent
280
+ readonly journal?: WorkflowJournal
281
+ readonly signal?: AbortSignal
282
+ readonly onEvent?: (event: WorkflowEvent) => void
283
+ readonly onControl?: (control: WorkflowControl) => void
284
+ /** Default: a fresh `w` + 8 base36 id. */
285
+ readonly runId?: string
286
+ }
287
+
288
+ export interface WorkflowRunResult {
289
+ readonly runId: string
290
+ readonly status: WorkflowFinishStatus
291
+ /** What the script returned (`completed` only). */
292
+ readonly result?: unknown
293
+ readonly error?: string
294
+ readonly usage: WorkflowUsage
295
+ readonly phases: readonly WorkflowPhaseSummary[]
296
+ /** True when the script logged more than the output cap and later lines were dropped. */
297
+ readonly logsTruncated: boolean
298
+ /** Set when the journal could not be written; the run itself was not affected. */
299
+ readonly journalError?: string
300
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ export {
19
19
  UnknownBackgroundJobError,
20
20
  bindOwner,
21
21
  } from './runtime/jobs/registry.js'
22
+ export {
23
+ createBackgroundJobId,
24
+ isBackgroundJobId,
25
+ } from './runtime/jobs/job-id.js'
22
26
  export type {
23
27
  BackgroundJob,
24
28
  BackgroundJobOutput,
@@ -231,7 +231,8 @@ export function runShellHook(
231
231
  // its own — the hook's deadline becomes the grandchild's.
232
232
  detached: process.platform !== 'win32',
233
233
  // A hook has always run under `sh`, whatever shell the `bash` tool
234
- // chose. Windows has no `sh`, so there the host's own shell runs it.
234
+ // chose. Windows has no `sh`, so there the host's own shell runs it: Git Bash
235
+ // when installed, cmd.exe when not.
235
236
  ...(process.platform === 'win32'
236
237
  ? {}
237
238
  : { shell: { path: 'sh', dialect: 'sh', source: 'sh' } as const }),
@@ -1,6 +1,47 @@
1
1
  import { execFileSync } from 'node:child_process'
2
2
  import type { spawn } from 'node:child_process'
3
3
 
4
+ /**
5
+ * Git Bash children that the Windows process tree cannot reach.
6
+ *
7
+ * Git for Windows' msys runtime starts a command that is not the last of its
8
+ * line by `fork`, and the process that forked is gone by the time the command
9
+ * runs: the command's Windows parent no longer exists, so `taskkill /T` walks
10
+ * from the shell to its child and never finds the grandchild (`sleep 60; echo`,
11
+ * `cd app && npm start & wait`). Each such shell is started with a unique tag
12
+ * in its environment, which every descendant inherits however it was started;
13
+ * the tag is how the tree is found again.
14
+ */
15
+ const MSYS_SHELLS = new WeakMap<object, { readonly bash: string; readonly tag: string }>()
16
+
17
+ /** Remember how to find every process a Git Bash child started. Called by the spawn. */
18
+ export function registerMsysShell(child: object, shell: { bash: string; tag: string }): void {
19
+ MSYS_SHELLS.set(child, shell)
20
+ }
21
+
22
+ /** The environment variable that carries the tag. */
23
+ export const MSYS_TAG_VARIABLE = 'NAMZU_SHELL_TAG'
24
+
25
+ /** Kill every process whose environment carries the tag, from inside the msys runtime. */
26
+ function killMsysTagged(shell: { readonly bash: string; readonly tag: string }): void {
27
+ // `$$` is skipped so the killer cannot end itself. Parameter expansion keeps
28
+ // the script to the two programs (grep, kill) the launcher puts on PATH.
29
+ const script = [
30
+ `for f in $(grep -laF "${MSYS_TAG_VARIABLE}=${shell.tag}" /proc/[0-9]*/environ 2>/dev/null); do`,
31
+ 'p=${f#/proc/}; p=${p%/environ}; [ "$p" != "$$" ] && kill -9 $p;',
32
+ 'done',
33
+ ].join(' ')
34
+ try {
35
+ execFileSync(shell.bash, ['-c', script], {
36
+ stdio: 'ignore',
37
+ windowsHide: true,
38
+ timeout: 5000,
39
+ })
40
+ } catch {
41
+ // Nothing tagged was left, or the runtime could not start: taskkill still runs.
42
+ }
43
+ }
44
+
4
45
  /** The system's own taskkill, so a PATH entry cannot stand in for it. */
5
46
  function taskkillPath(): string {
6
47
  const root = process.env.SystemRoot ?? process.env.windir
@@ -46,6 +87,8 @@ export function killTree(child: ReturnType<typeof spawn>, signal: NodeJS.Signals
46
87
  if (!child.pid) return
47
88
  try {
48
89
  if (process.platform === 'win32') {
90
+ const msys = MSYS_SHELLS.get(child)
91
+ if (msys !== undefined) killMsysTagged(msys)
49
92
  execFileSync(taskkillPath(), ['/pid', String(child.pid), '/t', '/f'], {
50
93
  stdio: 'ignore',
51
94
  windowsHide: true,
@@ -76,6 +76,8 @@ export class ProviderRequestError extends Error {
76
76
  * See {@link vendorDetail} for what is kept and what is scrubbed.
77
77
  */
78
78
  public readonly detail?: string
79
+ /** The provider's `x-should-retry` verdict, when it sent one; see {@link ProviderRequestErrorInit.shouldRetry}. */
80
+ public readonly shouldRetry?: boolean
79
81
 
80
82
  constructor(init: ProviderRequestErrorInit) {
81
83
  super(buildProviderErrorMessage(init))
@@ -87,6 +89,7 @@ export class ProviderRequestError extends Error {
87
89
  if (init.status !== undefined) this.status = init.status
88
90
  if (init.retryAfterMs !== undefined) this.retryAfterMs = init.retryAfterMs
89
91
  if (init.detail !== undefined) this.detail = init.detail
92
+ if (init.shouldRetry !== undefined) this.shouldRetry = init.shouldRetry
90
93
  }
91
94
  }
92
95
 
@@ -273,6 +276,15 @@ export function bodySaysContextOverflow(body: string | undefined | null): boolea
273
276
  * a negative delay is worse than none, because a caller would treat it as
274
277
  * "retry immediately" against a provider that just asked it to wait.
275
278
  */
279
+ /**
280
+ * `x-should-retry` is only read when it says `false`: that is the provider
281
+ * refusing a resend, and it outranks the status code. `true` changes nothing,
282
+ * because a retryable status is already retried.
283
+ */
284
+ export function parseShouldRetry(value: string | null | undefined): false | undefined {
285
+ return value?.trim().toLowerCase() === 'false' ? false : undefined
286
+ }
287
+
276
288
  export function parseRetryAfterMs(
277
289
  headerValue: string | null | undefined,
278
290
  now: number = Date.now(),
@@ -525,6 +537,7 @@ export function providerVendorError(input: {
525
537
  input.retryAfter ?? vendorRetryAfter(error),
526
538
  input.now ?? Date.now(),
527
539
  )
540
+ const shouldRetry = parseShouldRetry(vendorHeader(error, ['x-should-retry']))
528
541
  const fields = error as { type?: unknown; param?: unknown; requestID?: unknown } | null
529
542
  // A message that already carries its body (Anthropic's) has the fields; one that is only the
530
543
  // parsed sentence (OpenAI's) gets them from the SDK error's own properties.
@@ -544,6 +557,7 @@ export function providerVendorError(input: {
544
557
  ...(status !== undefined ? { status } : {}),
545
558
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
546
559
  ...(detail !== undefined ? { detail } : {}),
560
+ ...(shouldRetry !== undefined ? { shouldRetry } : {}),
547
561
  })
548
562
  }
549
563
 
@@ -605,6 +619,8 @@ export function providerHttpError(input: {
605
619
  readonly retryAfter?: string | null
606
620
  /** The response's request ID header, when the driver read one. */
607
621
  readonly requestId?: string | null
622
+ /** The response's `x-should-retry` header, when the driver read one. */
623
+ readonly shouldRetry?: string | null
608
624
  readonly now?: number
609
625
  }): ProviderRequestError {
610
626
  const kind = classifyProviderHttpStatus(input.status, input.body)
@@ -621,6 +637,7 @@ export function providerHttpError(input: {
621
637
  })
622
638
  : vendorDetail(input.body)
623
639
  const exactProviderCode = providerCode(input.body)
640
+ const shouldRetry = parseShouldRetry(input.shouldRetry)
624
641
  return new ProviderRequestError({
625
642
  kind,
626
643
  providerId: input.providerId,
@@ -628,5 +645,6 @@ export function providerHttpError(input: {
628
645
  status: input.status,
629
646
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
630
647
  ...(detail !== undefined ? { detail } : {}),
648
+ ...(shouldRetry !== undefined ? { shouldRetry } : {}),
631
649
  })
632
650
  }
@@ -1,7 +1,12 @@
1
1
  import { GENAI } from '../constants/telemetry/index.js'
2
2
  import { classifyProviderError, isAbortError } from '../types/provider/errors.js'
3
3
  import type { ChatCompletionParams, LLMProvider, StreamChunk } from '../types/provider/index.js'
4
- import { type BackoffPolicy, backoffWithJitter, sleep } from '../utils/backoff.js'
4
+ import {
5
+ type BackoffPolicy,
6
+ backoffProportional,
7
+ backoffWithJitter,
8
+ sleep,
9
+ } from '../utils/backoff.js'
5
10
  import type { Logger } from '../utils/logger.js'
6
11
  import { isProviderRequestError } from './errors.js'
7
12
 
@@ -31,6 +36,33 @@ export interface ProviderRetryConfig extends BackoffPolicy {
31
36
  * still slept exactly as instructed.
32
37
  */
33
38
  readonly maxRetryAfterMs: number
39
+ /**
40
+ * Shape of the random part of a backoff.
41
+ *
42
+ * `'full'` (the default) sleeps a uniform amount in `[0, backoff]`.
43
+ * `'proportional'` sleeps the backoff plus up to a quarter of it, so a step
44
+ * is never shorter than its exponential value.
45
+ */
46
+ readonly jitter?: 'full' | 'proportional'
47
+ /**
48
+ * What a server-named wait means.
49
+ *
50
+ * `'exact'` (the default) sleeps exactly what `Retry-After` said.
51
+ * `'floor'` sleeps the longer of that and the backoff for this attempt, so
52
+ * a server that says "1 second" on the seventh failure is still given the
53
+ * backoff's thirty. The ceiling above applies either way.
54
+ */
55
+ readonly retryAfter?: 'exact' | 'floor'
56
+ /**
57
+ * Also retry a stream that failed AFTER it produced text or reasoning.
58
+ *
59
+ * Off by default, because the decorator cannot un-emit: the consumer has
60
+ * already seen those deltas. When on, the retry notice carries
61
+ * `restart: true` and the consumer must discard what the failed attempt
62
+ * produced; the runtime does this itself. A stream that has begun a tool
63
+ * call is never restarted, because the call may already be underway.
64
+ */
65
+ readonly retryMidStream?: boolean
34
66
  }
35
67
 
36
68
  export const DEFAULT_PROVIDER_RETRY: ProviderRetryConfig = {
@@ -97,6 +129,9 @@ export function withProviderRetry(
97
129
  async function* chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
98
130
  for (let attempt = 0; ; attempt++) {
99
131
  let produced = false
132
+ // A tool call or hosted tool began: past this point a restart could
133
+ // repeat work that is already underway, so it is never offered.
134
+ let acted = false
100
135
  try {
101
136
  for await (const chunk of provider.chatStream(params)) {
102
137
  // An error-only chunk is a failure report, not output: it
@@ -106,6 +141,9 @@ export function withProviderRetry(
106
141
  chunk.error !== undefined && !chunk.delta.content && !chunk.delta.toolCalls?.length
107
142
 
108
143
  if (!isErrorOnly) produced = true
144
+ if (chunk.delta.toolCalls?.length || chunk.delta.toolCallEnd || chunk.delta.hostedTool) {
145
+ acted = true
146
+ }
109
147
 
110
148
  if (isErrorOnly && !produced && attempt < config.maxRetries) {
111
149
  throw new Error(chunk.error)
@@ -132,7 +170,8 @@ export function withProviderRetry(
132
170
  const classified = classifyProviderError(err, provider.id)
133
171
  const exhausted = attempt >= config.maxRetries
134
172
 
135
- if (produced || !classified.retryable || exhausted) {
173
+ const restartable = produced && config.retryMidStream === true && !acted
174
+ if ((produced && !restartable) || !classified.retryable || exhausted) {
136
175
  log?.warn('Provider call failed', {
137
176
  [GENAI.SYSTEM]: provider.id,
138
177
  'namzu.provider.code': classified.code,
@@ -200,7 +239,16 @@ export function withProviderRetry(
200
239
  throw isProviderRequestError(err) ? err : classified
201
240
  }
202
241
 
203
- const delay = serverDirected ?? backoffWithJitter(attempt, config, random)
242
+ const backoff =
243
+ config.jitter === 'proportional'
244
+ ? backoffProportional(attempt, config, random)
245
+ : backoffWithJitter(attempt, config, random)
246
+ const delay =
247
+ serverDirected === undefined
248
+ ? backoff
249
+ : config.retryAfter === 'floor'
250
+ ? Math.max(serverDirected, backoff)
251
+ : serverDirected
204
252
 
205
253
  log?.warn('Provider call failed — retrying', {
206
254
  [GENAI.SYSTEM]: provider.id,
@@ -210,6 +258,7 @@ export function withProviderRetry(
210
258
  'namzu.provider.max_retries': config.maxRetries,
211
259
  'namzu.provider.delay_ms': delay,
212
260
  'namzu.provider.server_directed': serverDirected !== undefined,
261
+ 'namzu.provider.restart': restartable,
213
262
  })
214
263
 
215
264
  // Tell the consumer BEFORE sleeping. With the default policy a
@@ -231,6 +280,7 @@ export function withProviderRetry(
231
280
  code: classified.code,
232
281
  ...(classified.status !== undefined ? { status: classified.status } : {}),
233
282
  serverDirected: serverDirected !== undefined,
283
+ ...(restartable ? { restart: true } : {}),
234
284
  },
235
285
  }
236
286