@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.
- package/CHANGELOG.md +25 -0
- package/README.md +279 -129
- package/dist/bridge/tools/connector/adapter.d.ts +2 -2
- package/dist/bridge/tools/connector/adapter.d.ts.map +1 -1
- package/dist/bridge/tools/connector/adapter.js +3 -1
- package/dist/bridge/tools/connector/adapter.js.map +1 -1
- package/dist/connector/BaseConnector.d.ts +2 -1
- package/dist/connector/BaseConnector.d.ts.map +1 -1
- package/dist/connector/BaseConnector.js.map +1 -1
- package/dist/connector/builtins/http.d.ts +1 -1
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +1 -1
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/webhook.d.ts +1 -1
- package/dist/connector/builtins/webhook.d.ts.map +1 -1
- package/dist/connector/builtins/webhook.js +1 -1
- package/dist/connector/builtins/webhook.js.map +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +7 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/index.d.ts +5 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -23
- package/dist/index.js.map +1 -1
- package/dist/manager/connector/environment.d.ts +4 -4
- package/dist/manager/connector/environment.d.ts.map +1 -1
- package/dist/manager/connector/environment.js.map +1 -1
- package/dist/manager/connector/lifecycle.d.ts +2 -2
- package/dist/manager/connector/lifecycle.d.ts.map +1 -1
- package/dist/manager/connector/lifecycle.js.map +1 -1
- package/dist/manager/connector/tenant.d.ts +3 -3
- package/dist/manager/connector/tenant.d.ts.map +1 -1
- package/dist/manager/connector/tenant.js.map +1 -1
- package/dist/manager/index.d.ts +2 -1
- package/dist/manager/index.d.ts.map +1 -1
- package/dist/manager/index.js +2 -1
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/run/emergency.d.ts.map +1 -1
- package/dist/manager/run/emergency.js +44 -12
- package/dist/manager/run/emergency.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +0 -2
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +0 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/plugin/__tests__/enable-contributions.test.d.ts +2 -0
- package/dist/plugin/__tests__/enable-contributions.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +265 -0
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -0
- package/dist/plugin/__tests__/lifecycle.test.js +30 -0
- package/dist/plugin/__tests__/lifecycle.test.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +4 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +157 -49
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/rag/vector-store.d.ts +2 -2
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/ManagedRegistry.d.ts +6 -0
- package/dist/registry/ManagedRegistry.d.ts.map +1 -1
- package/dist/registry/ManagedRegistry.js +9 -3
- package/dist/registry/ManagedRegistry.js.map +1 -1
- package/dist/registry/agent/definitions.d.ts +2 -7
- package/dist/registry/agent/definitions.d.ts.map +1 -1
- package/dist/registry/agent/definitions.js +3 -41
- package/dist/registry/agent/definitions.js.map +1 -1
- package/dist/registry/connector/definitions.d.ts +2 -7
- package/dist/registry/connector/definitions.d.ts.map +1 -1
- package/dist/registry/connector/definitions.js +3 -41
- package/dist/registry/connector/definitions.js.map +1 -1
- package/dist/registry/connector/scoped.d.ts +5 -4
- package/dist/registry/connector/scoped.d.ts.map +1 -1
- package/dist/registry/connector/scoped.js.map +1 -1
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +2 -4
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +4 -21
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/index.d.ts +4 -4
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +3 -3
- package/dist/run/index.js.map +1 -1
- package/dist/run/reporter.d.ts +0 -2
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +0 -1
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +236 -0
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -0
- package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/plugin-hooks.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/plugin-hooks.test.js +21 -0
- package/dist/runtime/query/__tests__/plugin-hooks.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +6 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +110 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +8 -6
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +13 -16
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +11 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -0
- package/dist/runtime/query/plugin-hooks.js +28 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -0
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/store/index.d.ts +5 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +4 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/disk.d.ts +0 -2
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +0 -1
- package/dist/store/run/disk.js.map +1 -1
- package/dist/store/task/__tests__/disk-concurrency.test.d.ts +2 -0
- package/dist/store/task/__tests__/disk-concurrency.test.d.ts.map +1 -0
- package/dist/store/task/__tests__/disk-concurrency.test.js +91 -0
- package/dist/store/task/__tests__/disk-concurrency.test.js.map +1 -0
- package/dist/store/task/disk.d.ts +6 -0
- package/dist/store/task/disk.d.ts.map +1 -1
- package/dist/store/task/disk.js +150 -36
- package/dist/store/task/disk.js.map +1 -1
- package/dist/types/connector/core.d.ts +2 -2
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +7 -7
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +4 -4
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/connector/scope.d.ts +3 -2
- package/dist/types/connector/scope.d.ts.map +1 -1
- package/dist/types/connector/scope.js.map +1 -1
- package/dist/types/connector/tenant.d.ts +4 -4
- package/dist/types/connector/tenant.d.ts.map +1 -1
- package/dist/types/plugin/index.d.ts +4 -21
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -10
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/rag/knowledge-base.d.ts +2 -2
- package/dist/types/rag/knowledge-base.d.ts.map +1 -1
- package/dist/types/rag/scope.d.ts +2 -1
- package/dist/types/rag/scope.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +3 -3
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +3 -3
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +0 -6
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/vault/InMemoryCredentialVault.d.ts +3 -3
- package/dist/vault/InMemoryCredentialVault.d.ts.map +1 -1
- package/dist/vault/InMemoryCredentialVault.js.map +1 -1
- package/package.json +1 -1
- package/src/bridge/tools/connector/adapter.ts +5 -3
- package/src/connector/BaseConnector.ts +2 -1
- package/src/connector/builtins/http.ts +1 -1
- package/src/connector/builtins/webhook.ts +1 -1
- package/src/contracts/api.ts +3 -0
- package/src/contracts/index.ts +10 -0
- package/src/index.ts +46 -41
- package/src/manager/connector/environment.ts +5 -5
- package/src/manager/connector/lifecycle.ts +2 -2
- package/src/manager/connector/tenant.ts +8 -3
- package/src/manager/index.ts +2 -1
- package/src/manager/run/emergency.ts +45 -16
- package/src/manager/run/persistence.ts +0 -4
- package/src/plugin/__tests__/enable-contributions.test.ts +295 -0
- package/src/plugin/__tests__/lifecycle.test.ts +44 -0
- package/src/plugin/lifecycle.ts +186 -57
- package/src/rag/vector-store.ts +2 -2
- package/src/registry/ManagedRegistry.ts +14 -3
- package/src/registry/agent/definitions.ts +3 -53
- package/src/registry/connector/definitions.ts +3 -53
- package/src/registry/connector/scoped.ts +7 -6
- package/src/registry/index.ts +1 -0
- package/src/registry/tool/execute.ts +4 -22
- package/src/run/index.ts +4 -4
- package/src/run/reporter.ts +0 -4
- package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +305 -0
- package/src/runtime/query/__tests__/plugin-hooks.test.ts +28 -0
- package/src/runtime/query/executor.ts +148 -7
- package/src/runtime/query/index.ts +16 -6
- package/src/runtime/query/iteration/index.ts +29 -16
- package/src/runtime/query/plugin-hooks.ts +34 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/store/index.ts +6 -1
- package/src/store/run/disk.ts +0 -4
- package/src/store/task/__tests__/disk-concurrency.test.ts +118 -0
- package/src/store/task/disk.ts +150 -37
- package/src/types/connector/core.ts +2 -2
- package/src/types/connector/definition.ts +7 -7
- package/src/types/connector/mcp.ts +4 -4
- package/src/types/connector/scope.ts +3 -2
- package/src/types/connector/tenant.ts +4 -4
- package/src/types/plugin/index.ts +0 -14
- package/src/types/rag/knowledge-base.ts +2 -2
- package/src/types/rag/scope.ts +3 -1
- package/src/types/rag/storage.ts +3 -3
- package/src/types/rag/vector.ts +3 -3
- package/src/types/run/config.ts +0 -9
- package/src/vault/InMemoryCredentialVault.ts +3 -3
- package/dist/manager/agent/index.d.ts +0 -2
- package/dist/manager/agent/index.d.ts.map +0 -1
- package/dist/manager/agent/index.js +0 -2
- package/dist/manager/agent/index.js.map +0 -1
- package/dist/registry/agent/index.d.ts +0 -2
- package/dist/registry/agent/index.d.ts.map +0 -1
- package/dist/registry/agent/index.js +0 -2
- package/dist/registry/agent/index.js.map +0 -1
- package/dist/router/index.d.ts +0 -2
- package/dist/router/index.d.ts.map +0 -1
- package/dist/router/index.js +0 -2
- package/dist/router/index.js.map +0 -1
- package/src/manager/agent/index.ts +0 -1
- package/src/registry/agent/index.ts +0 -1
- package/src/router/index.ts +0 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { PluginRegistry } from '../../registry/plugin/index.js'
|
|
3
|
+
import type { PluginId } from '../../types/ids/index.js'
|
|
4
|
+
import type { PluginDefinition } from '../../types/plugin/index.js'
|
|
5
|
+
import type { ToolDefinition, ToolRegistryContract } from '../../types/tool/index.js'
|
|
6
|
+
import type { Logger } from '../../utils/logger.js'
|
|
7
|
+
import { PluginLifecycleManager } from '../lifecycle.js'
|
|
8
|
+
|
|
9
|
+
const mockConnect = vi.fn(async (): Promise<unknown> => undefined)
|
|
10
|
+
const mockDisconnect = vi.fn(async (): Promise<void> => undefined)
|
|
11
|
+
const mockListTools = vi.fn(async (): Promise<unknown[]> => [])
|
|
12
|
+
|
|
13
|
+
vi.mock('../../connector/mcp/client.js', () => ({
|
|
14
|
+
MCPClient: vi.fn().mockImplementation(() => ({
|
|
15
|
+
id: 'mcp-client-mock',
|
|
16
|
+
connect: mockConnect,
|
|
17
|
+
disconnect: mockDisconnect,
|
|
18
|
+
listTools: mockListTools,
|
|
19
|
+
})),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('../../connector/mcp/adapter.js', () => ({
|
|
23
|
+
mcpToolToToolDefinition: vi.fn((mcpTool: { name: string }) => ({
|
|
24
|
+
name: mcpTool.name,
|
|
25
|
+
description: `mcp tool ${mcpTool.name}`,
|
|
26
|
+
inputSchema: { parse: vi.fn() } as any,
|
|
27
|
+
async execute() {
|
|
28
|
+
return { success: true, output: 'ok' }
|
|
29
|
+
},
|
|
30
|
+
})),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
function makeLogger(): Logger {
|
|
34
|
+
const s = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
35
|
+
return { ...s, child: vi.fn(() => ({ ...s, child: vi.fn() })) } as unknown as Logger
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makePluginRegistry(base: Partial<PluginDefinition>): {
|
|
39
|
+
registry: PluginRegistry
|
|
40
|
+
state: { current: PluginDefinition }
|
|
41
|
+
} {
|
|
42
|
+
const state = {
|
|
43
|
+
current: {
|
|
44
|
+
id: 'plugin_test' as PluginId,
|
|
45
|
+
manifest: base.manifest ?? {
|
|
46
|
+
name: 'test-plugin',
|
|
47
|
+
version: '0.0.1',
|
|
48
|
+
description: 'test',
|
|
49
|
+
},
|
|
50
|
+
scope: 'project',
|
|
51
|
+
status: 'installed',
|
|
52
|
+
rootDir: '/tmp/plugin',
|
|
53
|
+
installedAt: 0,
|
|
54
|
+
...base,
|
|
55
|
+
} as PluginDefinition,
|
|
56
|
+
}
|
|
57
|
+
const registry = {
|
|
58
|
+
register: vi.fn((def: PluginDefinition) => {
|
|
59
|
+
state.current = def
|
|
60
|
+
}),
|
|
61
|
+
unregister: vi.fn(),
|
|
62
|
+
getOrThrow: vi.fn(() => state.current),
|
|
63
|
+
findByName: vi.fn(),
|
|
64
|
+
getAll: vi.fn(() => [state.current]),
|
|
65
|
+
} as unknown as PluginRegistry
|
|
66
|
+
return { registry, state }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeToolRegistry(): ToolRegistryContract {
|
|
70
|
+
const names: string[] = []
|
|
71
|
+
return {
|
|
72
|
+
register: vi.fn((tool: ToolDefinition) => {
|
|
73
|
+
names.push(tool.name)
|
|
74
|
+
}),
|
|
75
|
+
unregister: vi.fn((name: string) => {
|
|
76
|
+
const i = names.indexOf(name)
|
|
77
|
+
if (i >= 0) names.splice(i, 1)
|
|
78
|
+
}),
|
|
79
|
+
listNames: vi.fn(() => [...names]),
|
|
80
|
+
has: vi.fn((name: string) => names.includes(name)),
|
|
81
|
+
get: vi.fn(),
|
|
82
|
+
execute: vi.fn(),
|
|
83
|
+
getAvailability: vi.fn(),
|
|
84
|
+
} as unknown as ToolRegistryContract
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('PluginLifecycleManager enable() contribution types', () => {
|
|
88
|
+
const pluginId = 'plugin_test' as PluginId
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
mockConnect.mockReset()
|
|
92
|
+
mockDisconnect.mockReset()
|
|
93
|
+
mockListTools.mockReset()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('unsupported contribution types', () => {
|
|
97
|
+
it('throws when manifest declares skills', async () => {
|
|
98
|
+
const { registry } = makePluginRegistry({
|
|
99
|
+
manifest: {
|
|
100
|
+
name: 'p',
|
|
101
|
+
version: '0.0.1',
|
|
102
|
+
description: 't',
|
|
103
|
+
skills: ['./s'],
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
const mgr = new PluginLifecycleManager({
|
|
107
|
+
pluginRegistry: registry,
|
|
108
|
+
toolRegistry: makeToolRegistry(),
|
|
109
|
+
log: makeLogger(),
|
|
110
|
+
})
|
|
111
|
+
await expect(mgr.enable(pluginId)).rejects.toThrow(/\[skills\]/)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('throws when manifest declares connectors', async () => {
|
|
115
|
+
const { registry } = makePluginRegistry({
|
|
116
|
+
manifest: {
|
|
117
|
+
name: 'p',
|
|
118
|
+
version: '0.0.1',
|
|
119
|
+
description: 't',
|
|
120
|
+
connectors: ['./c'],
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
const mgr = new PluginLifecycleManager({
|
|
124
|
+
pluginRegistry: registry,
|
|
125
|
+
toolRegistry: makeToolRegistry(),
|
|
126
|
+
log: makeLogger(),
|
|
127
|
+
})
|
|
128
|
+
await expect(mgr.enable(pluginId)).rejects.toThrow(/\[connectors\]/)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('throws when manifest declares personas', async () => {
|
|
132
|
+
const { registry } = makePluginRegistry({
|
|
133
|
+
manifest: {
|
|
134
|
+
name: 'p',
|
|
135
|
+
version: '0.0.1',
|
|
136
|
+
description: 't',
|
|
137
|
+
personas: ['./pp'],
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
const mgr = new PluginLifecycleManager({
|
|
141
|
+
pluginRegistry: registry,
|
|
142
|
+
toolRegistry: makeToolRegistry(),
|
|
143
|
+
log: makeLogger(),
|
|
144
|
+
})
|
|
145
|
+
await expect(mgr.enable(pluginId)).rejects.toThrow(/\[personas\]/)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('lists all unsupported types together when multiple declared', async () => {
|
|
149
|
+
const { registry } = makePluginRegistry({
|
|
150
|
+
manifest: {
|
|
151
|
+
name: 'p',
|
|
152
|
+
version: '0.0.1',
|
|
153
|
+
description: 't',
|
|
154
|
+
skills: ['./s'],
|
|
155
|
+
connectors: ['./c'],
|
|
156
|
+
personas: ['./pp'],
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
const mgr = new PluginLifecycleManager({
|
|
160
|
+
pluginRegistry: registry,
|
|
161
|
+
toolRegistry: makeToolRegistry(),
|
|
162
|
+
log: makeLogger(),
|
|
163
|
+
})
|
|
164
|
+
await expect(mgr.enable(pluginId)).rejects.toThrow(/skills, connectors, personas/)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('mcpServers wiring', () => {
|
|
169
|
+
it('registers namespaced tools for each MCP server tool', async () => {
|
|
170
|
+
mockConnect.mockResolvedValue(undefined)
|
|
171
|
+
mockListTools.mockResolvedValue([
|
|
172
|
+
{ name: 'read_file', inputSchema: { type: 'object' } },
|
|
173
|
+
{ name: 'write_file', inputSchema: { type: 'object' } },
|
|
174
|
+
])
|
|
175
|
+
const { registry } = makePluginRegistry({
|
|
176
|
+
manifest: {
|
|
177
|
+
name: 'fs-plugin',
|
|
178
|
+
version: '0.0.1',
|
|
179
|
+
description: 't',
|
|
180
|
+
mcpServers: [{ name: 'fs', command: '/bin/true' }],
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
const toolRegistry = makeToolRegistry()
|
|
184
|
+
const mgr = new PluginLifecycleManager({
|
|
185
|
+
pluginRegistry: registry,
|
|
186
|
+
toolRegistry,
|
|
187
|
+
log: makeLogger(),
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
await mgr.enable(pluginId)
|
|
191
|
+
|
|
192
|
+
expect(mockConnect).toHaveBeenCalledOnce()
|
|
193
|
+
expect(mockListTools).toHaveBeenCalledOnce()
|
|
194
|
+
expect(toolRegistry.listNames()).toEqual([
|
|
195
|
+
'fs-plugin:mcp__fs__read_file',
|
|
196
|
+
'fs-plugin:mcp__fs__write_file',
|
|
197
|
+
])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('disconnects MCP clients and unregisters tools on disable', async () => {
|
|
201
|
+
mockConnect.mockResolvedValue(undefined)
|
|
202
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
203
|
+
mockListTools.mockResolvedValue([{ name: 'ping', inputSchema: { type: 'object' } }])
|
|
204
|
+
const { registry } = makePluginRegistry({
|
|
205
|
+
manifest: {
|
|
206
|
+
name: 'net',
|
|
207
|
+
version: '0.0.1',
|
|
208
|
+
description: 't',
|
|
209
|
+
mcpServers: [{ name: 'srv', command: '/bin/true' }],
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
const toolRegistry = makeToolRegistry()
|
|
213
|
+
const mgr = new PluginLifecycleManager({
|
|
214
|
+
pluginRegistry: registry,
|
|
215
|
+
toolRegistry,
|
|
216
|
+
log: makeLogger(),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await mgr.enable(pluginId)
|
|
220
|
+
expect(toolRegistry.listNames()).toContain('net:mcp__srv__ping')
|
|
221
|
+
|
|
222
|
+
await mgr.disable(pluginId)
|
|
223
|
+
expect(mockDisconnect).toHaveBeenCalledOnce()
|
|
224
|
+
expect(toolRegistry.listNames()).toEqual([])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('disconnects MCP clients before unregistering tools on disable', async () => {
|
|
228
|
+
mockConnect.mockResolvedValue(undefined)
|
|
229
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
230
|
+
mockListTools.mockResolvedValue([{ name: 'ping', inputSchema: { type: 'object' } }])
|
|
231
|
+
const { registry } = makePluginRegistry({
|
|
232
|
+
manifest: {
|
|
233
|
+
name: 'net',
|
|
234
|
+
version: '0.0.1',
|
|
235
|
+
description: 't',
|
|
236
|
+
mcpServers: [{ name: 'srv', command: '/bin/true' }],
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
const events: string[] = []
|
|
240
|
+
const toolRegistry = {
|
|
241
|
+
register: vi.fn(),
|
|
242
|
+
unregister: vi.fn(() => events.push('unregister')),
|
|
243
|
+
listNames: vi.fn(() => []),
|
|
244
|
+
has: vi.fn(),
|
|
245
|
+
get: vi.fn(),
|
|
246
|
+
execute: vi.fn(),
|
|
247
|
+
getAvailability: vi.fn(),
|
|
248
|
+
} as unknown as ToolRegistryContract
|
|
249
|
+
mockDisconnect.mockImplementation(async () => {
|
|
250
|
+
events.push('disconnect')
|
|
251
|
+
})
|
|
252
|
+
const mgr = new PluginLifecycleManager({
|
|
253
|
+
pluginRegistry: registry,
|
|
254
|
+
toolRegistry,
|
|
255
|
+
log: makeLogger(),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
await mgr.enable(pluginId)
|
|
259
|
+
await mgr.disable(pluginId)
|
|
260
|
+
|
|
261
|
+
expect(events).toEqual(['disconnect', 'unregister'])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('rolls back tools and MCP clients when connect fails mid-enable', async () => {
|
|
265
|
+
mockConnect
|
|
266
|
+
.mockResolvedValueOnce(undefined) // first server connects
|
|
267
|
+
.mockRejectedValueOnce(new Error('connect refused')) // second fails
|
|
268
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
269
|
+
mockListTools.mockResolvedValue([{ name: 't', inputSchema: { type: 'object' } }])
|
|
270
|
+
const { registry } = makePluginRegistry({
|
|
271
|
+
manifest: {
|
|
272
|
+
name: 'multi',
|
|
273
|
+
version: '0.0.1',
|
|
274
|
+
description: 't',
|
|
275
|
+
mcpServers: [
|
|
276
|
+
{ name: 'a', command: '/bin/true' },
|
|
277
|
+
{ name: 'b', command: '/bin/false' },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
const toolRegistry = makeToolRegistry()
|
|
282
|
+
const mgr = new PluginLifecycleManager({
|
|
283
|
+
pluginRegistry: registry,
|
|
284
|
+
toolRegistry,
|
|
285
|
+
log: makeLogger(),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
await expect(mgr.enable(pluginId)).rejects.toThrow(/connect refused/)
|
|
289
|
+
|
|
290
|
+
// Rollback: first server's tools unregistered, first client disconnected.
|
|
291
|
+
expect(toolRegistry.listNames()).toEqual([])
|
|
292
|
+
expect(mockDisconnect).toHaveBeenCalledOnce()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
@@ -426,5 +426,49 @@ describe('PluginLifecycleManager', () => {
|
|
|
426
426
|
await expect(executePromise).resolves.not.toThrow()
|
|
427
427
|
})
|
|
428
428
|
})
|
|
429
|
+
|
|
430
|
+
describe('RunEvent emission', () => {
|
|
431
|
+
it('should emit plugin_hook_executing and plugin_hook_completed when emitRunEvent provided', async () => {
|
|
432
|
+
const emitted: any[] = []
|
|
433
|
+
const emitRunEvent = vi.fn(async (event: any) => {
|
|
434
|
+
emitted.push(event)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const handler = vi.fn(
|
|
438
|
+
async (): Promise<PluginHookResult> => ({ action: 'modify', input: { x: 1 } }),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
manager['hookHandlers'].set('pre_tool_use', [{ pluginId: mockPluginId, handler }])
|
|
442
|
+
|
|
443
|
+
await manager.executeHooks(
|
|
444
|
+
'pre_tool_use',
|
|
445
|
+
{ runId: mockRunId, toolName: 't', toolInput: {} },
|
|
446
|
+
emitRunEvent,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
expect(emitted).toHaveLength(2)
|
|
450
|
+
expect(emitted[0]).toMatchObject({
|
|
451
|
+
type: 'plugin_hook_executing',
|
|
452
|
+
runId: mockRunId,
|
|
453
|
+
pluginId: mockPluginId,
|
|
454
|
+
hookEvent: 'pre_tool_use',
|
|
455
|
+
})
|
|
456
|
+
expect(emitted[1]).toMatchObject({
|
|
457
|
+
type: 'plugin_hook_completed',
|
|
458
|
+
runId: mockRunId,
|
|
459
|
+
pluginId: mockPluginId,
|
|
460
|
+
hookEvent: 'pre_tool_use',
|
|
461
|
+
})
|
|
462
|
+
expect(emitted[1].result).toEqual({ action: 'modify', input: { x: 1 } })
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should not emit RunEvents when emitRunEvent omitted', async () => {
|
|
466
|
+
const handler = vi.fn(async (): Promise<PluginHookResult> => ({ action: 'continue' }))
|
|
467
|
+
manager['hookHandlers'].set('run_start', [{ pluginId: mockPluginId, handler }])
|
|
468
|
+
await expect(manager.executeHooks('run_start', { runId: mockRunId })).resolves.toHaveLength(
|
|
469
|
+
1,
|
|
470
|
+
)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
429
473
|
})
|
|
430
474
|
})
|
package/src/plugin/lifecycle.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { mcpToolToToolDefinition } from '../connector/mcp/adapter.js'
|
|
4
|
+
import { MCPClient } from '../connector/mcp/client.js'
|
|
3
5
|
import { HOOK_TIMEOUT_MS, PLUGIN_NAMESPACE_SEPARATOR } from '../constants/plugin/index.js'
|
|
4
6
|
import type { PluginRegistry } from '../registry/plugin/index.js'
|
|
5
7
|
import type { PluginId } from '../types/ids/index.js'
|
|
@@ -11,14 +13,21 @@ import type {
|
|
|
11
13
|
PluginHookEvent,
|
|
12
14
|
PluginHookResult,
|
|
13
15
|
PluginLifecycleEvent,
|
|
16
|
+
PluginMCPServerConfig,
|
|
14
17
|
PluginScope,
|
|
15
18
|
} from '../types/plugin/index.js'
|
|
19
|
+
import type { RunEvent } from '../types/run/index.js'
|
|
16
20
|
import type { ToolDefinition, ToolRegistryContract } from '../types/tool/index.js'
|
|
17
21
|
import { toErrorMessage } from '../utils/error.js'
|
|
18
22
|
import { generatePluginId } from '../utils/id.js'
|
|
19
23
|
import type { Logger } from '../utils/logger.js'
|
|
20
24
|
import { loadPluginManifest } from './loader.js'
|
|
21
25
|
|
|
26
|
+
interface PluginContributionRecord {
|
|
27
|
+
toolNames: string[]
|
|
28
|
+
mcpClients: MCPClient[]
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export interface PluginLifecycleManagerConfig {
|
|
23
32
|
pluginRegistry: PluginRegistry
|
|
24
33
|
toolRegistry: ToolRegistryContract
|
|
@@ -34,7 +43,7 @@ export class PluginLifecycleManager {
|
|
|
34
43
|
PluginHookEvent,
|
|
35
44
|
Array<{ pluginId: PluginId; handler: PluginHookDefinition['handler'] }>
|
|
36
45
|
> = new Map()
|
|
37
|
-
private pluginContributions: Map<PluginId,
|
|
46
|
+
private pluginContributions: Map<PluginId, PluginContributionRecord> = new Map()
|
|
38
47
|
private hookTimeoutMs: number
|
|
39
48
|
private log: Logger
|
|
40
49
|
|
|
@@ -99,70 +108,80 @@ export class PluginLifecycleManager {
|
|
|
99
108
|
)
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
const registeredToolNames: string[] = []
|
|
103
111
|
const { manifest } = plugin
|
|
104
112
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
// Unsupported contribution types must fail fast (Convention #0, #5).
|
|
114
|
+
// SDK lacks removable registries / instance factories / persona loader design
|
|
115
|
+
// for these categories. Remove from manifest or upgrade runtime.
|
|
116
|
+
const unsupported: string[] = []
|
|
117
|
+
if (manifest.skills?.length) unsupported.push('skills')
|
|
118
|
+
if (manifest.connectors?.length) unsupported.push('connectors')
|
|
119
|
+
if (manifest.personas?.length) unsupported.push('personas')
|
|
120
|
+
if (unsupported.length > 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Plugin "${manifest.name}": contribution type(s) [${unsupported.join(', ')}] not yet supported by the runtime. Remove from manifest or upgrade @namzu/sdk.`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
const contributions: PluginContributionRecord = { toolNames: [], mcpClients: [] }
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Load tools
|
|
130
|
+
if (manifest.tools && manifest.tools.length > 0) {
|
|
131
|
+
for (const toolPath of manifest.tools) {
|
|
132
|
+
const absolutePath = join(plugin.rootDir, toolPath)
|
|
133
|
+
const fileUrl = pathToFileURL(absolutePath).href
|
|
134
|
+
const mod = (await import(fileUrl)) as { tools?: ToolDefinition[] }
|
|
135
|
+
|
|
136
|
+
if (!mod.tools || !Array.isArray(mod.tools)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Plugin "${manifest.name}": tool module "${toolPath}" must export a "tools" array`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const tool of mod.tools) {
|
|
143
|
+
const namespacedName = manifest.name + PLUGIN_NAMESPACE_SEPARATOR + tool.name
|
|
144
|
+
const namespacedTool: ToolDefinition = { ...tool, name: namespacedName }
|
|
145
|
+
this.toolRegistry.register(namespacedTool, 'deferred')
|
|
146
|
+
contributions.toolNames.push(namespacedName)
|
|
147
|
+
}
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
|
-
}
|
|
126
150
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
// Load hooks
|
|
152
|
+
if (manifest.hooks && manifest.hooks.length > 0) {
|
|
153
|
+
for (const hookPath of manifest.hooks) {
|
|
154
|
+
const absolutePath = join(plugin.rootDir, hookPath)
|
|
155
|
+
const fileUrl = pathToFileURL(absolutePath).href
|
|
156
|
+
const mod = (await import(fileUrl)) as { hooks?: PluginHookDefinition[] }
|
|
157
|
+
|
|
158
|
+
if (!mod.hooks || !Array.isArray(mod.hooks)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Plugin "${manifest.name}": hook module "${hookPath}" must export a "hooks" array`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const hook of mod.hooks) {
|
|
165
|
+
const handlers = this.hookHandlers.get(hook.event) ?? []
|
|
166
|
+
handlers.push({ pluginId, handler: hook.handler })
|
|
167
|
+
this.hookHandlers.set(hook.event, handlers)
|
|
168
|
+
}
|
|
138
169
|
}
|
|
170
|
+
}
|
|
139
171
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
172
|
+
// Start MCP servers and adapt their tools
|
|
173
|
+
if (manifest.mcpServers && manifest.mcpServers.length > 0) {
|
|
174
|
+
for (const serverConfig of manifest.mcpServers) {
|
|
175
|
+
await this.attachMCPServer(manifest.name, serverConfig, contributions)
|
|
144
176
|
}
|
|
145
177
|
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
await this.rollbackContributions(pluginId, contributions)
|
|
180
|
+
throw err
|
|
146
181
|
}
|
|
147
182
|
|
|
148
|
-
|
|
149
|
-
if (manifest.skills && manifest.skills.length > 0) {
|
|
150
|
-
this.log.info(`Plugin "${manifest.name}": skills contribution not yet supported`)
|
|
151
|
-
}
|
|
152
|
-
if (manifest.mcpServers && manifest.mcpServers.length > 0) {
|
|
153
|
-
this.log.info(`Plugin "${manifest.name}": MCP servers contribution not yet supported`)
|
|
154
|
-
}
|
|
155
|
-
if (manifest.connectors && manifest.connectors.length > 0) {
|
|
156
|
-
this.log.info(`Plugin "${manifest.name}": connectors contribution not yet supported`)
|
|
157
|
-
}
|
|
158
|
-
if (manifest.personas && manifest.personas.length > 0) {
|
|
159
|
-
this.log.info(`Plugin "${manifest.name}": personas contribution not yet supported`)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Track contributions for cleanup
|
|
163
|
-
this.pluginContributions.set(pluginId, registeredToolNames)
|
|
183
|
+
this.pluginContributions.set(pluginId, contributions)
|
|
164
184
|
|
|
165
|
-
// Update status to enabled
|
|
166
185
|
const enabled: PluginDefinition = {
|
|
167
186
|
...plugin,
|
|
168
187
|
status: 'enabled',
|
|
@@ -178,10 +197,76 @@ export class PluginLifecycleManager {
|
|
|
178
197
|
|
|
179
198
|
this.log.info(`Plugin enabled: ${manifest.name}`, {
|
|
180
199
|
pluginId,
|
|
181
|
-
toolCount:
|
|
200
|
+
toolCount: contributions.toolNames.length,
|
|
201
|
+
mcpServerCount: contributions.mcpClients.length,
|
|
182
202
|
})
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
private async attachMCPServer(
|
|
206
|
+
pluginName: string,
|
|
207
|
+
config: PluginMCPServerConfig,
|
|
208
|
+
contributions: PluginContributionRecord,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const client = new MCPClient({
|
|
211
|
+
serverName: config.name,
|
|
212
|
+
transport: {
|
|
213
|
+
type: 'stdio',
|
|
214
|
+
command: config.command,
|
|
215
|
+
args: config.args ? [...config.args] : undefined,
|
|
216
|
+
env: config.env ? { ...config.env } : undefined,
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await client.connect()
|
|
221
|
+
contributions.mcpClients.push(client)
|
|
222
|
+
|
|
223
|
+
const mcpTools = await client.listTools()
|
|
224
|
+
for (const mcpTool of mcpTools) {
|
|
225
|
+
const baseDef = mcpToolToToolDefinition(mcpTool, client, config.name)
|
|
226
|
+
// Double-underscore between serverName and toolName so that e.g.
|
|
227
|
+
// (server="fs", tool="read_file") and (server="fs_read", tool="file")
|
|
228
|
+
// produce distinct final names.
|
|
229
|
+
const namespacedName = `${pluginName}${PLUGIN_NAMESPACE_SEPARATOR}mcp__${config.name}__${mcpTool.name}`
|
|
230
|
+
const namespacedTool: ToolDefinition = { ...baseDef, name: namespacedName }
|
|
231
|
+
this.toolRegistry.register(namespacedTool, 'deferred')
|
|
232
|
+
contributions.toolNames.push(namespacedName)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async rollbackContributions(
|
|
237
|
+
pluginId: PluginId,
|
|
238
|
+
contributions: PluginContributionRecord,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
for (const name of contributions.toolNames) {
|
|
241
|
+
try {
|
|
242
|
+
this.toolRegistry.unregister(name)
|
|
243
|
+
} catch (unregErr) {
|
|
244
|
+
this.log.warn('Rollback: tool unregister failed', {
|
|
245
|
+
tool: name,
|
|
246
|
+
error: toErrorMessage(unregErr),
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const client of contributions.mcpClients) {
|
|
251
|
+
try {
|
|
252
|
+
await client.disconnect()
|
|
253
|
+
} catch (discErr) {
|
|
254
|
+
this.log.warn('Rollback: MCP disconnect failed', {
|
|
255
|
+
clientId: client.id,
|
|
256
|
+
error: toErrorMessage(discErr),
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [event, handlers] of this.hookHandlers) {
|
|
261
|
+
const filtered = handlers.filter((h) => h.pluginId !== pluginId)
|
|
262
|
+
if (filtered.length === 0) {
|
|
263
|
+
this.hookHandlers.delete(event)
|
|
264
|
+
} else {
|
|
265
|
+
this.hookHandlers.set(event, filtered)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
185
270
|
async disable(pluginId: PluginId): Promise<void> {
|
|
186
271
|
const plugin = this.pluginRegistry.getOrThrow(pluginId)
|
|
187
272
|
|
|
@@ -191,9 +276,25 @@ export class PluginLifecycleManager {
|
|
|
191
276
|
)
|
|
192
277
|
}
|
|
193
278
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
279
|
+
const contributions = this.pluginContributions.get(pluginId) ?? {
|
|
280
|
+
toolNames: [],
|
|
281
|
+
mcpClients: [],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Disconnect MCP clients first so no new tool calls can reach them mid-teardown.
|
|
285
|
+
for (const client of contributions.mcpClients) {
|
|
286
|
+
try {
|
|
287
|
+
await client.disconnect()
|
|
288
|
+
} catch (err) {
|
|
289
|
+
this.log.warn('MCP client disconnect failed during disable', {
|
|
290
|
+
clientId: client.id,
|
|
291
|
+
error: toErrorMessage(err),
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Unregister contributed tools (plugin tools + MCP-adapted tools)
|
|
297
|
+
for (const name of contributions.toolNames) {
|
|
197
298
|
this.toolRegistry.unregister(name)
|
|
198
299
|
}
|
|
199
300
|
|
|
@@ -207,7 +308,6 @@ export class PluginLifecycleManager {
|
|
|
207
308
|
}
|
|
208
309
|
}
|
|
209
310
|
|
|
210
|
-
// Clear contributions tracking
|
|
211
311
|
this.pluginContributions.delete(pluginId)
|
|
212
312
|
|
|
213
313
|
// Update status to disabled
|
|
@@ -248,6 +348,7 @@ export class PluginLifecycleManager {
|
|
|
248
348
|
async executeHooks(
|
|
249
349
|
event: PluginHookEvent,
|
|
250
350
|
context: Omit<PluginHookContext, 'pluginId' | 'event'>,
|
|
351
|
+
emitRunEvent?: (event: RunEvent) => Promise<void>,
|
|
251
352
|
): Promise<PluginHookResult[]> {
|
|
252
353
|
const handlers = this.hookHandlers.get(event)
|
|
253
354
|
if (!handlers || handlers.length === 0) {
|
|
@@ -271,16 +372,30 @@ export class PluginLifecycleManager {
|
|
|
271
372
|
}
|
|
272
373
|
}
|
|
273
374
|
|
|
375
|
+
// Track input overlay so chained `modify` actions compose: each subsequent
|
|
376
|
+
// hook sees the input produced by the previous hook's modify.
|
|
377
|
+
let toolInputOverlay = context.toolInput
|
|
378
|
+
|
|
274
379
|
for (const idx of indicesToProcess) {
|
|
275
380
|
const hookEntry = handlers[idx]
|
|
276
381
|
if (!hookEntry) continue
|
|
277
382
|
const { pluginId, handler: handlerFn } = hookEntry
|
|
278
383
|
const hookContext: PluginHookContext = {
|
|
279
384
|
...context,
|
|
385
|
+
toolInput: toolInputOverlay,
|
|
280
386
|
pluginId,
|
|
281
387
|
event,
|
|
282
388
|
}
|
|
283
389
|
|
|
390
|
+
if (emitRunEvent) {
|
|
391
|
+
await emitRunEvent({
|
|
392
|
+
type: 'plugin_hook_executing',
|
|
393
|
+
runId: context.runId,
|
|
394
|
+
pluginId,
|
|
395
|
+
hookEvent: event,
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
284
399
|
const start = performance.now()
|
|
285
400
|
let result: PluginHookResult
|
|
286
401
|
|
|
@@ -305,8 +420,22 @@ export class PluginLifecycleManager {
|
|
|
305
420
|
durationMs,
|
|
306
421
|
})
|
|
307
422
|
|
|
423
|
+
if (emitRunEvent) {
|
|
424
|
+
await emitRunEvent({
|
|
425
|
+
type: 'plugin_hook_completed',
|
|
426
|
+
runId: context.runId,
|
|
427
|
+
pluginId,
|
|
428
|
+
hookEvent: event,
|
|
429
|
+
result,
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
308
433
|
results.push(result)
|
|
309
434
|
|
|
435
|
+
if (result.action === 'modify') {
|
|
436
|
+
toolInputOverlay = result.input
|
|
437
|
+
}
|
|
438
|
+
|
|
310
439
|
// Handle flow control: check priority order: error > skip > retry > resume > modify > continue
|
|
311
440
|
// Short-circuit on error or skip; return immediately on resume or retry
|
|
312
441
|
if (result.action === 'error' || result.action === 'skip') {
|