@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -1,14 +1,14 @@
1
1
  import { describe, test, beforeEach } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import { resolveModelConfig } from './ai-agent-model-config.js'
4
- import { resetPikkuState, pikkuState } from '../../pikku-state.js'
4
+ import { resetPikkuState } from '../../pikku-state.js'
5
5
 
6
6
  beforeEach(() => {
7
7
  resetPikkuState()
8
8
  })
9
9
 
10
10
  describe('resolveModelConfig', () => {
11
- test('should return agent model when no config', () => {
11
+ test('passes through the provider-qualified agent model', () => {
12
12
  const result = resolveModelConfig('testAgent', {
13
13
  model: 'anthropic/claude-3',
14
14
  temperature: 0.7,
@@ -17,99 +17,21 @@ describe('resolveModelConfig', () => {
17
17
  assert.strictEqual(result.temperature, 0.7)
18
18
  })
19
19
 
20
- test('should resolve model alias to string', () => {
21
- pikkuState(null, 'models', 'config', {
22
- models: { fast: 'anthropic/claude-haiku' },
23
- } as any)
24
-
25
- const result = resolveModelConfig('testAgent', { model: 'fast' })
26
- assert.strictEqual(result.model, 'anthropic/claude-haiku')
27
- })
28
-
29
- test('should resolve model alias with temperature and maxSteps', () => {
30
- pikkuState(null, 'models', 'config', {
31
- models: {
32
- smart: {
33
- model: 'anthropic/claude-opus',
34
- temperature: 0.3,
35
- maxSteps: 10,
36
- },
37
- },
38
- } as any)
39
-
40
- const result = resolveModelConfig('testAgent', { model: 'smart' })
41
- assert.strictEqual(result.model, 'anthropic/claude-opus')
20
+ test('passes through temperature and maxSteps from the agent', () => {
21
+ const result = resolveModelConfig('testAgent', {
22
+ model: 'openai/gpt-4',
23
+ temperature: 0.3,
24
+ maxSteps: 10,
25
+ })
26
+ assert.strictEqual(result.model, 'openai/gpt-4')
42
27
  assert.strictEqual(result.temperature, 0.3)
43
28
  assert.strictEqual(result.maxSteps, 10)
44
29
  })
45
30
 
46
- test('should throw for unknown model alias', () => {
47
- pikkuState(null, 'models', 'config', {
48
- models: {},
49
- } as any)
50
-
51
- assert.throws(() => resolveModelConfig('testAgent', { model: 'unknown' }), {
52
- message: "Unknown model alias 'unknown'.",
53
- })
54
- })
55
-
56
- test('should pass through model with slash (not an alias)', () => {
57
- pikkuState(null, 'models', 'config', {
58
- models: {},
59
- } as any)
60
-
31
+ test('leaves temperature and maxSteps undefined when not set', () => {
61
32
  const result = resolveModelConfig('testAgent', { model: 'openai/gpt-4' })
62
33
  assert.strictEqual(result.model, 'openai/gpt-4')
63
- })
64
-
65
- test('should use agent override model', () => {
66
- pikkuState(null, 'models', 'config', {
67
- models: { fast: 'anthropic/haiku' },
68
- agentOverrides: { testAgent: { model: 'fast' } },
69
- } as any)
70
-
71
- const result = resolveModelConfig('testAgent', {
72
- model: 'anthropic/default',
73
- })
74
- assert.strictEqual(result.model, 'anthropic/haiku')
75
- })
76
-
77
- test('should use agent override temperature over alias', () => {
78
- pikkuState(null, 'models', 'config', {
79
- models: { smart: { model: 'anthropic/opus', temperature: 0.5 } },
80
- agentOverrides: { testAgent: { model: 'smart', temperature: 0.1 } },
81
- } as any)
82
-
83
- const result = resolveModelConfig('testAgent', {
84
- model: 'anthropic/default',
85
- })
86
- assert.strictEqual(result.temperature, 0.1)
87
- })
88
-
89
- test('should use agentDefaults as fallback', () => {
90
- pikkuState(null, 'models', 'config', {
91
- models: {},
92
- agentDefaults: { temperature: 0.5, maxSteps: 20 },
93
- } as any)
94
-
95
- const result = resolveModelConfig('testAgent', {
96
- model: 'anthropic/claude-3',
97
- })
98
- assert.strictEqual(result.temperature, 0.5)
99
- assert.strictEqual(result.maxSteps, 20)
100
- })
101
-
102
- test('should prioritize: override > alias > defaults > agent', () => {
103
- pikkuState(null, 'models', 'config', {
104
- models: { fast: { model: 'anthropic/haiku', temperature: 0.3 } },
105
- agentDefaults: { temperature: 0.5 },
106
- agentOverrides: { testAgent: { temperature: 0.1 } },
107
- } as any)
108
-
109
- const result = resolveModelConfig('testAgent', {
110
- model: 'fast',
111
- temperature: 0.9,
112
- })
113
- assert.strictEqual(result.temperature, 0.1)
34
+ assert.strictEqual(result.temperature, undefined)
35
+ assert.strictEqual(result.maxSteps, undefined)
114
36
  })
115
37
  })
@@ -1,43 +1,19 @@
1
- import { pikkuState } from '../../pikku-state.js'
2
-
1
+ /**
2
+ * Resolves the effective model/temperature/maxSteps for an agent at runtime.
3
+ *
4
+ * Models are declared per-agent using the provider-qualified `provider/model`
5
+ * form (e.g. `openai/gpt-5-mini`); there is no config-level alias map. This
6
+ * function remains the single merge point for request-time overrides (see
7
+ * ai-agent-prepare's `input.model`), so callers stay decoupled from how the
8
+ * effective config is assembled.
9
+ */
3
10
  export function resolveModelConfig(
4
- agentName: string,
11
+ _agentName: string,
5
12
  agent: { model: string; temperature?: number; maxSteps?: number }
6
13
  ): { model: string; temperature?: number; maxSteps?: number } {
7
- let config: any = {}
8
- try {
9
- config = pikkuState(null, 'models', 'config') ?? {}
10
- } catch {}
11
- const models = config.models ?? {}
12
- const defaults = config.agentDefaults
13
- const agentOverride = config.agentOverrides?.[agentName]
14
-
15
- let rawModel = agentOverride?.model ?? agent.model
16
-
17
- let aliasTemperature: number | undefined
18
- let aliasMaxSteps: number | undefined
19
- if (!rawModel.includes('/')) {
20
- const entry = models[rawModel]
21
- if (!entry) throw new Error(`Unknown model alias '${rawModel}'.`)
22
- if (typeof entry === 'string') {
23
- rawModel = entry
24
- } else {
25
- rawModel = entry.model
26
- aliasTemperature = entry.temperature
27
- aliasMaxSteps = entry.maxSteps
28
- }
14
+ return {
15
+ model: agent.model,
16
+ temperature: agent.temperature,
17
+ maxSteps: agent.maxSteps,
29
18
  }
30
-
31
- const temperature =
32
- agentOverride?.temperature ??
33
- aliasTemperature ??
34
- defaults?.temperature ??
35
- agent.temperature
36
- const maxSteps =
37
- agentOverride?.maxSteps ??
38
- aliasMaxSteps ??
39
- defaults?.maxSteps ??
40
- agent.maxSteps
41
-
42
- return { model: rawModel, temperature, maxSteps }
43
19
  }
@@ -0,0 +1,292 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { resetPikkuState, pikkuState } from '../../pikku-state.js'
5
+ import {
6
+ buildInstructions,
7
+ buildToolDefs,
8
+ createScopedChannel,
9
+ getAddonCredentialRequirements,
10
+ resolveAgent,
11
+ } from './ai-agent-prepare.js'
12
+ import type {
13
+ AIStreamChannel,
14
+ AIStreamEvent,
15
+ CoreAIAgent,
16
+ PikkuAIMiddlewareHooks,
17
+ } from './ai-agent.types.js'
18
+
19
+ beforeEach(() => {
20
+ resetPikkuState()
21
+ })
22
+
23
+ const addAgent = (agentName: string, overrides: Partial<CoreAIAgent> = {}) => {
24
+ const agent: CoreAIAgent = {
25
+ name: agentName,
26
+ description: `${agentName} description`,
27
+ goal: `${agentName} goal`,
28
+ instructions: `${agentName} instructions`,
29
+ model: 'test/test-model',
30
+ ...overrides,
31
+ }
32
+
33
+ pikkuState(null, 'agent', 'agents').set(agentName, agent)
34
+ pikkuState(null, 'agent', 'agentsMeta')[agentName] = {
35
+ ...agent,
36
+ inputSchema: null,
37
+ outputSchema: null,
38
+ workingMemorySchema: null,
39
+ } as any
40
+ }
41
+
42
+ describe('ai-agent-prepare', () => {
43
+ test('getAddonCredentialRequirements returns deduplicated wire oauth credentials', () => {
44
+ pikkuState(null, 'addons', 'packages').set('github', {
45
+ package: '@test/github-addon',
46
+ })
47
+ pikkuState('@test/github-addon', 'package', 'credentialsMeta', {
48
+ githubAuth: {
49
+ type: 'wire',
50
+ oauth2: true,
51
+ displayName: 'GitHub OAuth',
52
+ },
53
+ apiKey: {
54
+ type: 'wire',
55
+ oauth2: false,
56
+ },
57
+ })
58
+
59
+ const requirements = getAddonCredentialRequirements([
60
+ 'github:getProfile',
61
+ 'github:listRepos',
62
+ 'localTool',
63
+ ])
64
+
65
+ assert.deepEqual(requirements, [
66
+ {
67
+ credentialName: 'githubAuth',
68
+ displayName: 'GitHub OAuth',
69
+ addonNamespace: 'github',
70
+ type: 'wire',
71
+ oauth2: true,
72
+ },
73
+ ])
74
+ })
75
+
76
+ test('resolveAgent resolves root and addon agents and rejects missing names', () => {
77
+ addAgent('root-agent')
78
+
79
+ const addonAgent: CoreAIAgent = {
80
+ name: 'addon-agent',
81
+ description: 'addon',
82
+ goal: 'addon goal',
83
+ instructions: 'addon instructions',
84
+ model: 'test/test-model',
85
+ }
86
+
87
+ pikkuState(null, 'addons', 'packages').set('support', {
88
+ package: '@test/support-addon',
89
+ })
90
+ pikkuState('@test/support-addon', 'agent', 'agents').set(
91
+ 'helper',
92
+ addonAgent
93
+ )
94
+
95
+ assert.equal(resolveAgent('root-agent').packageName, null)
96
+ assert.equal(resolveAgent('root-agent').resolvedName, 'root-agent')
97
+ assert.equal(
98
+ resolveAgent('support:helper').packageName,
99
+ '@test/support-addon'
100
+ )
101
+ assert.equal(resolveAgent('support:helper').resolvedName, 'helper')
102
+ assert.throws(() => resolveAgent(''), {
103
+ message: 'resolveAgent called with undefined agentName',
104
+ })
105
+ assert.throws(() => resolveAgent('missing-agent'), {
106
+ message: 'AI agent not found: missing-agent',
107
+ })
108
+ })
109
+
110
+ test('buildInstructions includes tool and sub-agent guidance when configured', async () => {
111
+ addAgent('planner', {
112
+ role: 'You are a planner.',
113
+ personality: 'Be concise.',
114
+ goal: 'Finish the task.',
115
+ tools: ['search'],
116
+ agents: ['coder'],
117
+ })
118
+
119
+ const instructions = await buildInstructions('planner', null)
120
+ assert.match(instructions, /You are a planner\./)
121
+ assert.match(instructions, /Be concise\./)
122
+ assert.match(instructions, /Finish the task\./)
123
+ assert.match(instructions, /Tool usage rules:/)
124
+ assert.match(instructions, /When calling a sub-agent/)
125
+ })
126
+
127
+ test('createScopedChannel forwards stream events, captures approvals, and suppresses done', () => {
128
+ const events: AIStreamEvent[] = []
129
+ const channel = createScopedChannel(
130
+ {
131
+ channelId: 'root',
132
+ openingData: undefined,
133
+ state: 'open',
134
+ send: (event: AIStreamEvent) => {
135
+ events.push(event)
136
+ },
137
+ sendBinary: () => {},
138
+ close: () => {},
139
+ setState: () => {},
140
+ getState: () => 'open',
141
+ clearState: () => {},
142
+ } as AIStreamChannel,
143
+ 'sub-agent',
144
+ 'session-1'
145
+ )
146
+
147
+ channel.send({ type: 'text-delta', text: 'hello' } as AIStreamEvent)
148
+ channel.send({
149
+ type: 'approval-request',
150
+ toolCallId: 'tc-1',
151
+ toolName: 'tool-a',
152
+ args: { x: 1 },
153
+ runId: 'run-1',
154
+ } as AIStreamEvent)
155
+ channel.send({ type: 'done' } as AIStreamEvent)
156
+
157
+ assert.equal(events.length, 1)
158
+ assert.equal(events[0].type, 'text-delta')
159
+ assert.deepEqual(channel.approvals, [
160
+ {
161
+ toolCallId: 'tc-1',
162
+ toolName: 'tool-a',
163
+ args: { x: 1 },
164
+ runId: 'run-1',
165
+ },
166
+ ])
167
+ assert.equal(channel.channelId, 'root:sub-agent:session-1')
168
+ })
169
+
170
+ test('buildToolDefs reports missing RPCs, builds approval descriptions, and wraps tool hooks', async () => {
171
+ addAgent('ops-agent')
172
+ pikkuState(null, 'agent', 'agentsMeta')['ops-agent'] = {
173
+ ...pikkuState(null, 'agent', 'agentsMeta')['ops-agent'],
174
+ tools: ['missingTool', 'deploy'],
175
+ } as any
176
+ pikkuState(null, 'rpc', 'meta').deploy = 'deploy'
177
+ pikkuState(null, 'function', 'meta').deploy = {
178
+ description: 'Deploy the service',
179
+ approvalRequired: true,
180
+ inputSchemaName: 'DeployInput',
181
+ }
182
+ pikkuState(null, 'misc', 'schemas').set('DeployInput', {
183
+ type: 'object',
184
+ })
185
+ pikkuState(null, 'function', 'functions').set('deploy', {
186
+ func: async (_services: any, input: any) => `ok:${JSON.stringify(input)}`,
187
+ approvalDescription: async (_services: any, input: any) =>
188
+ `Deploy ${input.env}`,
189
+ })
190
+
191
+ const beforeCalls: unknown[] = []
192
+ const afterCalls: unknown[] = []
193
+ const middlewares: PikkuAIMiddlewareHooks[] = [
194
+ {
195
+ beforeToolCall: async (_services, ctx) => {
196
+ beforeCalls.push(ctx)
197
+ return { args: { ...(ctx.args as any), extra: true } }
198
+ },
199
+ afterToolCall: async (_services, ctx) => {
200
+ afterCalls.push(ctx)
201
+ return { result: `wrapped:${String(ctx.result)}` }
202
+ },
203
+ },
204
+ ]
205
+
206
+ const singletonServices = {
207
+ logger: {
208
+ warn: () => {},
209
+ },
210
+ } as any
211
+ pikkuState(null, 'package', 'singletonServices', singletonServices)
212
+
213
+ const { tools, missingRpcs } = await buildToolDefs(
214
+ {},
215
+ new Map<string, string>(),
216
+ 'resource-1',
217
+ 'ops-agent',
218
+ null,
219
+ undefined,
220
+ middlewares
221
+ )
222
+
223
+ assert.deepEqual(missingRpcs, ['missingTool'])
224
+ assert.equal(tools.length, 1)
225
+ assert.equal(tools[0].name, 'deploy')
226
+ assert.equal(tools[0].needsApproval, true)
227
+ assert.deepEqual(tools[0].inputSchema, { type: 'object', properties: {} })
228
+ assert.equal(
229
+ await tools[0].approvalDescriptionFn?.({ env: 'prod' }),
230
+ 'Deploy prod'
231
+ )
232
+
233
+ const result = await tools[0].execute({ env: 'prod' })
234
+ assert.equal(result, 'wrapped:ok:{"env":"prod","extra":true}')
235
+ assert.equal(beforeCalls.length, 1)
236
+ assert.equal(afterCalls.length, 1)
237
+ })
238
+
239
+ test('buildToolDefs skips permissioned tools without a session and adds sub-agent tools', async () => {
240
+ addAgent('manager', {
241
+ agents: ['assistant'],
242
+ })
243
+ addAgent('assistant', {
244
+ description: 'Assistant agent',
245
+ })
246
+
247
+ pikkuState(null, 'agent', 'agentsMeta').manager = {
248
+ ...pikkuState(null, 'agent', 'agentsMeta').manager,
249
+ tools: ['secret-tool'],
250
+ agents: ['assistant'],
251
+ } as any
252
+ pikkuState(null, 'rpc', 'meta')['secret-tool'] = 'secret-tool'
253
+ pikkuState(null, 'function', 'meta')['secret-tool'] = {
254
+ description: 'Secret tool',
255
+ permissions: ['admin'],
256
+ inputSchemaName: 'SecretInput',
257
+ }
258
+ pikkuState(null, 'misc', 'schemas').set('SecretInput', {
259
+ type: 'object',
260
+ properties: { id: { type: 'string' } },
261
+ })
262
+
263
+ const singletonServices = {
264
+ logger: {
265
+ warn: () => {},
266
+ },
267
+ } as any
268
+ pikkuState(null, 'package', 'singletonServices', singletonServices)
269
+
270
+ const { tools } = await buildToolDefs(
271
+ {},
272
+ new Map<string, string>(),
273
+ 'resource-1',
274
+ 'manager',
275
+ null
276
+ )
277
+
278
+ assert.equal(tools.length, 1)
279
+ assert.equal(tools[0].name, 'assistant')
280
+ assert.deepEqual(tools[0].inputSchema, {
281
+ type: 'object',
282
+ properties: {
283
+ message: { type: 'string' },
284
+ session: {
285
+ type: 'string',
286
+ description: 'Short session label for thread continuity',
287
+ },
288
+ },
289
+ required: ['message', 'session'],
290
+ })
291
+ })
292
+ })
@@ -265,6 +265,9 @@ export function createScopedChannel(
265
265
  parent.send(event)
266
266
  }
267
267
  },
268
+ setState: (s) => parent.setState(s),
269
+ getState: () => parent.getState(),
270
+ clearState: () => parent.clearState(),
268
271
  }
269
272
  }
270
273
 
@@ -386,7 +389,7 @@ export async function buildToolDefs(
386
389
  }
387
390
 
388
391
  tools.push({
389
- name: toolName.replaceAll(':', '__'),
392
+ name: toolName.replaceAll('@', '_').replaceAll(':', '__'),
390
393
  description: fnMeta?.description || fnMeta?.title || toolName,
391
394
  inputSchema,
392
395
  needsApproval: needsApproval || undefined,