@namzu/sdk 7.0.0 → 9.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 (206) hide show
  1. package/CHANGELOG.md +652 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +186 -163
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts +2 -0
  6. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts.map +1 -0
  7. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js +177 -0
  8. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js.map +1 -0
  9. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
  12. package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +4 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +24 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/contracts/api.d.ts +1 -1
  20. package/dist/contracts/api.d.ts.map +1 -1
  21. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts +2 -0
  22. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts.map +1 -0
  23. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js +114 -0
  24. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js.map +1 -0
  25. package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
  26. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
  27. package/dist/gateway/completion-inbox.d.ts +94 -6
  28. package/dist/gateway/completion-inbox.d.ts.map +1 -1
  29. package/dist/gateway/completion-inbox.js +235 -15
  30. package/dist/gateway/completion-inbox.js.map +1 -1
  31. package/dist/gateway/local.d.ts.map +1 -1
  32. package/dist/gateway/local.js +19 -6
  33. package/dist/gateway/local.js.map +1 -1
  34. package/dist/manager/agent/__tests__/lifecycle.test.js +56 -0
  35. package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
  36. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  37. package/dist/manager/agent/lifecycle.js +62 -33
  38. package/dist/manager/agent/lifecycle.js.map +1 -1
  39. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts +2 -0
  40. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts.map +1 -0
  41. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js +73 -0
  42. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js.map +1 -0
  43. package/dist/manager/plan/lifecycle.d.ts +48 -0
  44. package/dist/manager/plan/lifecycle.d.ts.map +1 -1
  45. package/dist/manager/plan/lifecycle.js +56 -0
  46. package/dist/manager/plan/lifecycle.js.map +1 -1
  47. package/dist/manager/run/persistence.d.ts +8 -0
  48. package/dist/manager/run/persistence.d.ts.map +1 -1
  49. package/dist/manager/run/persistence.js +12 -0
  50. package/dist/manager/run/persistence.js.map +1 -1
  51. package/dist/run/reporter.d.ts.map +1 -1
  52. package/dist/run/reporter.js +11 -0
  53. package/dist/run/reporter.js.map +1 -1
  54. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
  55. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
  56. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
  57. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
  58. package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
  59. package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
  60. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
  61. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
  62. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
  63. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
  64. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts +2 -0
  65. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts.map +1 -0
  66. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js +62 -0
  67. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js.map +1 -0
  68. package/dist/runtime/query/events.d.ts.map +1 -1
  69. package/dist/runtime/query/events.js +5 -0
  70. package/dist/runtime/query/events.js.map +1 -1
  71. package/dist/runtime/query/guard.d.ts +28 -0
  72. package/dist/runtime/query/guard.d.ts.map +1 -1
  73. package/dist/runtime/query/guard.js +31 -0
  74. package/dist/runtime/query/guard.js.map +1 -1
  75. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
  76. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
  78. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
  79. package/dist/runtime/query/iteration/index.d.ts +92 -0
  80. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/index.js +814 -566
  82. package/dist/runtime/query/iteration/index.js.map +1 -1
  83. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
  84. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
  85. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
  86. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
  87. package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
  88. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  89. package/dist/runtime/query/iteration/phases/compaction.js +61 -4
  90. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  91. package/dist/store/task/disk.d.ts.map +1 -1
  92. package/dist/store/task/disk.js +14 -0
  93. package/dist/store/task/disk.js.map +1 -1
  94. package/dist/store/task/memory.d.ts.map +1 -1
  95. package/dist/store/task/memory.js +14 -0
  96. package/dist/store/task/memory.js.map +1 -1
  97. package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
  98. package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
  99. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts +2 -0
  100. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts.map +1 -0
  101. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js +101 -0
  102. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js.map +1 -0
  103. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
  104. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
  105. package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
  106. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  107. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts +2 -0
  108. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts.map +1 -0
  109. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js +99 -0
  110. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js.map +1 -0
  111. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  112. package/dist/tools/coordinator/agent.js +17 -23
  113. package/dist/tools/coordinator/agent.js.map +1 -1
  114. package/dist/tools/coordinator/index.d.ts.map +1 -1
  115. package/dist/tools/coordinator/index.js +142 -14
  116. package/dist/tools/coordinator/index.js.map +1 -1
  117. package/dist/tools/coordinator/outcome.d.ts +60 -0
  118. package/dist/tools/coordinator/outcome.d.ts.map +1 -0
  119. package/dist/tools/coordinator/outcome.js +68 -0
  120. package/dist/tools/coordinator/outcome.js.map +1 -0
  121. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  122. package/dist/tools/untrusted-envelope.js +9 -1
  123. package/dist/tools/untrusted-envelope.js.map +1 -1
  124. package/dist/types/agent/gateway.d.ts +37 -2
  125. package/dist/types/agent/gateway.d.ts.map +1 -1
  126. package/dist/types/agent/supervisor.d.ts +38 -1
  127. package/dist/types/agent/supervisor.d.ts.map +1 -1
  128. package/dist/types/plan/index.d.ts +26 -0
  129. package/dist/types/plan/index.d.ts.map +1 -1
  130. package/dist/types/provider/interface.d.ts +35 -0
  131. package/dist/types/provider/interface.d.ts.map +1 -1
  132. package/dist/types/run/entity.d.ts +16 -0
  133. package/dist/types/run/entity.d.ts.map +1 -1
  134. package/dist/types/run/events.d.ts +92 -0
  135. package/dist/types/run/events.d.ts.map +1 -1
  136. package/dist/types/run/events.js.map +1 -1
  137. package/dist/types/run/prepare-step.d.ts +17 -2
  138. package/dist/types/run/prepare-step.d.ts.map +1 -1
  139. package/dist/types/task/index.d.ts +19 -1
  140. package/dist/types/task/index.d.ts.map +1 -1
  141. package/dist/types/task/index.js +10 -1
  142. package/dist/types/task/index.js.map +1 -1
  143. package/dist/types/verification/index.d.ts +98 -0
  144. package/dist/types/verification/index.d.ts.map +1 -1
  145. package/dist/types/verification/index.js +10 -0
  146. package/dist/types/verification/index.js.map +1 -1
  147. package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
  148. package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
  149. package/dist/verification/__tests__/argument-pattern.test.js +122 -0
  150. package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
  151. package/dist/verification/gate.d.ts.map +1 -1
  152. package/dist/verification/gate.js +44 -0
  153. package/dist/verification/gate.js.map +1 -1
  154. package/dist/verification/rules.d.ts.map +1 -1
  155. package/dist/verification/rules.js +27 -0
  156. package/dist/verification/rules.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/agents/SupervisorAgent.ts +189 -167
  159. package/src/agents/__tests__/a-policy-nobody-can-select.test.ts +202 -0
  160. package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
  161. package/src/bridge/a2a/mapper.ts +4 -0
  162. package/src/bridge/sse/mapper.ts +25 -0
  163. package/src/contracts/api.ts +1 -0
  164. package/src/gateway/__tests__/a-knob-nobody-can-turn.test.ts +139 -0
  165. package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
  166. package/src/gateway/completion-inbox.ts +248 -16
  167. package/src/gateway/local.ts +19 -7
  168. package/src/manager/agent/__tests__/lifecycle.test.ts +74 -0
  169. package/src/manager/agent/lifecycle.ts +72 -42
  170. package/src/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.ts +88 -0
  171. package/src/manager/plan/lifecycle.ts +61 -0
  172. package/src/manager/run/persistence.ts +12 -0
  173. package/src/run/reporter.ts +12 -0
  174. package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
  175. package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
  176. package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
  177. package/src/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.ts +79 -0
  178. package/src/runtime/query/events.ts +5 -0
  179. package/src/runtime/query/guard.ts +32 -0
  180. package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
  181. package/src/runtime/query/iteration/index.ts +902 -636
  182. package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
  183. package/src/runtime/query/iteration/phases/compaction.ts +83 -10
  184. package/src/store/task/disk.ts +15 -0
  185. package/src/store/task/memory.ts +15 -0
  186. package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
  187. package/src/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.ts +133 -0
  188. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
  189. package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
  190. package/src/tools/coordinator/__tests__/the-plan-a-human-approves.test.ts +121 -0
  191. package/src/tools/coordinator/agent.ts +17 -25
  192. package/src/tools/coordinator/index.ts +155 -18
  193. package/src/tools/coordinator/outcome.ts +70 -0
  194. package/src/tools/untrusted-envelope.ts +9 -1
  195. package/src/types/agent/gateway.ts +37 -2
  196. package/src/types/agent/supervisor.ts +40 -1
  197. package/src/types/plan/index.ts +29 -0
  198. package/src/types/provider/interface.ts +39 -0
  199. package/src/types/run/entity.ts +17 -0
  200. package/src/types/run/events.ts +92 -0
  201. package/src/types/run/prepare-step.ts +17 -2
  202. package/src/types/task/index.ts +22 -3
  203. package/src/types/verification/index.ts +61 -0
  204. package/src/verification/__tests__/argument-pattern.test.ts +158 -0
  205. package/src/verification/gate.ts +45 -0
  206. package/src/verification/rules.ts +28 -0
