@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.
- package/CHANGELOG.md +17 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/handle-error.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/handle-error.test.ts +1 -1
- package/src/handle-error.ts +4 -1
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- 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
|
|
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('
|
|
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('
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
assert.strictEqual(result.model, '
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
_agentName: string,
|
|
5
12
|
agent: { model: string; temperature?: number; maxSteps?: number }
|
|
6
13
|
): { model: string; temperature?: number; maxSteps?: number } {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|