@namzu/sdk 0.1.5-rc.1-fix → 0.1.5

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 (224) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +279 -129
  3. package/dist/bridge/tools/connector/adapter.d.ts +2 -2
  4. package/dist/bridge/tools/connector/adapter.d.ts.map +1 -1
  5. package/dist/bridge/tools/connector/adapter.js +3 -1
  6. package/dist/bridge/tools/connector/adapter.js.map +1 -1
  7. package/dist/connector/BaseConnector.d.ts +2 -1
  8. package/dist/connector/BaseConnector.d.ts.map +1 -1
  9. package/dist/connector/BaseConnector.js.map +1 -1
  10. package/dist/connector/builtins/http.d.ts +1 -1
  11. package/dist/connector/builtins/http.d.ts.map +1 -1
  12. package/dist/connector/builtins/http.js +1 -1
  13. package/dist/connector/builtins/http.js.map +1 -1
  14. package/dist/connector/builtins/webhook.d.ts +1 -1
  15. package/dist/connector/builtins/webhook.d.ts.map +1 -1
  16. package/dist/connector/builtins/webhook.js +1 -1
  17. package/dist/connector/builtins/webhook.js.map +1 -1
  18. package/dist/contracts/api.d.ts.map +1 -1
  19. package/dist/contracts/index.d.ts +1 -0
  20. package/dist/contracts/index.d.ts.map +1 -1
  21. package/dist/contracts/index.js +7 -0
  22. package/dist/contracts/index.js.map +1 -1
  23. package/dist/index.d.ts +5 -33
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -23
  26. package/dist/index.js.map +1 -1
  27. package/dist/manager/connector/environment.d.ts +4 -4
  28. package/dist/manager/connector/environment.d.ts.map +1 -1
  29. package/dist/manager/connector/environment.js.map +1 -1
  30. package/dist/manager/connector/lifecycle.d.ts +2 -2
  31. package/dist/manager/connector/lifecycle.d.ts.map +1 -1
  32. package/dist/manager/connector/lifecycle.js.map +1 -1
  33. package/dist/manager/connector/tenant.d.ts +3 -3
  34. package/dist/manager/connector/tenant.d.ts.map +1 -1
  35. package/dist/manager/connector/tenant.js.map +1 -1
  36. package/dist/manager/index.d.ts +2 -1
  37. package/dist/manager/index.d.ts.map +1 -1
  38. package/dist/manager/index.js +2 -1
  39. package/dist/manager/index.js.map +1 -1
  40. package/dist/manager/run/emergency.d.ts.map +1 -1
  41. package/dist/manager/run/emergency.js +44 -12
  42. package/dist/manager/run/emergency.js.map +1 -1
  43. package/dist/manager/run/persistence.d.ts +0 -2
  44. package/dist/manager/run/persistence.d.ts.map +1 -1
  45. package/dist/manager/run/persistence.js +0 -1
  46. package/dist/manager/run/persistence.js.map +1 -1
  47. package/dist/plugin/__tests__/enable-contributions.test.d.ts +2 -0
  48. package/dist/plugin/__tests__/enable-contributions.test.d.ts.map +1 -0
  49. package/dist/plugin/__tests__/enable-contributions.test.js +265 -0
  50. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -0
  51. package/dist/plugin/__tests__/lifecycle.test.js +30 -0
  52. package/dist/plugin/__tests__/lifecycle.test.js.map +1 -1
  53. package/dist/plugin/lifecycle.d.ts +4 -1
  54. package/dist/plugin/lifecycle.d.ts.map +1 -1
  55. package/dist/plugin/lifecycle.js +157 -49
  56. package/dist/plugin/lifecycle.js.map +1 -1
  57. package/dist/rag/vector-store.d.ts +2 -2
  58. package/dist/rag/vector-store.d.ts.map +1 -1
  59. package/dist/rag/vector-store.js.map +1 -1
  60. package/dist/registry/ManagedRegistry.d.ts +6 -0
  61. package/dist/registry/ManagedRegistry.d.ts.map +1 -1
  62. package/dist/registry/ManagedRegistry.js +9 -3
  63. package/dist/registry/ManagedRegistry.js.map +1 -1
  64. package/dist/registry/agent/definitions.d.ts +2 -7
  65. package/dist/registry/agent/definitions.d.ts.map +1 -1
  66. package/dist/registry/agent/definitions.js +3 -41
  67. package/dist/registry/agent/definitions.js.map +1 -1
  68. package/dist/registry/connector/definitions.d.ts +2 -7
  69. package/dist/registry/connector/definitions.d.ts.map +1 -1
  70. package/dist/registry/connector/definitions.js +3 -41
  71. package/dist/registry/connector/definitions.js.map +1 -1
  72. package/dist/registry/connector/scoped.d.ts +5 -4
  73. package/dist/registry/connector/scoped.d.ts.map +1 -1
  74. package/dist/registry/connector/scoped.js.map +1 -1
  75. package/dist/registry/index.d.ts +1 -0
  76. package/dist/registry/index.d.ts.map +1 -1
  77. package/dist/registry/index.js +1 -0
  78. package/dist/registry/index.js.map +1 -1
  79. package/dist/registry/tool/execute.d.ts +2 -4
  80. package/dist/registry/tool/execute.d.ts.map +1 -1
  81. package/dist/registry/tool/execute.js +4 -21
  82. package/dist/registry/tool/execute.js.map +1 -1
  83. package/dist/run/index.d.ts +4 -4
  84. package/dist/run/index.d.ts.map +1 -1
  85. package/dist/run/index.js +3 -3
  86. package/dist/run/index.js.map +1 -1
  87. package/dist/run/reporter.d.ts +0 -2
  88. package/dist/run/reporter.d.ts.map +1 -1
  89. package/dist/run/reporter.js +0 -1
  90. package/dist/run/reporter.js.map +1 -1
  91. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts +2 -0
  92. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts.map +1 -0
  93. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +236 -0
  94. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -0
  95. package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts +2 -0
  96. package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts.map +1 -0
  97. package/dist/runtime/query/__tests__/plugin-hooks.test.js +21 -0
  98. package/dist/runtime/query/__tests__/plugin-hooks.test.js.map +1 -0
  99. package/dist/runtime/query/executor.d.ts +6 -0
  100. package/dist/runtime/query/executor.d.ts.map +1 -1
  101. package/dist/runtime/query/executor.js +110 -6
  102. package/dist/runtime/query/executor.js.map +1 -1
  103. package/dist/runtime/query/index.d.ts.map +1 -1
  104. package/dist/runtime/query/index.js +8 -6
  105. package/dist/runtime/query/index.js.map +1 -1
  106. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  107. package/dist/runtime/query/iteration/index.js +13 -16
  108. package/dist/runtime/query/iteration/index.js.map +1 -1
  109. package/dist/runtime/query/plugin-hooks.d.ts +11 -0
  110. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -0
  111. package/dist/runtime/query/plugin-hooks.js +28 -0
  112. package/dist/runtime/query/plugin-hooks.js.map +1 -0
  113. package/dist/runtime/query/tooling.d.ts +2 -0
  114. package/dist/runtime/query/tooling.d.ts.map +1 -1
  115. package/dist/runtime/query/tooling.js +1 -0
  116. package/dist/runtime/query/tooling.js.map +1 -1
  117. package/dist/store/index.d.ts +5 -1
  118. package/dist/store/index.d.ts.map +1 -1
  119. package/dist/store/index.js +4 -1
  120. package/dist/store/index.js.map +1 -1
  121. package/dist/store/run/disk.d.ts +0 -2
  122. package/dist/store/run/disk.d.ts.map +1 -1
  123. package/dist/store/run/disk.js +0 -1
  124. package/dist/store/run/disk.js.map +1 -1
  125. package/dist/store/task/__tests__/disk-concurrency.test.d.ts +2 -0
  126. package/dist/store/task/__tests__/disk-concurrency.test.d.ts.map +1 -0
  127. package/dist/store/task/__tests__/disk-concurrency.test.js +91 -0
  128. package/dist/store/task/__tests__/disk-concurrency.test.js.map +1 -0
  129. package/dist/store/task/disk.d.ts +6 -0
  130. package/dist/store/task/disk.d.ts.map +1 -1
  131. package/dist/store/task/disk.js +150 -36
  132. package/dist/store/task/disk.js.map +1 -1
  133. package/dist/types/connector/core.d.ts +2 -2
  134. package/dist/types/connector/core.d.ts.map +1 -1
  135. package/dist/types/connector/definition.d.ts +7 -7
  136. package/dist/types/connector/definition.d.ts.map +1 -1
  137. package/dist/types/connector/mcp.d.ts +4 -4
  138. package/dist/types/connector/mcp.d.ts.map +1 -1
  139. package/dist/types/connector/scope.d.ts +3 -2
  140. package/dist/types/connector/scope.d.ts.map +1 -1
  141. package/dist/types/connector/scope.js.map +1 -1
  142. package/dist/types/connector/tenant.d.ts +4 -4
  143. package/dist/types/connector/tenant.d.ts.map +1 -1
  144. package/dist/types/plugin/index.d.ts +4 -21
  145. package/dist/types/plugin/index.d.ts.map +1 -1
  146. package/dist/types/plugin/index.js +1 -10
  147. package/dist/types/plugin/index.js.map +1 -1
  148. package/dist/types/rag/knowledge-base.d.ts +2 -2
  149. package/dist/types/rag/knowledge-base.d.ts.map +1 -1
  150. package/dist/types/rag/scope.d.ts +2 -1
  151. package/dist/types/rag/scope.d.ts.map +1 -1
  152. package/dist/types/rag/storage.d.ts +3 -3
  153. package/dist/types/rag/storage.d.ts.map +1 -1
  154. package/dist/types/rag/vector.d.ts +3 -3
  155. package/dist/types/rag/vector.d.ts.map +1 -1
  156. package/dist/types/run/config.d.ts +0 -6
  157. package/dist/types/run/config.d.ts.map +1 -1
  158. package/dist/vault/InMemoryCredentialVault.d.ts +3 -3
  159. package/dist/vault/InMemoryCredentialVault.d.ts.map +1 -1
  160. package/dist/vault/InMemoryCredentialVault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/bridge/tools/connector/adapter.ts +5 -3
  163. package/src/connector/BaseConnector.ts +2 -1
  164. package/src/connector/builtins/http.ts +1 -1
  165. package/src/connector/builtins/webhook.ts +1 -1
  166. package/src/contracts/api.ts +3 -0
  167. package/src/contracts/index.ts +10 -0
  168. package/src/index.ts +46 -41
  169. package/src/manager/connector/environment.ts +5 -5
  170. package/src/manager/connector/lifecycle.ts +2 -2
  171. package/src/manager/connector/tenant.ts +8 -3
  172. package/src/manager/index.ts +2 -1
  173. package/src/manager/run/emergency.ts +45 -16
  174. package/src/manager/run/persistence.ts +0 -4
  175. package/src/plugin/__tests__/enable-contributions.test.ts +295 -0
  176. package/src/plugin/__tests__/lifecycle.test.ts +44 -0
  177. package/src/plugin/lifecycle.ts +186 -57
  178. package/src/rag/vector-store.ts +2 -2
  179. package/src/registry/ManagedRegistry.ts +14 -3
  180. package/src/registry/agent/definitions.ts +3 -53
  181. package/src/registry/connector/definitions.ts +3 -53
  182. package/src/registry/connector/scoped.ts +7 -6
  183. package/src/registry/index.ts +1 -0
  184. package/src/registry/tool/execute.ts +4 -22
  185. package/src/run/index.ts +4 -4
  186. package/src/run/reporter.ts +0 -4
  187. package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +305 -0
  188. package/src/runtime/query/__tests__/plugin-hooks.test.ts +28 -0
  189. package/src/runtime/query/executor.ts +148 -7
  190. package/src/runtime/query/index.ts +16 -6
  191. package/src/runtime/query/iteration/index.ts +29 -16
  192. package/src/runtime/query/plugin-hooks.ts +34 -0
  193. package/src/runtime/query/tooling.ts +3 -0
  194. package/src/store/index.ts +6 -1
  195. package/src/store/run/disk.ts +0 -4
  196. package/src/store/task/__tests__/disk-concurrency.test.ts +118 -0
  197. package/src/store/task/disk.ts +150 -37
  198. package/src/types/connector/core.ts +2 -2
  199. package/src/types/connector/definition.ts +7 -7
  200. package/src/types/connector/mcp.ts +4 -4
  201. package/src/types/connector/scope.ts +3 -2
  202. package/src/types/connector/tenant.ts +4 -4
  203. package/src/types/plugin/index.ts +0 -14
  204. package/src/types/rag/knowledge-base.ts +2 -2
  205. package/src/types/rag/scope.ts +3 -1
  206. package/src/types/rag/storage.ts +3 -3
  207. package/src/types/rag/vector.ts +3 -3
  208. package/src/types/run/config.ts +0 -9
  209. package/src/vault/InMemoryCredentialVault.ts +3 -3
  210. package/dist/manager/agent/index.d.ts +0 -2
  211. package/dist/manager/agent/index.d.ts.map +0 -1
  212. package/dist/manager/agent/index.js +0 -2
  213. package/dist/manager/agent/index.js.map +0 -1
  214. package/dist/registry/agent/index.d.ts +0 -2
  215. package/dist/registry/agent/index.d.ts.map +0 -1
  216. package/dist/registry/agent/index.js +0 -2
  217. package/dist/registry/agent/index.js.map +0 -1
  218. package/dist/router/index.d.ts +0 -2
  219. package/dist/router/index.d.ts.map +0 -1
  220. package/dist/router/index.js +0 -2
  221. package/dist/router/index.js.map +0 -1
  222. package/src/manager/agent/index.ts +0 -1
  223. package/src/registry/agent/index.ts +0 -1
  224. package/src/router/index.ts +0 -1
