@pikku/core 0.12.86 → 0.12.89
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 +115 -0
- package/dist/dev/hot-reload.d.ts +1 -1
- package/dist/dev/hot-reload.js +1 -1
- package/dist/types/core.types.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/wirings/agent/agent-memory.d.ts +11 -0
- package/dist/wirings/agent/agent-memory.js +17 -2
- package/dist/wirings/agent/agent-stream.js +115 -91
- package/dist/wirings/agent/agent.types.d.ts +9 -8
- package/dist/wirings/rpc/rpc-runner.js +1 -1
- package/dist/wirings/variable/validate-variable-definitions.js +2 -2
- package/dist/wirings/variable/variable.types.d.ts +13 -7
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +20 -6
- package/dist/wirings/workflow/pikku-workflow-service.js +29 -27
- package/dist/wirings/workflow/workflow-constants.d.ts +17 -0
- package/dist/wirings/workflow/workflow-constants.js +17 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +18 -1
- package/dist/wirings/workflow/workflow-recovery.js +30 -2
- package/dist/wirings/workflow/workflow-step-claim.d.ts +16 -0
- package/dist/wirings/workflow/workflow-step-claim.js +27 -0
- package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +47 -0
- package/knowledge/decisions/security/index.md +2 -1
- package/knowledge/decisions/security/scaffold-features-are-authenticated-unless-opted-out.md +6 -0
- package/package.json +1 -1
- package/src/dev/hot-reload.ts +1 -1
- package/src/types/core.types.ts +1 -1
- package/src/types/index.ts +24 -1
- package/src/wirings/agent/agent-memory.test.ts +73 -0
- package/src/wirings/agent/agent-memory.ts +16 -2
- package/src/wirings/agent/agent-middleware.types.test.ts +41 -0
- package/src/wirings/agent/agent-stream-delegate.test.ts +381 -0
- package/src/wirings/agent/agent-stream.ts +158 -78
- package/src/wirings/agent/agent.types.ts +9 -8
- package/src/wirings/rpc/rpc-runner.test.ts +6 -1
- package/src/wirings/rpc/rpc-runner.ts +1 -1
- package/src/wirings/variable/validate-variable-definitions.test.ts +11 -10
- package/src/wirings/variable/validate-variable-definitions.ts +2 -2
- package/src/wirings/variable/variable.types.ts +13 -7
- package/src/wirings/workflow/pikku-scenario-service.ts +29 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +34 -27
- package/src/wirings/workflow/workflow-constants.ts +19 -0
- package/src/wirings/workflow/workflow-recovery.ts +31 -1
- package/src/wirings/workflow/workflow-stalled-recovery.test.ts +46 -0
- package/src/wirings/workflow/workflow-step-claim.ts +46 -0
- package/src/wirings/workflow/workflow-terminal-run-guard.test.ts +105 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +2 -1
|
@@ -0,0 +1,381 @@
|
|
|
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 { streamAgent, resumeAgent } from './agent-stream.js'
|
|
6
|
+
import type {
|
|
7
|
+
CoreAgent,
|
|
8
|
+
AgentStreamChannel,
|
|
9
|
+
AgentStreamEvent,
|
|
10
|
+
} from './agent.types.js'
|
|
11
|
+
import type {
|
|
12
|
+
AgentStepResult,
|
|
13
|
+
AgentRunnerParams,
|
|
14
|
+
} from '../../services/agent-runner-service.js'
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
resetPikkuState()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const registerAgent = (
|
|
21
|
+
name: string,
|
|
22
|
+
overrides: Partial<CoreAgent> = {},
|
|
23
|
+
metaOverrides: Record<string, unknown> = {}
|
|
24
|
+
) => {
|
|
25
|
+
const agent: CoreAgent = {
|
|
26
|
+
name,
|
|
27
|
+
description: `${name} agent`,
|
|
28
|
+
instructions: name,
|
|
29
|
+
model: 'test/test-model',
|
|
30
|
+
...overrides,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pikkuState(null, 'agent', 'agentsMeta')[name] = {
|
|
34
|
+
...agent,
|
|
35
|
+
inputSchema: null,
|
|
36
|
+
outputSchema: null,
|
|
37
|
+
workingMemorySchema: null,
|
|
38
|
+
...metaOverrides,
|
|
39
|
+
} as any
|
|
40
|
+
pikkuState(null, 'agent', 'agents').set(name, agent)
|
|
41
|
+
return agent
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const makeStepResult = (
|
|
45
|
+
overrides?: Partial<AgentStepResult>
|
|
46
|
+
): AgentStepResult => ({
|
|
47
|
+
text: '',
|
|
48
|
+
toolCalls: [],
|
|
49
|
+
toolResults: [],
|
|
50
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
51
|
+
finishReason: 'stop',
|
|
52
|
+
...overrides,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const recordingChannel = (channelId: string) => {
|
|
56
|
+
const events: AgentStreamEvent[] = []
|
|
57
|
+
const channel = {
|
|
58
|
+
channelId,
|
|
59
|
+
openingData: undefined,
|
|
60
|
+
state: 'open',
|
|
61
|
+
send: (event: AgentStreamEvent) => {
|
|
62
|
+
events.push(event)
|
|
63
|
+
},
|
|
64
|
+
sendBinary: () => {},
|
|
65
|
+
setState: () => {},
|
|
66
|
+
close: () => {},
|
|
67
|
+
} as unknown as AgentStreamChannel
|
|
68
|
+
return { channel, events }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parentTextOf = (events: AgentStreamEvent[]) =>
|
|
72
|
+
events
|
|
73
|
+
.filter((e) => e.type === 'text-delta' && !(e as any).agent)
|
|
74
|
+
.map((e) => (e as any).text as string)
|
|
75
|
+
.join('')
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A parent that speaks either side of a hand-off to a specialist — the shape
|
|
79
|
+
* the issue describes, with the model's text made deterministic so the
|
|
80
|
+
* plumbing can be observed without a live call.
|
|
81
|
+
*/
|
|
82
|
+
const delegatingRunner = (
|
|
83
|
+
script: { preHandoff: string; postHandoff: string } = {
|
|
84
|
+
preHandoff: 'Planning. ',
|
|
85
|
+
postHandoff: 'Handed off. <working_memory>{"phase":"two"}</working_memory>',
|
|
86
|
+
}
|
|
87
|
+
) => {
|
|
88
|
+
let parentStep = 0
|
|
89
|
+
return {
|
|
90
|
+
stream: async (params: AgentRunnerParams, channel: AgentStreamChannel) => {
|
|
91
|
+
const subTool = params.tools.find((t) => t.name === 'sub')
|
|
92
|
+
if (!subTool) {
|
|
93
|
+
channel.send({ type: 'text-delta', text: 'specialist output' })
|
|
94
|
+
channel.send({
|
|
95
|
+
type: 'usage',
|
|
96
|
+
tokens: { input: 1, output: 1 },
|
|
97
|
+
} as AgentStreamEvent)
|
|
98
|
+
return makeStepResult({ text: 'specialist output' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (parentStep++ === 0) {
|
|
102
|
+
channel.send({ type: 'text-delta', text: script.preHandoff })
|
|
103
|
+
|
|
104
|
+
await subTool.execute!({ message: 'do phase two', session: 's1' })
|
|
105
|
+
|
|
106
|
+
channel.send({ type: 'text-delta', text: script.postHandoff })
|
|
107
|
+
channel.send({
|
|
108
|
+
type: 'usage',
|
|
109
|
+
tokens: { input: 1, output: 1 },
|
|
110
|
+
} as AgentStreamEvent)
|
|
111
|
+
return makeStepResult({
|
|
112
|
+
text: 'Planning. Handed off.',
|
|
113
|
+
toolCalls: [
|
|
114
|
+
{
|
|
115
|
+
toolCallId: 'call-1',
|
|
116
|
+
toolName: 'sub',
|
|
117
|
+
args: { message: 'do phase two', session: 's1' },
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
toolResults: [
|
|
121
|
+
{
|
|
122
|
+
toolCallId: 'call-1',
|
|
123
|
+
toolName: 'sub',
|
|
124
|
+
result: 'specialist output',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
finishReason: 'tool-calls',
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
channel.send({
|
|
132
|
+
type: 'text-delta',
|
|
133
|
+
text: 'Done. <working_memory>{"phase":"three"}</working_memory>',
|
|
134
|
+
})
|
|
135
|
+
channel.send({
|
|
136
|
+
type: 'usage',
|
|
137
|
+
tokens: { input: 1, output: 1 },
|
|
138
|
+
} as AgentStreamEvent)
|
|
139
|
+
return makeStepResult({ text: 'Done.' })
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const makeServices = (
|
|
145
|
+
runner: { stream: any },
|
|
146
|
+
savedWorkingMemory: unknown[],
|
|
147
|
+
savedMessages: any[] = []
|
|
148
|
+
) => {
|
|
149
|
+
let stored: Record<string, unknown> = {}
|
|
150
|
+
return {
|
|
151
|
+
logger: {
|
|
152
|
+
info: () => {},
|
|
153
|
+
warn: () => {},
|
|
154
|
+
error: () => {},
|
|
155
|
+
debug: () => {},
|
|
156
|
+
},
|
|
157
|
+
agentRunner: runner,
|
|
158
|
+
agentRunState: {
|
|
159
|
+
createRun: async () => 'run-1',
|
|
160
|
+
updateRun: async () => {},
|
|
161
|
+
},
|
|
162
|
+
agentStorage: {
|
|
163
|
+
createThread: async () => {},
|
|
164
|
+
getMessages: async () => [],
|
|
165
|
+
saveMessages: async (threadId: string, messages: any[]) => {
|
|
166
|
+
savedMessages.push(
|
|
167
|
+
...messages.map((message) => ({ threadId, message }))
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
getWorkingMemory: async () => stored,
|
|
171
|
+
saveWorkingMemory: async (
|
|
172
|
+
threadId: string,
|
|
173
|
+
scope: string,
|
|
174
|
+
value: any
|
|
175
|
+
) => {
|
|
176
|
+
stored = value
|
|
177
|
+
savedWorkingMemory.push({ threadId, scope, value })
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
} as any
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const wireDelegatingPair = (agentMode: 'delegate' | 'supervise') => {
|
|
184
|
+
registerAgent('sub', { instructions: 'sub' })
|
|
185
|
+
registerAgent(
|
|
186
|
+
'parent',
|
|
187
|
+
{
|
|
188
|
+
instructions: 'parent',
|
|
189
|
+
agentMode,
|
|
190
|
+
memory: { workingMemory: true } as any,
|
|
191
|
+
},
|
|
192
|
+
{ agents: ['sub'] }
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const runParent = async (script?: {
|
|
197
|
+
preHandoff: string
|
|
198
|
+
postHandoff: string
|
|
199
|
+
}) => {
|
|
200
|
+
const savedWorkingMemory: unknown[] = []
|
|
201
|
+
const savedMessages: any[] = []
|
|
202
|
+
pikkuState(
|
|
203
|
+
null,
|
|
204
|
+
'package',
|
|
205
|
+
'singletonServices',
|
|
206
|
+
makeServices(delegatingRunner(script), savedWorkingMemory, savedMessages)
|
|
207
|
+
)
|
|
208
|
+
const { channel, events } = recordingChannel('c1')
|
|
209
|
+
const fullText = await streamAgent(
|
|
210
|
+
'parent',
|
|
211
|
+
{ message: 'go', threadId: 't1', resourceId: 'r1' },
|
|
212
|
+
channel,
|
|
213
|
+
{}
|
|
214
|
+
)
|
|
215
|
+
return { savedWorkingMemory, savedMessages, events, fullText }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe('delegate mode and working memory', () => {
|
|
219
|
+
test('collects working memory the parent writes after a hand-off', async () => {
|
|
220
|
+
wireDelegatingPair('delegate')
|
|
221
|
+
const { savedWorkingMemory } = await runParent()
|
|
222
|
+
|
|
223
|
+
assert.deepEqual(
|
|
224
|
+
savedWorkingMemory.map((s: any) => s.value.phase),
|
|
225
|
+
['two', 'three']
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('still collects working memory written before the hand-off', async () => {
|
|
230
|
+
wireDelegatingPair('delegate')
|
|
231
|
+
const { savedWorkingMemory } = await runParent({
|
|
232
|
+
preHandoff: 'Planning. <working_memory>{"phase":"one"}</working_memory>',
|
|
233
|
+
postHandoff: 'Handed off. ',
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
assert.deepEqual(
|
|
237
|
+
savedWorkingMemory.map((s: any) => s.value.phase),
|
|
238
|
+
['one', 'three']
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('the client receives no parent text after the hand-off', async () => {
|
|
243
|
+
wireDelegatingPair('delegate')
|
|
244
|
+
const { events } = await runParent()
|
|
245
|
+
|
|
246
|
+
assert.equal(parentTextOf(events), 'Planning. ')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('the parent assistant text persisted stays suppressed', async () => {
|
|
250
|
+
wireDelegatingPair('delegate')
|
|
251
|
+
const { fullText, savedMessages } = await runParent()
|
|
252
|
+
|
|
253
|
+
assert.equal(fullText, 'Planning. ')
|
|
254
|
+
const assistantText = savedMessages
|
|
255
|
+
.filter(
|
|
256
|
+
({ threadId, message }: any) =>
|
|
257
|
+
threadId === 't1' && message.role === 'assistant'
|
|
258
|
+
)
|
|
259
|
+
.map(({ message }: any) =>
|
|
260
|
+
typeof message.content === 'string'
|
|
261
|
+
? message.content
|
|
262
|
+
: (message.content ?? [])
|
|
263
|
+
.filter((part: any) => part.type === 'text')
|
|
264
|
+
.map((part: any) => part.text)
|
|
265
|
+
.join('')
|
|
266
|
+
)
|
|
267
|
+
.join('')
|
|
268
|
+
assert.equal(assistantText, 'Planning. ')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('supervise mode streams every parent delta and collects every update', async () => {
|
|
272
|
+
wireDelegatingPair('supervise')
|
|
273
|
+
const { events, savedWorkingMemory } = await runParent()
|
|
274
|
+
|
|
275
|
+
assert.equal(parentTextOf(events), 'Planning. Handed off. Done. ')
|
|
276
|
+
assert.deepEqual(
|
|
277
|
+
savedWorkingMemory.map((s: any) => s.value.phase),
|
|
278
|
+
['two', 'three']
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('user channel middleware sees exactly the deltas the client sees', async () => {
|
|
283
|
+
wireDelegatingPair('delegate')
|
|
284
|
+
const seen: string[] = []
|
|
285
|
+
const agent = pikkuState(null, 'agent', 'agents').get('parent')!
|
|
286
|
+
agent.channelMiddleware = [
|
|
287
|
+
async (_services: any, event: any, next: any) => {
|
|
288
|
+
if (event.type === 'text-delta') seen.push(event.text)
|
|
289
|
+
await next(event)
|
|
290
|
+
},
|
|
291
|
+
] as any
|
|
292
|
+
pikkuState(null, 'agent', 'agents').set('parent', agent)
|
|
293
|
+
|
|
294
|
+
const { events } = await runParent({
|
|
295
|
+
preHandoff: 'Planning. <working_memory>{"phase":"one"}</working_memory>',
|
|
296
|
+
postHandoff: 'Handed off. ',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const clientDeltas = events
|
|
300
|
+
.filter((e) => e.type === 'text-delta' && !(e as any).agent)
|
|
301
|
+
.map((e) => (e as any).text as string)
|
|
302
|
+
|
|
303
|
+
assert.deepEqual(seen, clientDeltas)
|
|
304
|
+
assert.deepEqual(seen, ['Planning. '])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('the raw text a delegating parent speaks still reaches afterStep', async () => {
|
|
308
|
+
wireDelegatingPair('delegate')
|
|
309
|
+
|
|
310
|
+
const stepTexts: string[] = []
|
|
311
|
+
const agent = pikkuState(null, 'agent', 'agents').get('parent')!
|
|
312
|
+
agent.agentMiddleware = [
|
|
313
|
+
{
|
|
314
|
+
afterStep: async (_services: any, ctx: any) => {
|
|
315
|
+
stepTexts.push(ctx.text)
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
] as any
|
|
319
|
+
pikkuState(null, 'agent', 'agents').set('parent', agent)
|
|
320
|
+
|
|
321
|
+
await runParent()
|
|
322
|
+
|
|
323
|
+
assert.deepEqual(stepTexts, ['Planning. Handed off.', 'Done.'])
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('delegate mode on the resume path', () => {
|
|
328
|
+
test('suppresses parent text after a hand-off and still collects working memory', async () => {
|
|
329
|
+
wireDelegatingPair('delegate')
|
|
330
|
+
|
|
331
|
+
const savedWorkingMemory: unknown[] = []
|
|
332
|
+
const services = makeServices(delegatingRunner(), savedWorkingMemory)
|
|
333
|
+
let pendingApprovals: unknown[] = [
|
|
334
|
+
{
|
|
335
|
+
type: 'tool-call',
|
|
336
|
+
toolCallId: 'tc-1',
|
|
337
|
+
toolName: 'sub',
|
|
338
|
+
args: { message: 'approved hand-off', session: 's0' },
|
|
339
|
+
runId: 'run-1',
|
|
340
|
+
},
|
|
341
|
+
]
|
|
342
|
+
services.agentRunState = {
|
|
343
|
+
createRun: async () => 'run-1',
|
|
344
|
+
updateRun: async () => {},
|
|
345
|
+
resolveApproval: async () => {
|
|
346
|
+
pendingApprovals = []
|
|
347
|
+
return true
|
|
348
|
+
},
|
|
349
|
+
getRun: async () => ({
|
|
350
|
+
runId: 'run-1',
|
|
351
|
+
id: 'run-1',
|
|
352
|
+
agentName: 'parent',
|
|
353
|
+
threadId: 't1',
|
|
354
|
+
resourceId: 'r1',
|
|
355
|
+
status: 'suspended',
|
|
356
|
+
pendingApprovals,
|
|
357
|
+
messages: [],
|
|
358
|
+
}),
|
|
359
|
+
}
|
|
360
|
+
pikkuState(null, 'package', 'singletonServices', services)
|
|
361
|
+
|
|
362
|
+
const { channel, events } = recordingChannel('c1')
|
|
363
|
+
await resumeAgent(
|
|
364
|
+
{ runId: 'run-1', toolCallId: 'tc-1', approved: true },
|
|
365
|
+
channel,
|
|
366
|
+
{
|
|
367
|
+
sessionService: {
|
|
368
|
+
get: () => ({ userId: 'r1' }),
|
|
369
|
+
setInitial: () => {},
|
|
370
|
+
sessionChanged: false,
|
|
371
|
+
},
|
|
372
|
+
} as any
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
assert.equal(parentTextOf(events), 'Planning. ')
|
|
376
|
+
assert.deepEqual(
|
|
377
|
+
savedWorkingMemory.map((s: any) => s.value.phase),
|
|
378
|
+
['two', 'three']
|
|
379
|
+
)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
combineChannelMiddleware,
|
|
21
21
|
wrapChannelWithMiddleware,
|
|
22
22
|
} from '../channel/channel-middleware-runner.js'
|
|
23
|
+
import type { CorePikkuChannelMiddleware } from '../channel/channel.types.js'
|
|
24
|
+
import type { CoreSingletonServices } from '../../types/core.types.js'
|
|
23
25
|
import type { AgentStorageService } from '../../services/agent-storage-service.js'
|
|
24
26
|
import type {
|
|
25
27
|
AgentRunnerParams,
|
|
@@ -300,6 +302,64 @@ async function postStreamCleanup(
|
|
|
300
302
|
})
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Adapts `modifyOutputStream` hooks to channel middleware, one closure per
|
|
307
|
+
* hook so each keeps its own `state` and event log for the whole run.
|
|
308
|
+
*/
|
|
309
|
+
const toChannelStreamMiddleware = (
|
|
310
|
+
hooks: PikkuAgentMiddlewareHooks[],
|
|
311
|
+
sharedNotes: Record<string, unknown>,
|
|
312
|
+
signal: AbortSignal
|
|
313
|
+
) =>
|
|
314
|
+
hooks
|
|
315
|
+
.filter((mw) => mw.modifyOutputStream)
|
|
316
|
+
.map((mw) => {
|
|
317
|
+
const state: Record<string, unknown> = {}
|
|
318
|
+
const allEvents: AgentStreamEvent[] = []
|
|
319
|
+
return async (services: any, event: any, next: any) => {
|
|
320
|
+
allEvents.push(event)
|
|
321
|
+
const result = await mw.modifyOutputStream!(services, {
|
|
322
|
+
event,
|
|
323
|
+
allEvents,
|
|
324
|
+
state,
|
|
325
|
+
shared: sharedNotes,
|
|
326
|
+
// Sends downstream directly, so a hook can hand back the fast event
|
|
327
|
+
// now and push the slow one when it is ready.
|
|
328
|
+
emit: next,
|
|
329
|
+
signal,
|
|
330
|
+
})
|
|
331
|
+
if (result == null) return
|
|
332
|
+
if (Array.isArray(result)) {
|
|
333
|
+
for (const r of result) await next(r)
|
|
334
|
+
} else {
|
|
335
|
+
await next(result)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The branch a delegating parent's own spoken text is routed down once it has
|
|
342
|
+
* handed off: the same working-memory hook instances as the main chain, so the
|
|
343
|
+
* in-band `<working_memory>` blocks are still collected, ending in a sink so
|
|
344
|
+
* neither the client, the thread history, nor user channel middleware sees
|
|
345
|
+
* text the parent was supposed to keep to itself.
|
|
346
|
+
*
|
|
347
|
+
* Routing at ingress rather than dropping further down the chain keeps the
|
|
348
|
+
* decision synchronous with the send: the working-memory hook is async, so a
|
|
349
|
+
* hand-off landing mid-flight would otherwise retroactively suppress text the
|
|
350
|
+
* parent spoke before it.
|
|
351
|
+
*/
|
|
352
|
+
const createWorkingMemoryOnlyChannel = (
|
|
353
|
+
channel: AgentStreamChannel,
|
|
354
|
+
services: CoreSingletonServices,
|
|
355
|
+
workingMemoryMiddleware: readonly CorePikkuChannelMiddleware[]
|
|
356
|
+
): AgentStreamChannel =>
|
|
357
|
+
wrapChannelWithMiddleware(
|
|
358
|
+
{ channel: { ...channel, send: () => {} } as AgentStreamChannel },
|
|
359
|
+
services,
|
|
360
|
+
workingMemoryMiddleware
|
|
361
|
+
).channel as AgentStreamChannel
|
|
362
|
+
|
|
303
363
|
type StepLoopParams = {
|
|
304
364
|
agent: CoreAgent
|
|
305
365
|
runnerParams: AgentRunnerParams
|
|
@@ -720,13 +780,18 @@ export async function streamAgent(
|
|
|
720
780
|
return ''
|
|
721
781
|
}
|
|
722
782
|
|
|
723
|
-
const
|
|
724
|
-
|
|
783
|
+
const workingMemoryMiddleware = getWorkingMemoryMiddleware(
|
|
784
|
+
memoryConfig,
|
|
785
|
+
storage,
|
|
786
|
+
{
|
|
725
787
|
threadId,
|
|
726
788
|
workingMemorySchemaName,
|
|
727
789
|
logger: singletonServices.logger,
|
|
728
790
|
schemaService: singletonServices.schema,
|
|
729
|
-
}
|
|
791
|
+
}
|
|
792
|
+
)
|
|
793
|
+
const agentMiddlewares: PikkuAgentMiddlewareHooks[] = [
|
|
794
|
+
...workingMemoryMiddleware,
|
|
730
795
|
...(agent.agentMiddleware ?? []),
|
|
731
796
|
]
|
|
732
797
|
|
|
@@ -785,45 +850,36 @@ export async function streamAgent(
|
|
|
785
850
|
singletonServices.logger
|
|
786
851
|
)
|
|
787
852
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
state,
|
|
799
|
-
shared: sharedNotes,
|
|
800
|
-
// Sends downstream directly, so a hook can hand back the fast event
|
|
801
|
-
// now and push the slow one when it is ready.
|
|
802
|
-
emit: next,
|
|
803
|
-
signal: interruptHandle.signal,
|
|
804
|
-
})
|
|
805
|
-
if (result == null) return
|
|
806
|
-
if (Array.isArray(result)) {
|
|
807
|
-
for (const r of result) await next(r)
|
|
808
|
-
} else {
|
|
809
|
-
await next(result)
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
})
|
|
853
|
+
const workingMemoryStreamMiddleware = toChannelStreamMiddleware(
|
|
854
|
+
workingMemoryMiddleware,
|
|
855
|
+
sharedNotes,
|
|
856
|
+
interruptHandle.signal
|
|
857
|
+
)
|
|
858
|
+
const streamMiddleware = toChannelStreamMiddleware(
|
|
859
|
+
agent.agentMiddleware ?? [],
|
|
860
|
+
sharedNotes,
|
|
861
|
+
interruptHandle.signal
|
|
862
|
+
)
|
|
813
863
|
|
|
814
864
|
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
815
865
|
const meta = agentsMeta[resolvedName]
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
866
|
+
|
|
867
|
+
const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length
|
|
868
|
+
const delegateState = { delegated: false }
|
|
869
|
+
if (isDelegateMode) {
|
|
870
|
+
streamContext.delegateState = delegateState
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const allChannelMiddleware = [
|
|
874
|
+
...workingMemoryStreamMiddleware,
|
|
875
|
+
...combineChannelMiddleware('agent', `stream:${agentName}`, {
|
|
820
876
|
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
821
877
|
wireChannelMiddleware: [
|
|
822
878
|
...(agent.channelMiddleware ?? []),
|
|
823
879
|
...streamMiddleware,
|
|
824
880
|
],
|
|
825
|
-
}
|
|
826
|
-
|
|
881
|
+
}),
|
|
882
|
+
]
|
|
827
883
|
|
|
828
884
|
const persistingChannel = createPersistingChannel(
|
|
829
885
|
channel,
|
|
@@ -857,23 +913,27 @@ export async function streamAgent(
|
|
|
857
913
|
},
|
|
858
914
|
}
|
|
859
915
|
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
916
|
+
const workingMemoryOnlyChannel =
|
|
917
|
+
isDelegateMode && workingMemoryStreamMiddleware.length > 0
|
|
918
|
+
? createWorkingMemoryOnlyChannel(
|
|
919
|
+
channel,
|
|
920
|
+
singletonServices,
|
|
921
|
+
workingMemoryStreamMiddleware
|
|
922
|
+
)
|
|
923
|
+
: undefined
|
|
924
|
+
|
|
925
|
+
const outputChannel: AgentStreamChannel = isDelegateMode
|
|
866
926
|
? {
|
|
867
927
|
...credentialFilteredChannel,
|
|
868
928
|
send: (event: AgentStreamEvent) => {
|
|
869
929
|
if (
|
|
870
930
|
delegateState.delegated &&
|
|
871
931
|
(event.type === 'text-delta' || event.type === 'reasoning-delta')
|
|
872
|
-
)
|
|
873
|
-
return
|
|
932
|
+
) {
|
|
933
|
+
return workingMemoryOnlyChannel?.send(event)
|
|
934
|
+
}
|
|
874
935
|
return credentialFilteredChannel.send(event)
|
|
875
936
|
},
|
|
876
|
-
delegateState,
|
|
877
937
|
}
|
|
878
938
|
: credentialFilteredChannel
|
|
879
939
|
|
|
@@ -1350,13 +1410,18 @@ async function continueAfterToolResult(
|
|
|
1350
1410
|
|
|
1351
1411
|
const instructions = await buildInstructions(resolvedName, packageName)
|
|
1352
1412
|
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1413
|
+
const workingMemoryMiddleware = getWorkingMemoryMiddleware(
|
|
1414
|
+
memoryConfig,
|
|
1415
|
+
storage,
|
|
1416
|
+
{
|
|
1355
1417
|
threadId: run.threadId,
|
|
1356
1418
|
workingMemorySchemaName,
|
|
1357
1419
|
logger: singletonServices.logger,
|
|
1358
1420
|
schemaService: singletonServices.schema,
|
|
1359
|
-
}
|
|
1421
|
+
}
|
|
1422
|
+
)
|
|
1423
|
+
const agentMiddlewares: PikkuAgentMiddlewareHooks[] = [
|
|
1424
|
+
...workingMemoryMiddleware,
|
|
1360
1425
|
...(agent.agentMiddleware ?? []),
|
|
1361
1426
|
]
|
|
1362
1427
|
// One bag per run, shared by every middleware — see PikkuAgentMiddlewareHooks.
|
|
@@ -1379,43 +1444,27 @@ async function continueAfterToolResult(
|
|
|
1379
1444
|
singletonServices.logger
|
|
1380
1445
|
)
|
|
1381
1446
|
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
state,
|
|
1393
|
-
shared: sharedNotes,
|
|
1394
|
-
// Sends downstream directly, so a hook can hand back the fast event
|
|
1395
|
-
// now and push the slow one when it is ready.
|
|
1396
|
-
emit: next,
|
|
1397
|
-
signal: interruptHandle.signal,
|
|
1398
|
-
})
|
|
1399
|
-
if (result == null) return
|
|
1400
|
-
if (Array.isArray(result)) {
|
|
1401
|
-
for (const r of result) await next(r)
|
|
1402
|
-
} else {
|
|
1403
|
-
await next(result)
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
})
|
|
1447
|
+
const workingMemoryStreamMiddleware = toChannelStreamMiddleware(
|
|
1448
|
+
workingMemoryMiddleware,
|
|
1449
|
+
sharedNotes,
|
|
1450
|
+
interruptHandle.signal
|
|
1451
|
+
)
|
|
1452
|
+
const streamMiddleware = toChannelStreamMiddleware(
|
|
1453
|
+
agent.agentMiddleware ?? [],
|
|
1454
|
+
sharedNotes,
|
|
1455
|
+
interruptHandle.signal
|
|
1456
|
+
)
|
|
1407
1457
|
|
|
1408
|
-
const allChannelMiddleware =
|
|
1409
|
-
|
|
1410
|
-
`stream:${run.agentName}`,
|
|
1411
|
-
{
|
|
1458
|
+
const allChannelMiddleware = [
|
|
1459
|
+
...workingMemoryStreamMiddleware,
|
|
1460
|
+
...combineChannelMiddleware('agent', `stream:${run.agentName}`, {
|
|
1412
1461
|
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
1413
1462
|
wireChannelMiddleware: [
|
|
1414
1463
|
...(agent.channelMiddleware ?? []),
|
|
1415
1464
|
...streamMiddleware,
|
|
1416
1465
|
],
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1466
|
+
}),
|
|
1467
|
+
]
|
|
1419
1468
|
|
|
1420
1469
|
const persistingChannel = createPersistingChannel(
|
|
1421
1470
|
channel,
|
|
@@ -1433,7 +1482,38 @@ async function continueAfterToolResult(
|
|
|
1433
1482
|
).channel as AgentStreamChannel)
|
|
1434
1483
|
: persistingChannel
|
|
1435
1484
|
|
|
1485
|
+
const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length
|
|
1486
|
+
const delegateState = { delegated: false }
|
|
1487
|
+
|
|
1436
1488
|
const streamContext: StreamContext = { channel, options }
|
|
1489
|
+
if (isDelegateMode) {
|
|
1490
|
+
streamContext.delegateState = delegateState
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const workingMemoryOnlyChannel =
|
|
1494
|
+
isDelegateMode && workingMemoryStreamMiddleware.length > 0
|
|
1495
|
+
? createWorkingMemoryOnlyChannel(
|
|
1496
|
+
channel,
|
|
1497
|
+
singletonServices,
|
|
1498
|
+
workingMemoryStreamMiddleware
|
|
1499
|
+
)
|
|
1500
|
+
: undefined
|
|
1501
|
+
|
|
1502
|
+
const outputChannel: AgentStreamChannel = isDelegateMode
|
|
1503
|
+
? {
|
|
1504
|
+
...wrappedChannel,
|
|
1505
|
+
send: (event: AgentStreamEvent) => {
|
|
1506
|
+
if (
|
|
1507
|
+
delegateState.delegated &&
|
|
1508
|
+
(event.type === 'text-delta' || event.type === 'reasoning-delta')
|
|
1509
|
+
) {
|
|
1510
|
+
return workingMemoryOnlyChannel?.send(event)
|
|
1511
|
+
}
|
|
1512
|
+
return wrappedChannel.send(event)
|
|
1513
|
+
},
|
|
1514
|
+
}
|
|
1515
|
+
: wrappedChannel
|
|
1516
|
+
|
|
1437
1517
|
const resumeTools = (
|
|
1438
1518
|
await buildToolDefs(
|
|
1439
1519
|
params,
|
|
@@ -1472,7 +1552,7 @@ async function continueAfterToolResult(
|
|
|
1472
1552
|
runnerParams,
|
|
1473
1553
|
maxSteps,
|
|
1474
1554
|
agentRunner,
|
|
1475
|
-
streamChannel:
|
|
1555
|
+
streamChannel: outputChannel,
|
|
1476
1556
|
persistingChannel,
|
|
1477
1557
|
channel,
|
|
1478
1558
|
agentMiddlewares,
|