@@ -1,5 +1,5 @@
1
1
  import type { TaskId } from '../ids/index.js'
2
- import type { AgentRuntimeContext, BaseAgentResult } from './base.js'
2
+ import type { AgentRuntimeContext, BaseAgentConfig, BaseAgentResult } from './base.js'
3
3
  import type { AgentTaskState } from './task.js'
4
4
 
5
5
  export interface TaskHandle {
@@ -40,7 +40,24 @@ export interface CreateTaskOptions {
40
40
 
41
41
  runtimeContext?: AgentRuntimeContext
42
42
 
43
- configOverrides?: Record<string, unknown>
43
+ /**
44
+ * Config the spawned run should be built with, overriding what the
45
+ * agent's own definition supplies — the model it runs on, its iteration
46
+ * ceiling, its thinking or effort settings.
47
+ *
48
+ * **This was accepted and dropped.** `LocalTaskGateway.createTask` built
49
+ * its own `configOverrides` object out of `parentSpan` alone and never
50
+ * read this field, so a caller pinning a delegated run to a cheaper model
51
+ * got the agent's default model and no indication otherwise. It is
52
+ * forwarded now, with the dedicated {@link parentSpan} option winning if
53
+ * both name a span, since that one is the specific field for the job.
54
+ *
55
+ * Typed as `Partial<BaseAgentConfig>` rather than
56
+ * `Record<string, unknown>`: this lands on `SendMessageOptions`, which is
57
+ * already that shape, and the loose type let a misspelled key type-check
58
+ * and then do nothing — the same silence this field was already producing.
59
+ */
60
+ configOverrides?: Partial<BaseAgentConfig>
44
61
  }
45
62
 
46
63
  export interface TaskGateway {
@@ -52,6 +69,24 @@ export interface TaskGateway {
52
69
 
53
70
  cancelTask(taskId: TaskId): void
54
71
 
72
+ /**
73
+ * The task's current state, or `undefined` if this gateway does not know
74
+ * about it.
75
+ *
76
+ * **A task that has just settled should still be findable here.** The
77
+ * kernel uses this to recover one specific race: `createTask` resolves a
78
+ * microtask before its caller can record whose the task is, so a worker
79
+ * that finishes inside that window is announced to a listener that cannot
80
+ * yet place it. `CompletionInbox` buffers the announcement AND asks this
81
+ * method, and the second is what covers the case the buffer could not
82
+ * hold.
83
+ *
84
+ * This is a request, not a requirement, and the cost of not meeting it is
85
+ * yours: a gateway that forgets a task the instant it completes still
86
+ * works, but under a burst large enough to overflow the buffer a fast
87
+ * worker's result can go unannounced. `LocalTaskGateway` meets it for as
88
+ * long as the manager holds the record.
89
+ */
55
90
  getTask(taskId: TaskId): TaskHandle | undefined
56
91
 
57
92
  listTasks(): TaskHandle[]
@@ -10,7 +10,7 @@ import type { ToolRegistryContract } from '../tool/index.js'
10
10
  import type { VerificationGateConfig } from '../verification/index.js'
11
11
  import type { BaseAgentConfig, BaseAgentResult } from './base.js'
12
12
  import type { AgentFactoryOptions } from './factory.js'
13
- import type { TaskGateway } from './gateway.js'
13
+ import type { SiblingFailurePolicy, TaskGateway } from './gateway.js'
14
14
  import type { AgentManagerContract } from './manager.js'
15
15
  import type { WorkingMemoryProvider } from './working-memory.js'
16
16
 
@@ -72,6 +72,45 @@ export interface SupervisorAgentConfig extends BaseAgentConfig {
72
72
  */
73
73
  maxDepth?: number
74
74
 
75
+ /**
76
+ * How many tools may execute at once in one turn — which, for a
77
+ * supervisor, is how wide a fan-out actually runs.
78
+ *
79
+ * The kernel has honoured this all along and `ReactiveAgent` forwards it.
80
+ * It was missing here, so the agent whose entire job is delegation could
81
+ * not set the gate that bounds delegation, while the agent that does not
82
+ * delegate could. A host wanting a narrower fan-out had to reach past the
83
+ * supervisor to `drainQuery`.
84
+ *
85
+ * Absent leaves the kernel default. Note what it does and does not bound:
86
+ * it limits how many delegated children run CONCURRENTLY, not how many a
87
+ * turn may launch — a model that emits twenty `create_task` blocks still
88
+ * launches twenty, and they queue.
89
+ */
90
+ maxToolConcurrency?: number
91
+
92
+ /**
93
+ * What a failed child means for the siblings still running. Defaults to
94
+ * `'continue'`.
95
+ *
96
+ * `LocalTaskGateway` has honoured this since it was written, and the
97
+ * cancellation machinery behind `'cancel-siblings'` is complete — but the
98
+ * policy was a constructor argument on a gateway the supervisor builds
99
+ * itself, and the supervisor passed nothing. So every host in existence
100
+ * ran `'continue'`, and the only way to reach the other value was to
101
+ * construct the gateway by hand and hand it in. A policy nobody can select
102
+ * is not a policy.
103
+ *
104
+ * `'continue'` stays the default deliberately: partial results are usually
105
+ * worth having, and tearing down healthy siblings on any failure lets one
106
+ * flaky child waste four good ones. `'cancel-siblings'` is for a fan-out
107
+ * whose parts only mean something together — if one leg of a comparison
108
+ * dies, the others are spending budget on an answer nobody can use.
109
+ *
110
+ * Ignored when the host supplies its own `gateway`, which owns its policy.
111
+ */
112
+ siblingFailurePolicy?: SiblingFailurePolicy
113
+
75
114
  taskRouter?: TaskRouterConfig
76
115
 
77
116
  factoryOptions?: AgentFactoryOptions
@@ -18,7 +18,36 @@ export interface PlanStep {
18
18
  id: string
19
19
  description: string
20
20
  toolName?: string
21
+
22
+ /**
23
+ * Which agent this step is to be delegated to, when it is delegated at all.
24
+ *
25
+ * `approve_plan` invites the model to name an agent per step, and that
26
+ * answer was reduced to a boolean: the step got `toolName: 'create_task'`
27
+ * if any agent was named and nothing if not. So the human approving the
28
+ * plan was shown THAT a step delegates and never TO WHOM — while the model
29
+ * had said, and the approval is the one moment where the difference can
30
+ * still be acted on. Approving "delegate this" is not approving "delegate
31
+ * this to the agent with shell access".
32
+ *
33
+ * Typed rather than folded into {@link estimatedInput}, which is `unknown`:
34
+ * an approval gate's whole job is being readable, and a field a host has to
35
+ * cast before it can render is one a host renders wrong or not at all.
36
+ *
37
+ * Absent means the step is the orchestrator's own work, which is what
38
+ * omitting `agent_id` in `approve_plan` says.
39
+ */
40
+ agentId?: string
41
+
42
+ /**
43
+ * **No producer and no reader.** Nothing in the SDK writes this and
44
+ * nothing reads it; it is declared here and that is all. Noted rather
45
+ * than removed because it is on the published typings — see
46
+ * {@link agentId}, which is the field the plan approval path actually
47
+ * needed and did not have.
48
+ */
21
49
  estimatedInput?: unknown
50
+
22
51
  dependsOn: string[]
23
52
  status: 'pending' | 'running' | 'completed' | 'skipped' | 'failed'
24
53
  error?: string
@@ -49,4 +49,43 @@ export interface LLMProvider {
49
49
  * so the doctor doesn't mark them as failing — see ses_007 Q6.4.
50
50
  */
51
51
  doctorCheck?(): Promise<DoctorCheckResult>
52
+
53
+ /**
54
+ * Which {@link ChatCompletionParams.effort} levels this model accepts,
55
+ * under the thinking configuration you intend to send with it.
56
+ *
57
+ * Asked rather than assumed because effort is **refused, not clamped**:
58
+ * a level a model does not have makes the vendor reject the request, so a
59
+ * caller offering a choice it cannot honour produces a run that fails at
60
+ * the start rather than a quieter one. Building that choice needs the
61
+ * answer BEFORE the request exists.
62
+ *
63
+ * There are three states and they mean different things:
64
+ *
65
+ * - **method absent** — this driver has no effort concept at all. Setting
66
+ * `effort` on a run using it is refused, not ignored, so a caller should
67
+ * offer no control rather than a disabled one.
68
+ * - **empty array** — the driver implements effort and THIS model has no
69
+ * levels. A real answer, not a missing one.
70
+ * - **non-empty** — offer exactly these, and nothing else.
71
+ *
72
+ * `thinking` is a parameter rather than the caller reading two sibling
73
+ * arrays, and that is the whole reason this is a function. At least one
74
+ * model family accepts a narrower set of levels while thinking is
75
+ * disabled than while it is on, so an API returning both sets invites a
76
+ * caller to render a picker from one and then send the other — a
77
+ * combination the vendor rejects, on exactly one family, discovered in
78
+ * production. Passing the configuration you are actually going to send
79
+ * makes that mistake unspellable: there is one answer and it is the one
80
+ * for your request.
81
+ *
82
+ * The levels are not stable across models and have moved twice already,
83
+ * so a caller must not copy the answer into its own table. A copy goes
84
+ * stale on the next model release and goes stale SILENTLY — surfacing as
85
+ * a vendor rejection rather than a failing build.
86
+ */
87
+ effortLevelsFor?(
88
+ model: string,
89
+ thinking?: import('./chat.js').ThinkingConfig,
90
+ ): readonly import('./chat.js').ReasoningEffort[]
52
91
  }
@@ -59,6 +59,23 @@ export interface Run {
59
59
  */
60
60
  structuredOutput?: unknown
61
61
 
62
+ /**
63
+ * Delegated tasks that were still running when this run ended.
64
+ *
65
+ * A run can settle while a worker it launched is still going — the model
66
+ * answered, a terminal tool decided the result, a `stopWhen` fired. The
67
+ * worker is NOT cancelled: giving up on a wait is a statement about the
68
+ * waiter, not about the work, and killing a child that may be mid-write
69
+ * because its parent finished early is a policy only the host can judge.
70
+ * A host that wants them stopped has `cancel_task` and the run controller.
71
+ *
72
+ * What the kernel owes instead is not pretending the results arrived.
73
+ * These ids are the honest form of that: the run says which work it walked
74
+ * away from, so a host can reconcile, cancel, or wait on them itself.
75
+ * Absent when a run ended with nothing outstanding.
76
+ */
77
+ abandonedTaskIds?: readonly string[]
78
+
62
79
  parentRunId?: RunId
63
80
 
64
81
  depth?: number
@@ -85,6 +85,48 @@ type CoreRunEvent =
85
85
  */
86
86
  reachedResetThreshold?: boolean
87
87
  }
88
+ /**
89
+ * A compaction pass ran and shed nothing, so the history is unchanged.
90
+ *
91
+ * A shed that did not happen is exactly as consequential as one that did,
92
+ * and until this existed only one of them was on the wire. The three
93
+ * decline paths all reached a log line — and a host that silences its
94
+ * logger, which every command-line entry point does, made a failed
95
+ * compaction invisible to the user, to the host AND to the model. The run
96
+ * then continued at full context toward a provider rejection several turns
97
+ * later that named none of this.
98
+ *
99
+ * The history is guaranteed untouched on every one of these: the reducer's
100
+ * result is installed whole or not at all, so there is no partial state to
101
+ * reason about. That is the property that makes reporting sufficient and a
102
+ * repair unnecessary.
103
+ */
104
+ | {
105
+ type: 'compaction_failed'
106
+ runId: RunId
107
+ iteration: number
108
+ /**
109
+ * Which decline path was taken. These want different responses, so
110
+ * a single "it failed" would put the reader back where the silence
111
+ * did:
112
+ *
113
+ * - `reducer_threw` — the reducer raised. Usually a bug or a failed
114
+ * model call inside a summarising reducer; the next pass may work.
115
+ * - `shed_nothing` — it returned no fewer messages than it was
116
+ * given. The history is already at its floor, or the reducer's
117
+ * own threshold disagrees with the trigger's, and every later
118
+ * pass will decline identically.
119
+ * - `split_tool_pair` — its result separated a `tool_use` from its
120
+ * `tool_result`, so it was refused wholesale rather than sent to
121
+ * a provider that rejects the pairing. A reducer bug, and one
122
+ * `findSafeTrimIndex` exists to prevent.
123
+ */
124
+ cause: 'reducer_threw' | 'shed_nothing' | 'split_tool_pair'
125
+ /** Unchanged, and stated so a reader need not infer it. */
126
+ messages: number
127
+ /** Present only for `reducer_threw`. */
128
+ error?: string
129
+ }
88
130
  | {
89
131
  type: 'tool_executing'
90
132
  runId: RunId
@@ -321,6 +363,39 @@ type CoreRunEvent =
321
363
  runId: RunId
322
364
  usage: TokenUsage
323
365
  cost: CostInfo
366
+ /**
367
+ * How large the CONTEXT is right now, and how large it may get.
368
+ *
369
+ * These are a different quantity from `usage` beside them and the
370
+ * distinction is the whole reason they are named this explicitly.
371
+ * `usage` is CUMULATIVE SPEND over the run: prompt plus completion
372
+ * tokens summed across every turn, monotonically increasing, and
373
+ * untouched by compaction. `contextTokens` is the size of the
374
+ * conversation being sent right now, which falls when a compaction
375
+ * sheds.
376
+ *
377
+ * Dividing the first by a context window is a category error, and
378
+ * it is one this estate shipped: a host did exactly that, so its
379
+ * indicator climbed toward full on any long run no matter how much
380
+ * room the conversation actually had — most wrong precisely when
381
+ * someone needed it most. The numbers are here so nobody has to
382
+ * reach for the wrong one, and named so reaching for it is a
383
+ * visible mistake rather than a plausible guess.
384
+ *
385
+ * `contextMeasuredBy` says whether the provider counted the prompt
386
+ * or we estimated it, and `windowSource` where the ceiling came
387
+ * from. A fraction of two numbers is only as honest as the weaker
388
+ * of them, and a surface rendering these owes a reader the same
389
+ * distinction rather than presenting an estimate as a measurement.
390
+ *
391
+ * Absent when the run has no compaction configuration, because
392
+ * nothing then resolves a window and inventing one would be the
393
+ * guess this exists to replace.
394
+ */
395
+ contextTokens?: number
396
+ contextMeasuredBy?: 'provider' | 'estimate'
397
+ contextWindowTokens?: number
398
+ windowSource?: 'config' | 'model-table' | 'default'
324
399
  }
325
400
  | {
326
401
  type: 'activity_created'
@@ -386,6 +461,21 @@ type CoreRunEvent =
386
461
  taskId: TaskId
387
462
  subject: string
388
463
  status: TaskStatus
464
+ /**
465
+ * What this unit waits on, and who claims it.
466
+ *
467
+ * The store maintains a full dependency graph — `blocks` and
468
+ * `blockedBy` are mirrored on both ends, written under a lock, and
469
+ * deadlock-avoided — and none of it reached the wire. So a host
470
+ * could show a flat list of units and nothing about their order,
471
+ * while the model was already maintaining the order.
472
+ *
473
+ * Absent rather than empty when the unit depends on nothing, so a
474
+ * reader can tell "no dependencies" from an emitter that predates
475
+ * these fields.
476
+ */
477
+ blockedBy?: readonly TaskId[]
478
+ owner?: string
389
479
  }
390
480
  | {
391
481
  type: 'task_updated'
@@ -394,6 +484,8 @@ type CoreRunEvent =
394
484
  subject: string
395
485
  status: TaskStatus
396
486
  owner?: string
487
+ /** See `task_created`. Carried on updates because an edge can be added later. */
488
+ blockedBy?: readonly TaskId[]
397
489
  }
398
490
  | {
399
491
  type: 'plugin_hook_executing'
@@ -44,8 +44,23 @@ export interface PrepareStepResult {
44
44
  /**
45
45
  * Restrict which tools the model may call this step, by name. Names
46
46
  * that are not registered are dropped with a warning rather than
47
- * failing the run — a phase list that outlives a tool rename should
48
- * narrow the surface, not kill the agent mid-run.
47
+ * failing the run.
48
+ *
49
+ * **Dropping every name leaves the step able to call nothing**, and that
50
+ * is deliberate rather than an accident of the filter. This list means
51
+ * "only these": if a rename outlives a phase list, the only set
52
+ * satisfying "only the tools that no longer exist" is the empty one, and
53
+ * widening back to the run's list would grant precisely what the caller
54
+ * did not ask for. The step is constrained, not crashed — the model
55
+ * answers from what it has and the run continues.
56
+ *
57
+ * This changed meaning when the list started bounding what may RUN
58
+ * rather than only what the model is shown. Before, an aged-out list hid
59
+ * every tool from the model while leaving all of them callable, which
60
+ * was neither reading.
61
+ *
62
+ * The warning is the part to watch: it goes to the logger, and a host
63
+ * that silences its logger sees a phase quietly stop doing anything.
49
64
  *
50
65
  * **This costs a prompt-cache prefix.** Tools render at position 0, so
51
66
  * changing the set between steps invalidates the cached prefix for that
@@ -1,9 +1,27 @@
1
1
  import type { RunId, TaskId, TenantId } from '../ids/index.js'
2
2
 
3
- export type TaskStatus = 'pending' | 'in_progress' | 'completed'
4
-
3
+ /**
4
+ * `failed` exists because a unit that did not succeed had nowhere to say so.
5
+ *
6
+ * Delegation wrote a failed worker's task as `completed` with the failure
7
+ * encoded as prose in `description` — so a reader scanning statuses saw work
8
+ * that had been done, and only a reader of every description saw otherwise. A
9
+ * status nobody can set is a status nobody can act on: a dependent unit cannot
10
+ * decide whether to wait or give up, and a plan cannot report that it did not
11
+ * finish.
12
+ */
13
+ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed'
14
+
15
+ /**
16
+ * Terminal means "will not change on its own", not "succeeded".
17
+ *
18
+ * `failed` is terminal for the same reason `completed` is: nothing downstream
19
+ * should wait on it. That matters most to the blocker check in the task
20
+ * listing — a dependent unit blocked on something that failed would otherwise
21
+ * wait forever for a status that will never arrive.
22
+ */
5
23
  export function isTerminalTaskStatus(status: TaskStatus): boolean {
6
- return status === 'completed'
24
+ return status === 'completed' || status === 'failed'
7
25
  }
8
26
 
9
27
  export function assertTaskStatus(status: TaskStatus): void {
@@ -11,6 +29,7 @@ export function assertTaskStatus(status: TaskStatus): void {
11
29
  case 'pending':
12
30
  case 'in_progress':
13
31
  case 'completed':
32
+ case 'failed':
14
33
  return
15
34
  default: {
16
35
  const _exhaustive: never = status
@@ -16,11 +16,62 @@ export type VerificationRule =
16
16
  | { type: 'allow_by_name'; toolNames: string[] }
17
17
  | { type: 'deny_by_name'; toolNames: string[] }
18
18
  | {
19
+ /**
20
+ * Match a regular expression against the tool's NAME, the
21
+ * serialised arguments, or both concatenated.
22
+ *
23
+ * Read `target: 'args'` carefully before writing one: it tests
24
+ * `JSON.stringify(toolInput)`, so the subject is the JSON TEXT of
25
+ * the whole argument object — `{"command":"git push origin main"}`
26
+ * — and not any single argument. The name suggests otherwise, and
27
+ * that is what makes it a trap: an anchored pattern like
28
+ * `^git push.*$` is a natural thing to write and can never match,
29
+ * so the rule silently decides nothing. `'both'` PREFIXES the tool
30
+ * name to that text rather than requiring it, so it is not a scope
31
+ * either — a rule written with `bash` in mind still sees every
32
+ * other tool's arguments.
33
+ *
34
+ * When you mean "this tool, this argument", use
35
+ * {@link VerificationRule} `argument_pattern` instead. This one
36
+ * stays for the case it is actually good at: matching anywhere in
37
+ * the serialised input without caring where.
38
+ */
19
39
  type: 'custom_pattern'
20
40
  pattern: string
21
41
  target: 'name' | 'args' | 'both'
22
42
  decision: 'allow' | 'deny'
23
43
  }
44
+ | {
45
+ /**
46
+ * Match a regular expression against ONE named argument of ONE
47
+ * named set of tools.
48
+ *
49
+ * This exists because `custom_pattern` could express neither half.
50
+ * It carries no tool scope, so a rule an operator wrote about
51
+ * `bash` decided `edit` calls too; and its argument target tests
52
+ * the serialised object, so pinning the tool cost the ability to
53
+ * anchor and anchoring cost the tool scope. Every pattern rule was
54
+ * therefore one of those two wrong things.
55
+ *
56
+ * The subject here is the argument's own VALUE, so `^git push`
57
+ * means what it looks like it means.
58
+ *
59
+ * A rule whose tool is not called, or whose argument is absent,
60
+ * decides nothing — the rule's precondition simply is not met. So
61
+ * does one whose argument holds an object or an array: a pattern
62
+ * cannot say anything true about a structured value, and pretending
63
+ * otherwise by matching its serialisation would reintroduce exactly
64
+ * the confusion this rule was added to remove. If you need to
65
+ * refuse a tool over the SHAPE of its input rather than a string in
66
+ * it, deny it by name.
67
+ */
68
+ type: 'argument_pattern'
69
+ toolNames: string[]
70
+ /** The argument key, at the top level of the tool's input. */
71
+ argument: string
72
+ pattern: string
73
+ decision: 'allow' | 'deny'
74
+ }
24
75
  | { type: 'allow_by_tier'; tiers: string[] }
25
76
 
26
77
  const AllowReadOnlySchema = z.object({ type: z.literal('allow_read_only') })
@@ -43,6 +94,15 @@ const CustomPatternSchema = z.object({
43
94
  target: z.enum(['name', 'args', 'both']),
44
95
  decision: z.enum(['allow', 'deny']),
45
96
  })
97
+ const ArgumentPatternSchema = z.object({
98
+ type: z.literal('argument_pattern'),
99
+ toolNames: z.array(z.string()).min(1),
100
+ // A rule that names no argument would silently apply to none, which is the
101
+ // fail-open shape this rule type exists to remove.
102
+ argument: z.string().min(1),
103
+ pattern: z.string().max(MAX_CUSTOM_PATTERN_LENGTH),
104
+ decision: z.enum(['allow', 'deny']),
105
+ })
46
106
  const AllowByTierSchema = z.object({
47
107
  type: z.literal('allow_by_tier'),
48
108
  tiers: z.array(z.string()),
@@ -55,6 +115,7 @@ export const VerificationRuleSchema = z.discriminatedUnion('type', [
55
115
  AllowByNameSchema,
56
116
  DenyByNameSchema,
57
117
  CustomPatternSchema,
118
+ ArgumentPatternSchema,
58
119
  AllowByTierSchema,
59
120
  ])
60
121
 
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { ToolDefinition } from '../../types/tool/index.js'
4
+ import type { VerificationGateConfig, VerificationRule } from '../../types/verification/index.js'
5
+ import { getRootLogger } from '../../utils/logger.js'
6
+ import { VerificationGate } from '../gate.js'
7
+
8
+ /**
9
+ * Every pattern rule an operator could write was one of two wrong things.
10
+ *
11
+ * `custom_pattern` carries no tool scope, so a rule written about `bash`
12
+ * decided `edit` calls as well. And `target: 'args'` tests
13
+ * `JSON.stringify(toolInput)`, so the subject is the JSON TEXT of the whole
14
+ * argument object — meaning the natural, anchored thing to write
15
+ * (`^git push.*$`) can never match, and the rule silently decides nothing.
16
+ * Pinning the tool cost the anchor; anchoring cost the tool scope.
17
+ */
18
+
19
+ function gate(rules: VerificationGateConfig['rules']): VerificationGate {
20
+ return new VerificationGate(
21
+ {
22
+ enabled: true,
23
+ rules,
24
+ allowReadOnlyTools: false,
25
+ denyDangerousPatterns: false,
26
+ logDecisions: false,
27
+ } as VerificationGateConfig,
28
+ getRootLogger(),
29
+ )
30
+ }
31
+
32
+ /** Only the fields the gate reads. */
33
+ function toolDef(name: string): ToolDefinition {
34
+ return { name, isReadOnly: () => false } as unknown as ToolDefinition
35
+ }
36
+
37
+ const PUSH_RULE: VerificationRule = {
38
+ type: 'argument_pattern',
39
+ toolNames: ['bash'],
40
+ argument: 'command',
41
+ pattern: '^git push',
42
+ decision: 'deny',
43
+ }
44
+
45
+ function evaluate(
46
+ rules: VerificationGateConfig['rules'],
47
+ toolName: string,
48
+ toolInput: unknown,
49
+ ): ReturnType<VerificationGate['evaluate']> {
50
+ return gate(rules).evaluate({ toolName, toolInput, toolDef: toolDef(toolName) })
51
+ }
52
+
53
+ describe('a rule can name one tool and one argument at once', () => {
54
+ it('denies the call it was written about', () => {
55
+ const result = evaluate([PUSH_RULE], 'bash', { command: 'git push origin main' })
56
+ expect(result.decision).toBe('deny')
57
+ })
58
+
59
+ it('anchors against the value, which is what the old rule could not do', () => {
60
+ // The whole defect in one assertion: this same pattern under
61
+ // `custom_pattern` with `target: 'args'` is tested against
62
+ // `{"command":"git push origin main"}`, where `^git push` cannot match
63
+ // because the string does not start there.
64
+ const viaOldRule = evaluate(
65
+ [{ type: 'custom_pattern', pattern: '^git push', target: 'args', decision: 'deny' }],
66
+ 'bash',
67
+ { command: 'git push origin main' },
68
+ )
69
+ expect(viaOldRule.decision, 'the old rule silently decided nothing').not.toBe('deny')
70
+
71
+ expect(evaluate([PUSH_RULE], 'bash', { command: 'git push origin main' }).decision).toBe('deny')
72
+ })
73
+
74
+ it('leaves a different tool alone', () => {
75
+ // The other half: a rule about `bash` used to decide `edit` too,
76
+ // because the pattern was matched against a serialisation that has no
77
+ // idea which tool produced it.
78
+ const result = evaluate([PUSH_RULE], 'edit', { command: 'git push origin main' })
79
+ expect(result.decision).not.toBe('deny')
80
+ })
81
+
82
+ it('leaves a different argument alone', () => {
83
+ const result = evaluate([PUSH_RULE], 'bash', { description: 'git push origin main' })
84
+ expect(result.decision).not.toBe('deny')
85
+ })
86
+
87
+ it('says which argument decided, so a model knows whether to reword', () => {
88
+ const result = evaluate([PUSH_RULE], 'bash', { command: 'git push origin main' })
89
+
90
+ expect(result.reason).toContain('command')
91
+ expect(result.reason).toContain('^git push')
92
+ expect(result.reason).toContain('bash')
93
+ })
94
+ })
95
+
96
+ describe('what it deliberately does not decide', () => {
97
+ it('decides nothing when the argument is absent', () => {
98
+ const result = evaluate([PUSH_RULE], 'bash', {})
99
+ expect(result.decision).not.toBe('deny')
100
+ })
101
+
102
+ it('decides nothing about a structured argument', () => {
103
+ // No string a pattern could match says anything true about an object,
104
+ // and serialising it to try would put this rule back where
105
+ // `custom_pattern` already is. An operator who needs to refuse a tool
106
+ // over the SHAPE of its input wants deny_by_name.
107
+ const rule: VerificationRule = { ...PUSH_RULE, argument: 'env', pattern: 'PROD' }
108
+ const result = evaluate([rule], 'bash', { env: { NODE_ENV: 'PROD' } })
109
+
110
+ expect(result.decision).not.toBe('deny')
111
+ })
112
+
113
+ it('reads a number or a boolean rather than skipping it', () => {
114
+ // These render unambiguously, so skipping them would be a fail-open
115
+ // with no upside: a rule about a numeric argument is a rule someone
116
+ // can reasonably write.
117
+ const rule: VerificationRule = {
118
+ type: 'argument_pattern',
119
+ toolNames: ['sleep'],
120
+ argument: 'seconds',
121
+ pattern: '^[0-9]{4,}$',
122
+ decision: 'deny',
123
+ }
124
+
125
+ expect(evaluate([rule], 'sleep', { seconds: 86400 }).decision).toBe('deny')
126
+ expect(evaluate([rule], 'sleep', { seconds: 5 }).decision).not.toBe('deny')
127
+ })
128
+ })
129
+
130
+ describe('a rule that cannot be compiled decides nothing at all', () => {
131
+ it('does not widen into a rule about the whole tool', () => {
132
+ // The failure this forbids: a typo'd regex turning "deny bash when its
133
+ // command matches X" into "deny bash" — a far larger authorization than
134
+ // anybody wrote, granted by a mistake nobody would notice.
135
+ //
136
+ // What actually secures it is the missing-pattern check at the top of
137
+ // `evaluateRule`, which returns before the tool name is consulted. The
138
+ // gate's construction order (compile first, only then record the names)
139
+ // is defence in depth and NOT the mechanism: reversing those two lines
140
+ // fails nothing, which was measured rather than assumed. So this test
141
+ // pins the OUTCOME and the comment in the gate says which line to keep.
142
+ const broken: VerificationRule = { ...PUSH_RULE, pattern: '([unclosed' }
143
+ const result = evaluate([broken], 'bash', { command: 'ls' })
144
+
145
+ expect(result.decision).not.toBe('deny')
146
+ })
147
+
148
+ it('is secured by the pattern check, not by the construction order', () => {
149
+ // The honest version of the mutation: remove the check that actually
150
+ // holds and this fails. A rule whose pattern never compiled has no
151
+ // pattern to test, so it must decide nothing even for a tool it names.
152
+ const broken: VerificationRule = { ...PUSH_RULE, pattern: '([unclosed' }
153
+
154
+ expect(evaluate([broken], 'bash', { command: 'git push origin main' }).decision).not.toBe(
155
+ 'deny',
156
+ )
157
+ })
158
+ })