@@ -1,14 +1,16 @@
1
1
  import { extractFromToolCall, extractFromToolResult } from '../../compaction/extractor.js'
2
2
  import type { WorkingStateManager } from '../../compaction/manager.js'
3
+ import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
3
4
  import type { ActivityStore } from '../../store/activity/memory.js'
4
5
  import type { RunId } from '../../types/ids/index.js'
5
6
  import type { InvocationState } from '../../types/invocation/index.js'
6
7
  import { type Message, createToolMessage } from '../../types/message/index.js'
7
8
  import type { PermissionMode } from '../../types/permission/index.js'
9
+ import type { PluginHookResult } from '../../types/plugin/index.js'
8
10
  import type { ChatCompletionResponse } from '../../types/provider/index.js'
9
11
  import type { RunEvent } from '../../types/run/index.js'
10
12
  import type { Sandbox } from '../../types/sandbox/index.js'
11
- import type { ToolContext, ToolRegistryContract } from '../../types/tool/index.js'
13
+ import type { ToolContext, ToolRegistryContract, ToolResult } from '../../types/tool/index.js'
12
14
  import type { Logger } from '../../utils/logger.js'
13
15
  import { compressShellOutput } from '../../utils/shell-compress.js'
14
16
 
@@ -23,8 +25,14 @@ export interface ToolExecutorConfig {
23
25
  abortSignal: AbortSignal
24
26
  sandbox?: Sandbox
25
27
  invocationState?: InvocationState
28
+ pluginManager?: PluginLifecycleManager
26
29
  }
