@namzu/sdk 19.0.0 → 20.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 (44) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +1 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/runAgent.d.ts +20 -0
  6. package/dist/agents/runAgent.d.ts.map +1 -1
  7. package/dist/agents/runAgent.js +2 -1
  8. package/dist/agents/runAgent.js.map +1 -1
  9. package/dist/manager/run/persistence.d.ts +27 -0
  10. package/dist/manager/run/persistence.d.ts.map +1 -1
  11. package/dist/manager/run/persistence.js +28 -0
  12. package/dist/manager/run/persistence.js.map +1 -1
  13. package/dist/runtime/query/executor.d.ts.map +1 -1
  14. package/dist/runtime/query/executor.js +44 -9
  15. package/dist/runtime/query/executor.js.map +1 -1
  16. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  17. package/dist/runtime/query/plugin-hooks.js +1 -0
  18. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  19. package/dist/store/run/disk.d.ts.map +1 -1
  20. package/dist/store/run/disk.js +6 -0
  21. package/dist/store/run/disk.js.map +1 -1
  22. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  23. package/dist/tools/coordinator/agent.js +20 -5
  24. package/dist/tools/coordinator/agent.js.map +1 -1
  25. package/dist/tools/coordinator/index.d.ts.map +1 -1
  26. package/dist/tools/coordinator/index.js +19 -2
  27. package/dist/tools/coordinator/index.js.map +1 -1
  28. package/dist/types/agent/base.d.ts +20 -0
  29. package/dist/types/agent/base.d.ts.map +1 -1
  30. package/dist/types/plugin/index.d.ts +30 -1
  31. package/dist/types/plugin/index.d.ts.map +1 -1
  32. package/dist/types/plugin/index.js +1 -0
  33. package/dist/types/plugin/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/agents/ReactiveAgent.ts +1 -0
  36. package/src/agents/runAgent.ts +24 -1
  37. package/src/manager/run/persistence.ts +28 -0
  38. package/src/runtime/query/executor.ts +59 -11
  39. package/src/runtime/query/plugin-hooks.ts +1 -0
  40. package/src/store/run/disk.ts +6 -0
  41. package/src/tools/coordinator/agent.ts +20 -5
  42. package/src/tools/coordinator/index.ts +22 -2
  43. package/src/types/agent/base.ts +20 -0
  44. package/src/types/plugin/index.ts +27 -1
@@ -144,6 +144,20 @@ export interface ToolExecutorConfig {
144
144
  repairToolCall?: RepairToolCall
145
145
  }
146
146
 
147
+ /**
148
+ * What a `post_tool_use` hook decided to show the model instead.
149
+ *
150
+ * `isError` is the field this type exists for. The override used to be a bare
151
+ * string, so the executor had no way to tell "the call failed" from "the call
152
+ * succeeded and the model may not see all of it" — and it assumed the first,
153
+ * which turned every redaction into a reported tool failure.
154
+ */
155
+ interface PostToolOverride {
156
+ readonly output: string
157
+ readonly isError: boolean
158
+ readonly content?: ToolResultContent
159
+ }
160
+
147
161
  type PreToolHookOutcome =
148
162
  | { kind: 'continue'; input: unknown }
149
163
  | { kind: 'skip'; input: unknown; output: string }