27
30
 
31
+ type PreToolHookOutcome =
32
+ | { kind: 'continue'; input: unknown }
33
+ | { kind: 'skip'; input: unknown; output: string }
34
+ | { kind: 'error'; input: unknown; output: string }
35
+
28
36
  export interface ToolExecutionBatch {
29
37
  messages: Message[]
30
38
  results: Array<{ toolCallId: string; output: string }>
@@ -118,6 +126,12 @@ export class ToolExecutor {
118
126
  }
119
127
  }
120
128
 
129
+ const preOutcome = await this.runPreToolHook(toolName, input)
130
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
131
+ return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome)
132
+ }
133
+ input = preOutcome.input
134
+
121
135
  const activity = this.activityStore.create({
122
136
  type: 'tool_call',
123
137
  description: toolName,
@@ -137,7 +151,7 @@ export class ToolExecutor {
137
151
  })
138
152
 
139
153
  if (this.workingStateManager) {
140
- extractFromToolCall(this.workingStateManager, toolName, toolCall.function.arguments)
154
+ extractFromToolCall(this.workingStateManager, toolName, JSON.stringify(input))
141
155
  }
142
156
 
143
157
  const startMs = Date.now()
@@ -148,10 +162,17 @@ export class ToolExecutor {
148
162
  ? result.output
149
163
  : `Error: ${result.error ?? 'Tool execution failed'}`
150
164
 
151
- const output = result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput
165
+ let output = result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput
166
+
167
+ const postOverride = await this.runPostToolHook(toolName, input, result)
168
+ if (postOverride !== null) {
169
+ output = postOverride
170
+ }
171
+
172
+ const effectiveIsError = !result.success || postOverride !== null
152
173
 
153
174
  if (this.workingStateManager) {
154
- extractFromToolResult(this.workingStateManager, toolName, output, !result.success)
175
+ extractFromToolResult(this.workingStateManager, toolName, output, effectiveIsError)
155
176
  }
156
177
 
157
178
  if (result.success) {
@@ -171,10 +192,10 @@ export class ToolExecutor {
171
192
  }
172
193
 
173
194
  if (activity) {
174
- if (result.success) {
175
- this.activityStore.complete(activity.id, output)
195
+ if (effectiveIsError) {
196
+ this.activityStore.fail(activity.id, output)
176
197
  } else {
177
- this.activityStore.fail(activity.id, result.error ?? 'Tool execution failed')
198
+ this.activityStore.complete(activity.id, output)
178
199
  }
179
200
  }
180
201
 
@@ -188,6 +209,126 @@ export class ToolExecutor {
188
209
  return { toolCallId: toolCall.id, output }
189
210
  }
190
211
 
212
+ private async runPreToolHook(toolName: string, input: unknown): Promise<PreToolHookOutcome> {
213
+ if (!this.config.pluginManager) return { kind: 'continue', input }
214
+ const results = await this.config.pluginManager.executeHooks(
215
+ 'pre_tool_use',
216
+ { runId: this.config.runId, toolName, toolInput: input },
217
+ this.emitEvent,
218
+ )
219
+ return this.interpretPreToolResults(toolName, input, results)
220
+ }
221
+
222
+ private interpretPreToolResults(
223
+ toolName: string,
224
+ initialInput: unknown,
225
+ results: readonly PluginHookResult[],
226
+ ): PreToolHookOutcome {
227
+ let currentInput = initialInput
228
+ for (const result of results) {
229
+ switch (result.action) {
230
+ case 'continue':
231
+ continue
232
+ case 'modify':
233
+ currentInput = result.input
234
+ continue
235
+ case 'skip':
236
+ return {
237
+ kind: 'skip',
238
+ input: currentInput,
239
+ output: `Tool ${toolName} skipped by plugin: ${result.reason}`,
240
+ }
241
+ case 'error':
242
+ return {
243
+ kind: 'error',
244
+ input: currentInput,
245
+ output: `Error: ${result.message}`,
246
+ }
247
+ case 'retry':
248
+ case 'resume':
249
+ throw new Error(
250
+ `Plugin hook pre_tool_use returned unsupported action '${result.action}' for tool ${toolName}`,
251
+ )
252
+ default: {
253
+ const _exhaustive: never = result
254
+ throw new Error(`Unknown PluginHookResult: ${JSON.stringify(_exhaustive)}`)
255
+ }
256
+ }
257
+ }
258
+ return { kind: 'continue', input: currentInput }
259
+ }
260
+
261
+ private async runPostToolHook(
262
+ toolName: string,
263
+ input: unknown,
264
+ toolResult: ToolResult,
265
+ ): Promise<string | null> {
266
+ if (!this.config.pluginManager) return null
267
+ const results = await this.config.pluginManager.executeHooks(
268
+ 'post_tool_use',
269
+ { runId: this.config.runId, toolName, toolInput: input, toolResult },
270
+ this.emitEvent,
271
+ )
272
+ let override: string | null = null
273
+ for (const result of results) {
274
+ switch (result.action) {
275
+ case 'continue':
276
+ continue
277
+ case 'error':
278
+ override = `Error: ${result.message}`
279
+ continue
280
+ case 'skip':
281
+ case 'modify':
282
+ case 'retry':
283
+ case 'resume':
284
+ throw new Error(
285
+ `Plugin hook post_tool_use returned unsupported action '${result.action}' for tool ${toolName}`,
286
+ )
287
+ default: {
288
+ const _exhaustive: never = result
289
+ throw new Error(`Unknown PluginHookResult: ${JSON.stringify(_exhaustive)}`)
290
+ }
291
+ }
292
+ }
293
+ return override
294
+ }
295
+
296
+ private async recordSyntheticHookOutcome(
297
+ toolCallId: string,
298
+ toolName: string,
299
+ input: unknown,
300
+ outcome: { kind: 'skip' | 'error'; output: string },
301
+ ): Promise<{ toolCallId: string; output: string }> {
302
+ const activity = this.activityStore.create({
303
+ type: 'tool_call',
304
+ description: toolName,
305
+ input,
306
+ toolName,
307
+ toolCallId,
308
+ })
309
+ if (activity) {
310
+ this.activityStore.start(activity.id)
311
+ if (outcome.kind === 'skip') {
312
+ this.activityStore.complete(activity.id, outcome.output)
313
+ } else {
314
+ this.activityStore.fail(activity.id, outcome.output)
315
+ }
316
+ }
317
+ await this.emitEvent({
318
+ type: 'tool_executing',
319
+ runId: this.config.runId,
320
+ toolName,
321
+ input,
322
+ })
323
+ await this.emitEvent({
324
+ type: 'tool_completed',
325
+ runId: this.config.runId,
326
+ toolName,
327
+ result: outcome.output,
328
+ })
329
+ return { toolCallId, output: outcome.output }
330
+ }
331
+
191
332
  private maybeCompress(toolName: string, output: string): string {
192
333
  const tool = this.config.tools.get(toolName)
193
334
  if (!tool || tool.category !== 'shell') {
@@ -40,6 +40,7 @@ import { RunContextFactory } from './context.js'
40
40
  import { EventTranslator } from './events.js'
41
41
  import { GuardCoordinator } from './guard.js'
42
42
  import { IterationOrchestrator } from './iteration/index.js'
43
+ import { applyLifecycleHookResults } from './plugin-hooks.js'
43
44
  import { PromptBuilder } from './prompt.js'
44
45
  import type { PromptSegments } from './prompt.js'
45
46
  import { ResultAssembler } from './result.js'
@@ -193,6 +194,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Agen
193
194
  env: params.runConfig.env ?? {},
194
195
  abortSignal: ctx.abortController.signal,
195
196
  invocationState: params.invocationState,
197
+ pluginManager: params.pluginManager,
196
198
  },
197
199
  ctx.activityStore,
198
200
  eventTranslator.emitEvent,
@@ -397,9 +399,13 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Agen
397
399
  yield* eventTranslator.drainPending()
398
400
 
399
401
  if (params.pluginManager) {
400
- await params.pluginManager.executeHooks('run_start', {
401
- runId: ctx.runId,
402
- })
402
+ const hookResults = await params.pluginManager.executeHooks(
403
+ 'run_start',
404
+ { runId: ctx.runId },
405
+ eventTranslator.emitEvent,
406
+ )
407
+ applyLifecycleHookResults('run_start', hookResults)
408
+ yield* eventTranslator.drainPending()
403
409
  }
404
410
 
405
411
  // --- Sandbox lifecycle: create before iteration loop ---
@@ -429,9 +435,13 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Agen
429
435
  yield* iterationOrchestrator.runLoop()
430
436
 
431
437
  if (params.pluginManager) {
432
- await params.pluginManager.executeHooks('run_end', {
433
- runId: ctx.runId,
434
- })
438
+ const hookResults = await params.pluginManager.executeHooks(
439
+ 'run_end',
440
+ { runId: ctx.runId },
441
+ eventTranslator.emitEvent,
442
+ )
443
+ applyLifecycleHookResults('run_end', hookResults)
444
+ yield* eventTranslator.drainPending()
435
445
  }
436
446
 
437
447
  yield* resultAssembler.completeRun(rootSpan)
@@ -19,6 +19,7 @@ import type { CheckpointManager } from '../checkpoint.js'
19
19
  import type { EmitEvent } from '../events.js'
20
20
  import type { ToolExecutor } from '../executor.js'
21
21
  import type { GuardCoordinator } from '../guard.js'
22
+ import { applyLifecycleHookResults } from '../plugin-hooks.js'
22
23
  import { runAdvisoryPhase } from './phases/advisory.js'
23
24
  import { runIterationCheckpoint } from './phases/checkpoint.js'
24
25
  import { runCompactionCheck } from './phases/compaction.js'
@@ -175,10 +176,13 @@ export class IterationOrchestrator {
175
176
 
176
177
  try {
177
178
  if (this.ctx.pluginManager) {
178
- await this.ctx.pluginManager.executeHooks('iteration_start', {
179
- runId: runMgr.id,
180
- iteration: iterationNum,
181
- })
179
+ const hookResults = await this.ctx.pluginManager.executeHooks(
180
+ 'iteration_start',
181
+ { runId: runMgr.id, iteration: iterationNum },
182
+ this.ctx.emitEvent,
183
+ )
184
+ applyLifecycleHookResults('iteration_start', hookResults)
185
+ yield* this.ctx.drainPending()
182
186
  }
183
187
 
184
188
  if (this.ctx.pendingNotifications.length > 0) {
@@ -201,10 +205,13 @@ export class IterationOrchestrator {
201
205
  : runMgr.messages
202
206
 
203
207
  if (this.ctx.pluginManager) {
204
- await this.ctx.pluginManager.executeHooks('pre_llm_call', {
205
- runId: runMgr.id,
206
- iteration: iterationNum,
207
- })
208
+ const hookResults = await this.ctx.pluginManager.executeHooks(
209
+ 'pre_llm_call',
210
+ { runId: runMgr.id, iteration: iterationNum },
211
+ this.ctx.emitEvent,
212
+ )
213
+ applyLifecycleHookResults('pre_llm_call', hookResults)
214
+ yield* this.ctx.drainPending()
208
215
  }
209
216
 
210
217
  const response = await this.ctx.provider.chat({
@@ -219,10 +226,13 @@ export class IterationOrchestrator {
219
226
  runMgr.accumulateUsage(response.usage)
220
227
 
221
228
  if (this.ctx.pluginManager) {
222
- await this.ctx.pluginManager.executeHooks('post_llm_call', {
223
- runId: runMgr.id,
224
- iteration: iterationNum,
225
- })
229
+ const hookResults = await this.ctx.pluginManager.executeHooks(
230
+ 'post_llm_call',
231
+ { runId: runMgr.id, iteration: iterationNum },
232
+ this.ctx.emitEvent,
233
+ )
234
+ applyLifecycleHookResults('post_llm_call', hookResults)
235
+ yield* this.ctx.drainPending()
226
236
  }
227
237
 
228
238
  this.ctx.log.debug('LLM response received', {
@@ -357,10 +367,13 @@ export class IterationOrchestrator {
357
367
  await runAdvisoryPhase(this.ctx, iterationNum, response)
358
368
 
359
369
  if (this.ctx.pluginManager) {
360
- await this.ctx.pluginManager.executeHooks('iteration_end', {
361
- runId: runMgr.id,
362
- iteration: iterationNum,
363
- })
370
+ const hookResults = await this.ctx.pluginManager.executeHooks(
371
+ 'iteration_end',
372
+ { runId: runMgr.id, iteration: iterationNum },
373
+ this.ctx.emitEvent,
374
+ )
375
+ applyLifecycleHookResults('iteration_end', hookResults)
376
+ yield* this.ctx.drainPending()
364
377
  }
365
378
 
366
379
  await this.ctx.emitEvent({
@@ -0,0 +1,34 @@
1
+ import type { PluginHookEvent, PluginHookResult } from '../../types/plugin/index.js'
2
+
3
+ /**
4
+ * Interpret hook results from non-tool lifecycle events (run_start, run_end,
5
+ * iteration_start, iteration_end, pre_llm_call, post_llm_call). These contexts
6
+ * carry no mutable payload, so only `continue` and `error` are meaningful.
7
+ *
8
+ * Throws on `error` (propagated to run-failure path) or on action misuse
9
+ * (skip / modify / retry / resume have no defined contract here).
10
+ */
11
+ export function applyLifecycleHookResults(
12
+ event: PluginHookEvent,
13
+ results: readonly PluginHookResult[],
14
+ ): void {
15
+ for (const result of results) {
16
+ switch (result.action) {
17
+ case 'continue':
18
+ continue
19
+ case 'error':
20
+ throw new Error(`Plugin hook ${event} reported error: ${result.message}`)
21
+ case 'skip':
22
+ case 'modify':
23
+ case 'retry':
24
+ case 'resume':
25
+ throw new Error(
26
+ `Plugin hook ${event} returned unsupported action '${result.action}' for a lifecycle event`,
27
+ )
28
+ default: {
29
+ const _exhaustive: never = result
30
+ throw new Error(`Unknown PluginHookResult: ${JSON.stringify(_exhaustive)}`)
31
+ }
32
+ }
33
+ }
34
+ }
@@ -1,3 +1,4 @@
1
+ import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
1
2
  import type { ActivityStore } from '../../store/activity/memory.js'
2
3
  import type { RunId } from '../../types/ids/index.js'
3
4
  import type { InvocationState } from '../../types/invocation/index.js'
@@ -17,6 +18,7 @@ export interface ToolingBootstrapConfig {
17
18
  env: Record<string, string>
18
19
  abortSignal: AbortSignal
19
20
  invocationState?: InvocationState
21
+ pluginManager?: PluginLifecycleManager
20
22
  }
21
23
 
22
24
  export class ToolingBootstrap {
@@ -35,6 +37,7 @@ export class ToolingBootstrap {
35
37
  env: config.env,
36
38
  abortSignal: config.abortSignal,
37
39
  invocationState: config.invocationState,
40
+ pluginManager: config.pluginManager,
38
41
  },
39
42
  activityStore,
40
43
  emitEvent,
@@ -1,7 +1,7 @@
1
1
  export { InMemoryStore } from './InMemoryStore.js'
2
2
  export type { Identifiable, Timestamped } from './InMemoryStore.js'
3
3
 
4
- export { RunDiskStore, SessionStore } from './run/disk.js'
4
+ export { RunDiskStore } from './run/disk.js'
5
5
 
6
6
  export { ActivityStore } from './activity/memory.js'
7
7
  export type { ActivityEvent, ActivityEventListener } from './activity/memory.js'
@@ -12,3 +12,8 @@ export type { DiskTaskStoreConfig } from './task/disk.js'
12
12
 
13
13
  export { InMemoryConversationStore } from './conversation/memory.js'
14
14
  export type { InMemoryConversationStoreConfig } from './conversation/memory.js'
15
+
16
+ export { InMemoryMemoryIndex } from './memory/index.js'
17
+ export { InMemoryMemoryStore } from './memory/memory.js'
18
+ export { DiskMemoryStore } from './memory/disk.js'
19
+ export type { DiskMemoryStoreConfig } from './memory/disk.js'
@@ -217,7 +217,3 @@ async function atomicWriteJson(filePath: string, value: unknown): Promise<void>
217
217
  function isFileNotFound(err: unknown): boolean {
218
218
  return typeof err === 'object' && err !== null && (err as NodeJS.ErrnoException).code === 'ENOENT'
219
219
  }
220
-
221
- export const SessionStore = RunDiskStore
222
-
223
- export type SessionStore = RunDiskStore
@@ -0,0 +1,118 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import type { RunId } from '../../../types/ids/index.js'
6
+ import { generateRunId } from '../../../utils/id.js'
7
+ import { DiskTaskStore } from '../disk.js'
8
+
9
+ describe('DiskTaskStore — concurrency regressions', () => {
10
+ let baseDir: string
11
+ let runId: RunId
12
+ let store: DiskTaskStore
13
+
14
+ beforeEach(() => {
15
+ baseDir = mkdtempSync(join(tmpdir(), 'namzu-task-concurrency-'))
16
+ runId = generateRunId()
17
+ store = new DiskTaskStore({ baseDir, defaultRunId: runId })
18
+ })
19
+
20
+ afterEach(() => {
21
+ rmSync(baseDir, { recursive: true, force: true })
22
+ })
23
+
24
+ it('does not deadlock when two deletes race on mutually-referencing tasks', async () => {
25
+ const a = await store.create({ runId, subject: 'A' })
26
+ const b = await store.create({ runId, subject: 'B' })
27
+
28
+ // Establish bidirectional edge: A blocks B AND B blocks A.
29
+ // (Nonsensical semantically, but the store allows it and the lock logic
30
+ // must not deadlock.)
31
+ await store.block(a.id, b.id)
32
+ await store.block(b.id, a.id)
33
+
34
+ // Race the two deletes. The pre-fix implementation (lock this → iterate
35
+ // related → lock each) could acquire A→B from one call and B→A from the
36
+ // other, deadlocking. withLocks() sorts IDs canonically so both calls
37
+ // acquire [A, B] in the same order.
38
+ const withTimeout = <T>(p: Promise<T>, ms: number): Promise<T> =>
39
+ Promise.race([
40
+ p,
41
+ new Promise<never>((_resolve, reject) =>
42
+ setTimeout(() => reject(new Error('timeout — likely deadlocked')), ms),
43
+ ),
44
+ ])
45
+
46
+ const [resA, resB] = await withTimeout(
47
+ Promise.all([store.delete(a.id), store.delete(b.id)]),
48
+ 2000,
49
+ )
50
+ expect(resA).toBe(true)
51
+ expect(resB).toBe(true)
52
+ expect(await store.get(a.id)).toBeUndefined()
53
+ expect(await store.get(b.id)).toBeUndefined()
54
+ })
55
+
56
+ it('serializes concurrent same-ID updates (withLock race regression)', async () => {
57
+ const task = await store.create({ runId, subject: 'shared' })
58
+
59
+ // update()'s metadata merge does `{ ...task.metadata, ...updates.metadata }`
60
+ // inside withLock. If withLock serializes correctly, every update's key
61
+ // survives into the final metadata (each sees the latest merged state).
62
+ // If withLock had the race bug, two concurrent updates would both read
63
+ // the SAME snapshot, each add their one key, and one update's key would
64
+ // be overwritten when the second committed — yielding < N keys in the
65
+ // final metadata.
66
+ const n = 20
67
+ await Promise.all(
68
+ Array.from({ length: n }, (_v, i) =>
69
+ store.update(task.id, { metadata: { [`k${i}`]: true } }),
70
+ ),
71
+ )
72
+
73
+ const final = await store.get(task.id)
74
+ const keys = Object.keys(final?.metadata ?? {})
75
+ expect(keys).toHaveLength(n)
76
+ for (let i = 0; i < n; i++) {
77
+ expect(final?.metadata?.[`k${i}`]).toBe(true)
78
+ }
79
+ })
80
+
81
+ it('establishes bidirectional edge atomically under create()', async () => {
82
+ const blocker = await store.create({ runId, subject: 'blocker' })
83
+
84
+ // Race: create a child with blockedBy=[blocker] while concurrently
85
+ // deleting the blocker. Either outcome is acceptable (child created
86
+ // and blocker still present in its blocks list, OR blocker gone and
87
+ // child created with dangling reference), but we must NEVER see
88
+ // blocker still present WITHOUT having child in its blocks list.
89
+ const tasks = await Promise.all([
90
+ store.create({ runId, subject: 'child', blockedBy: [blocker.id] }),
91
+ // No delete here — keep the create edge test focused. The point is
92
+ // that after create() resolves, the blocker's blocks list contains
93
+ // the new task ID atomically.
94
+ ])
95
+ const child = tasks[0]
96
+
97
+ const blockerAfter = await store.get(blocker.id)
98
+ expect(blockerAfter).toBeDefined()
99
+ expect(blockerAfter?.blocks).toContain(child.id)
100
+ expect(child.blockedBy).toContain(blocker.id)
101
+ })
102
+
103
+ it('block() skips gracefully when one task disappeared before lock acquired', async () => {
104
+ const a = await store.create({ runId, subject: 'A' })
105
+ const b = await store.create({ runId, subject: 'B' })
106
+
107
+ // Delete B, then try to block a → b. The findTask pre-check passes for a
108
+ // but fails for b, so block() returns early (silent no-op per existing
109
+ // contract).
110
+ await store.delete(b.id)
111
+
112
+ // block() should not throw and A's blocks list should remain empty.
113
+ await store.block(a.id, b.id)
114
+
115
+ const aAfter = await store.get(a.id)
116
+ expect(aAfter?.blocks).not.toContain(b.id)
117
+ })
118
+ })