@@ -708,10 +722,14 @@ export class ToolExecutor {
708
722
 
709
723
  const postOverride = post.override
710
724
  if (postOverride !== null) {
711
- output = postOverride
725
+ output = postOverride.output
712
726
  }
713
727
 
714
- const effectiveIsError = !result.success || postOverride !== null
728
+ // A failed call, or an override that says the call failed. A `replace`
729
+ // says the opposite, and reading it as a failure is what made redaction
730
+ // unusable: the model was told a successful call had gone wrong, and
731
+ // routed around it.
732
+ const effectiveIsError = !result.success || (postOverride?.isError ?? false)
715
733
 
716
734
  if (this.workingStateManager) {
717
735
  extractFromToolResult(this.workingStateManager, toolName, output, effectiveIsError)
@@ -756,17 +774,31 @@ export class ToolExecutor {
756
774
  ...(budgeted.spillPath ? { outputSpillPath: budgeted.spillPath } : {}),
757
775
  })
758
776
 
777
+ const resolveContent = (): { content?: ToolResultContent } => {
778
+ if (postOverride?.content !== undefined) {
779
+ return { content: this.budgetContent(postOverride.content, toolName) }
780
+ }
781
+ if (postOverride?.isError) return {}
782
+ if (budgeted.truncated || result.content === undefined) return {}
783
+ return { content: this.budgetContent(result.content, toolName) }
784
+ }
785
+
759
786
  return {
760
787
  toolCallId: toolCall.id,
761
788
  toolName,
762
789
  output,
763
790
  isError: effectiveIsError,
764
- // A plugin override replaces what the model sees, and a spilled
765
- // preview is no longer the tool's own payload — neither may carry
766
- // rich content through.
767
- ...(result.content !== undefined && postOverride === null && !budgeted.truncated
768
- ? { content: this.budgetContent(result.content, toolName) }
769
- : {}),
791
+ // Rich content follows the override's own decision.
792
+ //
793
+ // An ERROR override drops it: the payload is no longer the tool's,
794
+ // and shipping an image beside a failure message describes something
795
+ // the model was just told did not happen. A spilled preview drops it
796
+ // for the same reason.
797
+ //
798
+ // A REPLACE keeps it, because the common case is redacting text from
799
+ // a result whose image is unaffected — and a hook that needs it gone
800
+ // says so with `content`, which wins over both.
801
+ ...resolveContent(),
770
802
  }
771
803
  }
772
804
 
@@ -919,6 +951,11 @@ export class ToolExecutor {
919
951
  output: `Error: ${result.message}`,
920
952
  }
921
953
  case 'retry':
954
+ // There is no result to replace yet. Rejecting loudly beats
955
+ // silently ignoring it: a hook author who returned this here
956
+ // meant to redact something and would otherwise watch the secret
957
+ // go through.
958
+ case 'replace':
922
959
  throw new Error(
923
960
  `Plugin hook pre_tool_use returned unsupported action '${result.action}' for tool ${toolName}`,
924
961
  )
@@ -1120,21 +1157,32 @@ export class ToolExecutor {
1120
1157
  toolName: string,
1121
1158
  input: unknown,
1122
1159
  toolResult: ToolResult,
1123
- ): Promise<{ override: string | null; retry: boolean }> {
1160
+ ): Promise<{ override: PostToolOverride | null; retry: boolean }> {
1124
1161
  if (!this.config.pluginManager) return { override: null, retry: false }
1125
1162
  const results = await this.config.pluginManager.executeHooks(
1126
1163
  'post_tool_use',
1127
1164
  { runId: this.config.runId, toolName, toolInput: input, toolResult },
1128
1165
  this.emitEvent,
1129
1166
  )
1130
- let override: string | null = null
1167
+ let override: PostToolOverride | null = null
1131
1168
  let retry = false
1132
1169
  for (const result of results) {
1133
1170
  switch (result.action) {
1134
1171
  case 'continue':
1135
1172
  continue
1136
1173
  case 'error':
1137
- override = `Error: ${result.message}`
1174
+ override = { output: `Error: ${result.message}`, isError: true }
1175
+ continue
1176
+ // A redaction, not a failure. The call stood; the model is shown
1177
+ // less of it. Rich content survives unless the hook replaced it —
1178
+ // see the variant's own documentation for why that default, and
1179
+ // for what a hook redacting a secret in an image has to do.
1180
+ case 'replace':
1181
+ override = {
1182
+ output: result.output,
1183
+ isError: false,
1184
+ ...(result.content !== undefined ? { content: result.content } : {}),
1185
+ }
1138
1186
  continue
1139
1187
  // `retry` was a declared variant with no implementation: every
1140
1188
  // site that consumed it threw. Here it finally means something
@@ -26,6 +26,7 @@ export function applyLifecycleHookResults(
26
26
  case 'skip':
27
27
  case 'modify':
28
28
  case 'retry':
29
+ case 'replace':
29
30
  throw new Error(
30
31
  `Plugin hook ${event} returned unsupported action '${result.action}' for a lifecycle event`,
31
32
  )
@@ -136,6 +136,12 @@ export class RunDiskStore {
136
136
  messageCount: run.messages.length,
137
137
  }
138
138
 
139
+ // The schema-validated answer belongs in the durable record for the same
140
+ // reason `result` does: a run reloaded by id that has lost its answer has
141
+ // lost the thing it was run for. Written only when present, so a run that
142
+ // asked for no schema carries no key rather than an explicit `undefined`.
143
+ if (run.structuredOutput !== undefined) meta.structuredOutput = run.structuredOutput
144
+
139
145
  if (run.parentRunId) meta.parentRunId = run.parentRunId
140
146
  if (run.depth !== undefined && run.depth > 0) meta.depth = run.depth
141
147
 
@@ -175,12 +175,27 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
175
175
  // the other one.
176
176
  const succeeded = taskSucceeded(completed)
177
177
 
178
+ // A schema-configured child answers with an OBJECT, and this used to
179
+ // hand the parent model the child's prose instead — so a supervisor
180
+ // fanning out to five specialists got five strings and had to
181
+ // re-parse what it had just caused to be serialized.
182
+ //
183
+ // `structuredOutput` wins over `result` when present, and reading it
184
+ // first is what makes that true. They agree by construction anyway:
185
+ // `setStructuredOutput` serializes the value into `result`, so this
186
+ // preference is about which field is authoritative rather than about
187
+ // which string is produced.
188
+ const structured = completed.result?.structuredOutput
178
189
  const resultText =
179
- typeof completed.result?.result === 'string'
180
- ? completed.result.result
181
- : completed.result?.result !== undefined
182
- ? JSON.stringify(completed.result.result)
183
- : ''
190
+ structured !== undefined
191
+ ? typeof structured === 'string'
192
+ ? structured
193
+ : JSON.stringify(structured)
194
+ : typeof completed.result?.result === 'string'
195
+ ? completed.result.result
196
+ : completed.result?.result !== undefined
197
+ ? JSON.stringify(completed.result.result)
198
+ : ''
184
199
 
185
200
  if (!succeeded) {
186
201
  const detail =
@@ -288,6 +288,26 @@ function readPositiveIntEnv(key: string, fallback: number): number {
288
288
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
289
289
  }
290
290
 
291
+ /**
292
+ * The answer a delegated child produced, as the string a parent model reads.
293
+ *
294
+ * One function because two delegation surfaces ask the same question, and the
295
+ * comment on the other one records what happens when a rule lives at one site
296
+ * only: create_task shipped without the success check that agent already had.
297
+ *
298
+ * A schema-configured child answers with an object. Reading structuredOutput
299
+ * first is what stops a supervisor receiving prose it then has to re-parse.
300
+ */
301
+ function delegatedAnswer(
302
+ result: { structuredOutput?: unknown; result?: string } | undefined,
303
+ ): string | undefined {
304
+ const structured = result?.structuredOutput
305
+ if (structured !== undefined) {
306
+ return typeof structured === 'string' ? structured : JSON.stringify(structured)
307
+ }
308
+ return result?.result
309
+ }
310
+
291
311
  export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
292
312
  const {
293
313
  gateway,
@@ -614,7 +634,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
614
634
  // the work had been done.
615
635
  const success = taskSucceeded(completed)
616
636
  const resultText =
617
- completed.result?.result ??
637
+ delegatedAnswer(completed.result) ??
618
638
  completed.result?.lastError ??
619
639
  `Task finished with state: ${failureLabel(completed)}`
620
640
 
@@ -744,7 +764,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
744
764
 
745
765
  const success = completed.state === 'completed'
746
766
  const resultText =
747
- completed.result?.result ??
767
+ delegatedAnswer(completed.result) ??
748
768
  completed.result?.lastError ??
749
769
  `Task finished with state: ${completed.state}`
750
770
  return {
@@ -186,6 +186,26 @@ export interface BaseAgentResult {
186
186
  durationMs: number
187
187
  messages: Message[]
188
188
  result?: string
189
+ /**
190
+ * The schema-validated answer, when the run was configured to produce one.
191
+ *
192
+ * `Run.structuredOutput` has carried this all along and every ergonomic
193
+ * boundary above it dropped the value three lines from its caller: an
194
+ * archetype's result literal did not copy it, `runAgent` did not even
195
+ * forward the config that produces it, and both delegation tools handed a
196
+ * parent the child's prose. So a supervisor fanning out to five
197
+ * schema-configured specialists received five strings and had to make the
198
+ * model re-parse what it had just caused to be serialized.
199
+ *
200
+ * `unknown` rather than a generic, deliberately. The schema lives on the
201
+ * run's config and a result type parameter would have to be threaded
202
+ * through every archetype, both delegation tools and the task record to
203
+ * reach here — and at the delegation boundary the parent does not hold the
204
+ * child's schema anyway, so the parameter would be `unknown` again at the
205
+ * only place it was wanted. Narrow it at the call site with the schema you
206
+ * already have.
207
+ */
208
+ structuredOutput?: unknown
189
209
  lastError?: string
190
210
  }
191
211
 
@@ -9,7 +9,7 @@ import {
9
9
  PLUGIN_NAME_MAX_LENGTH,
10
10
  } from '../../constants/plugin/index.js'
11
11
  import type { PluginId, RunId } from '../ids/index.js'
12
- import type { Message } from '../message/index.js'
12
+ import type { Message, ToolResultContent } from '../message/index.js'
13
13
  import type { ToolResult } from '../tool/index.js'
14
14
 
15
15
  // ---------------------------------------------------------------------------
@@ -191,6 +191,31 @@ export type PluginHookResult =
191
191
  | { action: 'modify'; input: unknown }
192
192
  | { action: 'error'; message: string }
193
193
  | { action: 'retry' }
194
+ /**
195
+ * Replace what the model sees, WITHOUT reporting the call as failed.
196
+ *
197
+ * The substitution seam already existed and was typed as a failure channel:
198
+ * the only way a `post_tool_use` hook could change the output was
199
+ * `action: 'error'`, which prefixes `Error: ` and sets the error flag. So
200
+ * redacting a credential out of a successful result was delivered to the
201
+ * model as a tool failure, and the model routed around a call that had
202
+ * worked — retrying it, or reporting to the user that it had failed.
203
+ *
204
+ * That is the difference this variant exists for. `error` says the call went
205
+ * wrong; this says the call went right and the model may not see all of it.
206
+ *
207
+ * `modify` is not this. It carries `input` and belongs to the pre-call
208
+ * hooks, which is why `post_tool_use` rejects it — a result is not an input,
209
+ * and reusing the variant would have made one action mean two things
210
+ * depending on where it was returned.
211
+ *
212
+ * Rich content blocks SURVIVE a replace unless `content` is given, because
213
+ * the common case is redacting text from a result whose image or resource
214
+ * is unaffected. A hook that needs to drop them passes `content: []`, and a
215
+ * hook redacting a secret that also appears in an image must — this variant
216
+ * cannot inspect what it is preserving.
217
+ */
218
+ | { action: 'replace'; output: string; content?: ToolResultContent }
194
219
 
195
220
  export function assertPluginHookResult(result: PluginHookResult): asserts result {
196
221
  const action = result.action
@@ -200,6 +225,7 @@ export function assertPluginHookResult(result: PluginHookResult): asserts result
200
225
  case 'modify':
201
226
  case 'error':
202
227
  case 'retry':
228
+ case 'replace':
203
229
  break
204
230
  default: {
205
231
  const _exhaustive: never = action