@pikku/core 0.12.86 → 0.12.88
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 +79 -0
- package/dist/dev/hot-reload.d.ts +1 -1
- package/dist/dev/hot-reload.js +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/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 +25 -25
- 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/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/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 +31 -27
- package/src/wirings/workflow/workflow-step-claim.ts +46 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
+
## 0.12.88
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 4712e73: fix: collect working memory from a delegate-mode parent agent
|
|
6
|
+
|
|
7
|
+
A delegating parent's `text-delta` events were dropped at the outermost output
|
|
8
|
+
channel, above the working-memory hook, so every `<working_memory>` block it
|
|
9
|
+
wrote from its first hand-off onward was discarded before anything could read
|
|
10
|
+
it. The parent's text is now routed through the working-memory hook and into a
|
|
11
|
+
sink instead of being dropped outright, so the blocks are collected while the
|
|
12
|
+
client, the thread history and user channel middleware still see nothing.
|
|
13
|
+
|
|
14
|
+
The resume path built no delegate filter at all and streamed a delegating
|
|
15
|
+
parent's text to the client after an approval; it now suppresses text the same
|
|
16
|
+
way the initial path does.
|
|
17
|
+
|
|
18
|
+
The AI SDK rejects a system message inside `messages` outright, so the working
|
|
19
|
+
memory prompt the framework injects as one failed every run that enabled
|
|
20
|
+
working memory at all. The runner now lifts system messages onto the `system`
|
|
21
|
+
option, after the agent's own instructions.
|
|
22
|
+
|
|
23
|
+
- 082403f: fix(agent): make working-memory array semantics explicit
|
|
24
|
+
|
|
25
|
+
`deepMergeWorkingMemory` replaced arrays wholesale as a side effect of its
|
|
26
|
+
object-recursion guard, so nothing in the code said whether that was the
|
|
27
|
+
contract or an accident. The merge now handles arrays in an explicit,
|
|
28
|
+
documented branch. Replace is kept over append: the full state is echoed back
|
|
29
|
+
every turn, so appending would duplicate every item whenever the model re-emitted
|
|
30
|
+
the array.
|
|
31
|
+
|
|
32
|
+
`buildWorkingMemoryPrompt` now states that contract to the model rather than
|
|
33
|
+
leaving "only include changed fields" to be read as permission to send a partial
|
|
34
|
+
array. This is defensive: measured against `gpt-4.1-mini` the wording changes
|
|
35
|
+
nothing, because that model already re-emits the whole list. It is there for
|
|
36
|
+
models that do not.
|
|
37
|
+
|
|
38
|
+
## 0.12.87
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- 9687ad1: fix: hand agent middleware the singleton services its type promises
|
|
43
|
+
|
|
44
|
+
`PikkuAgentMiddlewareHooks` typed its `services` parameter as the project's full
|
|
45
|
+
wire `Services`, while every runtime call site has only ever passed the singleton
|
|
46
|
+
services. A middleware that destructured a wire service typechecked and silently
|
|
47
|
+
received `undefined`.
|
|
48
|
+
|
|
49
|
+
The hooks are now bounded by `CoreSingletonServices` in core, and the generated
|
|
50
|
+
`pikkuAgentMiddleware` defaults to `WiredSingletonServices` like the other
|
|
51
|
+
middleware definers. Nothing changes at runtime: agent middleware hooks a _run_,
|
|
52
|
+
and a run is not a request — it can start from a scheduler or a workflow with no
|
|
53
|
+
wire behind it. A tool the run calls is an ordinary function call and still gets
|
|
54
|
+
its own wire services through `runPikkuFunc`.
|
|
55
|
+
|
|
56
|
+
- 2d21628: fix(kysely): claim a workflow step atomically in every SQL dialect
|
|
57
|
+
|
|
58
|
+
The workflow engine's "atomic claim" was a read-then-write guarded by
|
|
59
|
+
`withStepLock`, and `@pikku/kysely` inherited a silent pass-through for that
|
|
60
|
+
lock — so on every dialect but Postgres and MySQL a redelivered queue job could
|
|
61
|
+
claim a step another dispatch was already running, executing a side-effecting
|
|
62
|
+
step twice.
|
|
63
|
+
|
|
64
|
+
`@pikku/kysely` now claims the step with a status-guarded `UPDATE` and reads the
|
|
65
|
+
affected-row count, which is atomic in every SQL dialect without an
|
|
66
|
+
advisory-lock primitive. Relay redispatch is enabled for all Kysely dialects as
|
|
67
|
+
a result, not just Postgres and MySQL.
|
|
68
|
+
|
|
69
|
+
- 985b87b: Follow through on the variable `required` → `optional` rename: regenerate the
|
|
70
|
+
core API report and update the inspector's `defineVariable` gating-flag test,
|
|
71
|
+
both of which #1369 left describing the deleted `required` flag.
|
|
72
|
+
- 3a83f85: Stop re-exporting package internals through entry points
|
|
73
|
+
|
|
74
|
+
66 names reached consumers only because an `export *` in an entry point swept
|
|
75
|
+
them up. Each one is referenced solely inside its own package, so the star is
|
|
76
|
+
now an explicit named re-export listing what is genuinely public. The
|
|
77
|
+
declarations themselves are untouched — this narrows the entry point, not the
|
|
78
|
+
module.
|
|
79
|
+
|
|
1
80
|
## 0.12.86
|
|
2
81
|
|
|
3
82
|
### Patch Changes
|
package/dist/dev/hot-reload.d.ts
CHANGED
package/dist/dev/hot-reload.js
CHANGED
|
@@ -7,7 +7,7 @@ import { clearPermissionsCache } from '../permissions.js';
|
|
|
7
7
|
import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
|
|
8
8
|
import { httpRouter } from '../wirings/http/routers/http-router.js';
|
|
9
9
|
import { createModuleRunner } from './module-runner.js';
|
|
10
|
-
export
|
|
10
|
+
export { reloadGeneratedMeta, reconcileAddonRegistry } from './reload-meta.js';
|
|
11
11
|
const isFunctionConfig = (value) => {
|
|
12
12
|
return (typeof value === 'object' &&
|
|
13
13
|
value !== null &&
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type { AuthInstance, CommonWireMeta, CoreConfig, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, CreateSingletonServices, CreateWireServices, GetCredential, PikkuWire, PikkuWiringTypes, PostgresConfig, SecretlessServices, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, ServerLifecycle, WireServices, } from './core.types.js';
|
|
2
2
|
export type * from './state.types.js';
|
package/dist/types/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {};
|
|
@@ -8,6 +8,17 @@ export declare function resolveMemoryServices(agent: CoreAgent, singletonService
|
|
|
8
8
|
storage: AgentStorageService | undefined;
|
|
9
9
|
};
|
|
10
10
|
export declare function isWorkingMemoryEnabled(memoryConfig: AgentMemoryConfig | undefined, storage: AgentStorageService | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Merges a model-emitted working memory update into the stored state.
|
|
13
|
+
*
|
|
14
|
+
* Objects merge key by key, and `null` deletes a key. Arrays are replaced
|
|
15
|
+
* wholesale — deliberately, not as a side effect of the object guard below.
|
|
16
|
+
* The full state is echoed back to the model every turn by
|
|
17
|
+
* {@link buildWorkingMemoryPrompt}, so it can always re-emit an array in full,
|
|
18
|
+
* and appending instead would duplicate every item each time it did.
|
|
19
|
+
* `buildWorkingMemoryPrompt` states this contract to the model; the two must
|
|
20
|
+
* stay in agreement.
|
|
21
|
+
*/
|
|
11
22
|
export declare function deepMergeWorkingMemory(existing: Record<string, unknown>, updates: Record<string, unknown>): Record<string, unknown>;
|
|
12
23
|
export declare function buildWorkingMemoryPrompt(currentState: Record<string, unknown> | null, jsonSchema?: Record<string, unknown>): string;
|
|
13
24
|
export declare function loadContextMessages(memoryConfig: AgentMemoryConfig | undefined, storage: AgentStorageService | undefined, input: AgentInput, workingMemoryJsonSchema?: Record<string, unknown>): Promise<AgentMessage[]>;
|
|
@@ -11,6 +11,17 @@ export function resolveMemoryServices(agent, singletonServices) {
|
|
|
11
11
|
export function isWorkingMemoryEnabled(memoryConfig, storage) {
|
|
12
12
|
return !!memoryConfig?.workingMemory && !!storage;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Merges a model-emitted working memory update into the stored state.
|
|
16
|
+
*
|
|
17
|
+
* Objects merge key by key, and `null` deletes a key. Arrays are replaced
|
|
18
|
+
* wholesale — deliberately, not as a side effect of the object guard below.
|
|
19
|
+
* The full state is echoed back to the model every turn by
|
|
20
|
+
* {@link buildWorkingMemoryPrompt}, so it can always re-emit an array in full,
|
|
21
|
+
* and appending instead would duplicate every item each time it did.
|
|
22
|
+
* `buildWorkingMemoryPrompt` states this contract to the model; the two must
|
|
23
|
+
* stay in agreement.
|
|
24
|
+
*/
|
|
14
25
|
export function deepMergeWorkingMemory(existing, updates) {
|
|
15
26
|
const result = { ...existing };
|
|
16
27
|
for (const key of Object.keys(updates)) {
|
|
@@ -21,8 +32,10 @@ export function deepMergeWorkingMemory(existing, updates) {
|
|
|
21
32
|
if (value === null) {
|
|
22
33
|
delete result[key];
|
|
23
34
|
}
|
|
35
|
+
else if (Array.isArray(value)) {
|
|
36
|
+
result[key] = value;
|
|
37
|
+
}
|
|
24
38
|
else if (typeof value === 'object' &&
|
|
25
|
-
!Array.isArray(value) &&
|
|
26
39
|
typeof result[key] === 'object' &&
|
|
27
40
|
result[key] !== null &&
|
|
28
41
|
!Array.isArray(result[key])) {
|
|
@@ -56,7 +69,9 @@ export function buildWorkingMemoryPrompt(currentState, jsonSchema) {
|
|
|
56
69
|
parts.push('When you learn new information, output a partial JSON update in <working_memory> tags. ' +
|
|
57
70
|
'Only include durable facts you have actually derived or the user has confirmed. ' +
|
|
58
71
|
'Do not output templates, placeholders, or narration. ' +
|
|
59
|
-
'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it.'
|
|
72
|
+
'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it. ' +
|
|
73
|
+
'Arrays are replaced, not merged: to change one, repeat every item you want to keep alongside the new ones, ' +
|
|
74
|
+
'because any item you leave out is deleted.');
|
|
60
75
|
return parts.join('\n\n');
|
|
61
76
|
}
|
|
62
77
|
export async function loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema) {
|
|
@@ -203,6 +203,51 @@ async function postStreamCleanup(persistingChannel, agentRunState, runId, run) {
|
|
|
203
203
|
usage: persistingChannel.totalUsage,
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Adapts `modifyOutputStream` hooks to channel middleware, one closure per
|
|
208
|
+
* hook so each keeps its own `state` and event log for the whole run.
|
|
209
|
+
*/
|
|
210
|
+
const toChannelStreamMiddleware = (hooks, sharedNotes, signal) => hooks
|
|
211
|
+
.filter((mw) => mw.modifyOutputStream)
|
|
212
|
+
.map((mw) => {
|
|
213
|
+
const state = {};
|
|
214
|
+
const allEvents = [];
|
|
215
|
+
return async (services, event, next) => {
|
|
216
|
+
allEvents.push(event);
|
|
217
|
+
const result = await mw.modifyOutputStream(services, {
|
|
218
|
+
event,
|
|
219
|
+
allEvents,
|
|
220
|
+
state,
|
|
221
|
+
shared: sharedNotes,
|
|
222
|
+
// Sends downstream directly, so a hook can hand back the fast event
|
|
223
|
+
// now and push the slow one when it is ready.
|
|
224
|
+
emit: next,
|
|
225
|
+
signal,
|
|
226
|
+
});
|
|
227
|
+
if (result == null)
|
|
228
|
+
return;
|
|
229
|
+
if (Array.isArray(result)) {
|
|
230
|
+
for (const r of result)
|
|
231
|
+
await next(r);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
await next(result);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* The branch a delegating parent's own spoken text is routed down once it has
|
|
240
|
+
* handed off: the same working-memory hook instances as the main chain, so the
|
|
241
|
+
* in-band `<working_memory>` blocks are still collected, ending in a sink so
|
|
242
|
+
* neither the client, the thread history, nor user channel middleware sees
|
|
243
|
+
* text the parent was supposed to keep to itself.
|
|
244
|
+
*
|
|
245
|
+
* Routing at ingress rather than dropping further down the chain keeps the
|
|
246
|
+
* decision synchronous with the send: the working-memory hook is async, so a
|
|
247
|
+
* hand-off landing mid-flight would otherwise retroactively suppress text the
|
|
248
|
+
* parent spoke before it.
|
|
249
|
+
*/
|
|
250
|
+
const createWorkingMemoryOnlyChannel = (channel, services, workingMemoryMiddleware) => wrapChannelWithMiddleware({ channel: { ...channel, send: () => { } } }, services, workingMemoryMiddleware).channel;
|
|
206
251
|
async function runStreamStepLoop(params) {
|
|
207
252
|
const { agent, runnerParams, maxSteps, agentRunner, streamChannel, channel, agentMiddlewares, } = params;
|
|
208
253
|
const singletonServices = getSingletonServices();
|
|
@@ -446,13 +491,14 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
446
491
|
channel.send({ type: 'done' });
|
|
447
492
|
return '';
|
|
448
493
|
}
|
|
494
|
+
const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
|
|
495
|
+
threadId,
|
|
496
|
+
workingMemorySchemaName,
|
|
497
|
+
logger: singletonServices.logger,
|
|
498
|
+
schemaService: singletonServices.schema,
|
|
499
|
+
});
|
|
449
500
|
const agentMiddlewares = [
|
|
450
|
-
...
|
|
451
|
-
threadId,
|
|
452
|
-
workingMemorySchemaName,
|
|
453
|
-
logger: singletonServices.logger,
|
|
454
|
-
schemaService: singletonServices.schema,
|
|
455
|
-
}),
|
|
501
|
+
...workingMemoryMiddleware,
|
|
456
502
|
...(agent.agentMiddleware ?? []),
|
|
457
503
|
];
|
|
458
504
|
// One bag per run, shared by every middleware — see PikkuAgentMiddlewareHooks.
|
|
@@ -491,43 +537,25 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
491
537
|
await storage.saveMessages(threadId, [persistedUserMessage]);
|
|
492
538
|
}
|
|
493
539
|
warnUnstreamedOutputHooks(agentName, agentMiddlewares, singletonServices.logger);
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
.map((mw) => {
|
|
497
|
-
const state = {};
|
|
498
|
-
const allEvents = [];
|
|
499
|
-
return async (services, event, next) => {
|
|
500
|
-
allEvents.push(event);
|
|
501
|
-
const result = await mw.modifyOutputStream(services, {
|
|
502
|
-
event,
|
|
503
|
-
allEvents,
|
|
504
|
-
state,
|
|
505
|
-
shared: sharedNotes,
|
|
506
|
-
// Sends downstream directly, so a hook can hand back the fast event
|
|
507
|
-
// now and push the slow one when it is ready.
|
|
508
|
-
emit: next,
|
|
509
|
-
signal: interruptHandle.signal,
|
|
510
|
-
});
|
|
511
|
-
if (result == null)
|
|
512
|
-
return;
|
|
513
|
-
if (Array.isArray(result)) {
|
|
514
|
-
for (const r of result)
|
|
515
|
-
await next(r);
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
await next(result);
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
});
|
|
540
|
+
const workingMemoryStreamMiddleware = toChannelStreamMiddleware(workingMemoryMiddleware, sharedNotes, interruptHandle.signal);
|
|
541
|
+
const streamMiddleware = toChannelStreamMiddleware(agent.agentMiddleware ?? [], sharedNotes, interruptHandle.signal);
|
|
522
542
|
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
|
|
523
543
|
const meta = agentsMeta[resolvedName];
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
544
|
+
const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length;
|
|
545
|
+
const delegateState = { delegated: false };
|
|
546
|
+
if (isDelegateMode) {
|
|
547
|
+
streamContext.delegateState = delegateState;
|
|
548
|
+
}
|
|
549
|
+
const allChannelMiddleware = [
|
|
550
|
+
...workingMemoryStreamMiddleware,
|
|
551
|
+
...combineChannelMiddleware('agent', `stream:${agentName}`, {
|
|
552
|
+
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
553
|
+
wireChannelMiddleware: [
|
|
554
|
+
...(agent.channelMiddleware ?? []),
|
|
555
|
+
...streamMiddleware,
|
|
556
|
+
],
|
|
557
|
+
}),
|
|
558
|
+
];
|
|
531
559
|
const persistingChannel = createPersistingChannel(channel, storage, threadId, singletonServices.logger);
|
|
532
560
|
const wrappedChannel = allChannelMiddleware.length > 0
|
|
533
561
|
? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
|
|
@@ -545,21 +573,19 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
|
|
|
545
573
|
return wrappedChannel.send(event);
|
|
546
574
|
},
|
|
547
575
|
};
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
streamContext.delegateState = delegateState;
|
|
552
|
-
}
|
|
576
|
+
const workingMemoryOnlyChannel = isDelegateMode && workingMemoryStreamMiddleware.length > 0
|
|
577
|
+
? createWorkingMemoryOnlyChannel(channel, singletonServices, workingMemoryStreamMiddleware)
|
|
578
|
+
: undefined;
|
|
553
579
|
const outputChannel = isDelegateMode
|
|
554
580
|
? {
|
|
555
581
|
...credentialFilteredChannel,
|
|
556
582
|
send: (event) => {
|
|
557
583
|
if (delegateState.delegated &&
|
|
558
|
-
(event.type === 'text-delta' || event.type === 'reasoning-delta'))
|
|
559
|
-
return;
|
|
584
|
+
(event.type === 'text-delta' || event.type === 'reasoning-delta')) {
|
|
585
|
+
return workingMemoryOnlyChannel?.send(event);
|
|
586
|
+
}
|
|
560
587
|
return credentialFilteredChannel.send(event);
|
|
561
588
|
},
|
|
562
|
-
delegateState,
|
|
563
589
|
}
|
|
564
590
|
: credentialFilteredChannel;
|
|
565
591
|
try {
|
|
@@ -880,13 +906,14 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
880
906
|
const allMessages = [...contextMessages, ...messages];
|
|
881
907
|
const trimmedMessages = trimMessages(allMessages);
|
|
882
908
|
const instructions = await buildInstructions(resolvedName, packageName);
|
|
909
|
+
const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
|
|
910
|
+
threadId: run.threadId,
|
|
911
|
+
workingMemorySchemaName,
|
|
912
|
+
logger: singletonServices.logger,
|
|
913
|
+
schemaService: singletonServices.schema,
|
|
914
|
+
});
|
|
883
915
|
const agentMiddlewares = [
|
|
884
|
-
...
|
|
885
|
-
threadId: run.threadId,
|
|
886
|
-
workingMemorySchemaName,
|
|
887
|
-
logger: singletonServices.logger,
|
|
888
|
-
schemaService: singletonServices.schema,
|
|
889
|
-
}),
|
|
916
|
+
...workingMemoryMiddleware,
|
|
890
917
|
...(agent.agentMiddleware ?? []),
|
|
891
918
|
];
|
|
892
919
|
// One bag per run, shared by every middleware — see PikkuAgentMiddlewareHooks.
|
|
@@ -895,46 +922,43 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
895
922
|
// knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
|
|
896
923
|
const interruptHandle = registerInterruptibleRun(run.runId);
|
|
897
924
|
warnUnstreamedOutputHooks(run.agentName, agentMiddlewares, singletonServices.logger);
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
// Sends downstream directly, so a hook can hand back the fast event
|
|
911
|
-
// now and push the slow one when it is ready.
|
|
912
|
-
emit: next,
|
|
913
|
-
signal: interruptHandle.signal,
|
|
914
|
-
});
|
|
915
|
-
if (result == null)
|
|
916
|
-
return;
|
|
917
|
-
if (Array.isArray(result)) {
|
|
918
|
-
for (const r of result)
|
|
919
|
-
await next(r);
|
|
920
|
-
}
|
|
921
|
-
else {
|
|
922
|
-
await next(result);
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
});
|
|
926
|
-
const allChannelMiddleware = combineChannelMiddleware('agent', `stream:${run.agentName}`, {
|
|
927
|
-
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
928
|
-
wireChannelMiddleware: [
|
|
929
|
-
...(agent.channelMiddleware ?? []),
|
|
930
|
-
...streamMiddleware,
|
|
931
|
-
],
|
|
932
|
-
});
|
|
925
|
+
const workingMemoryStreamMiddleware = toChannelStreamMiddleware(workingMemoryMiddleware, sharedNotes, interruptHandle.signal);
|
|
926
|
+
const streamMiddleware = toChannelStreamMiddleware(agent.agentMiddleware ?? [], sharedNotes, interruptHandle.signal);
|
|
927
|
+
const allChannelMiddleware = [
|
|
928
|
+
...workingMemoryStreamMiddleware,
|
|
929
|
+
...combineChannelMiddleware('agent', `stream:${run.agentName}`, {
|
|
930
|
+
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
931
|
+
wireChannelMiddleware: [
|
|
932
|
+
...(agent.channelMiddleware ?? []),
|
|
933
|
+
...streamMiddleware,
|
|
934
|
+
],
|
|
935
|
+
}),
|
|
936
|
+
];
|
|
933
937
|
const persistingChannel = createPersistingChannel(channel, storage, run.threadId, singletonServices.logger);
|
|
934
938
|
const wrappedChannel = allChannelMiddleware.length > 0
|
|
935
939
|
? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
|
|
936
940
|
: persistingChannel;
|
|
941
|
+
const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length;
|
|
942
|
+
const delegateState = { delegated: false };
|
|
937
943
|
const streamContext = { channel, options };
|
|
944
|
+
if (isDelegateMode) {
|
|
945
|
+
streamContext.delegateState = delegateState;
|
|
946
|
+
}
|
|
947
|
+
const workingMemoryOnlyChannel = isDelegateMode && workingMemoryStreamMiddleware.length > 0
|
|
948
|
+
? createWorkingMemoryOnlyChannel(channel, singletonServices, workingMemoryStreamMiddleware)
|
|
949
|
+
: undefined;
|
|
950
|
+
const outputChannel = isDelegateMode
|
|
951
|
+
? {
|
|
952
|
+
...wrappedChannel,
|
|
953
|
+
send: (event) => {
|
|
954
|
+
if (delegateState.delegated &&
|
|
955
|
+
(event.type === 'text-delta' || event.type === 'reasoning-delta')) {
|
|
956
|
+
return workingMemoryOnlyChannel?.send(event);
|
|
957
|
+
}
|
|
958
|
+
return wrappedChannel.send(event);
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
: wrappedChannel;
|
|
938
962
|
const resumeTools = (await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, agentMiddlewares)).tools;
|
|
939
963
|
const resolved = resolveModelConfig(resolvedName, agent);
|
|
940
964
|
const maxSteps = resolved.maxSteps ?? 10;
|
|
@@ -959,7 +983,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
959
983
|
runnerParams,
|
|
960
984
|
maxSteps,
|
|
961
985
|
agentRunner,
|
|
962
|
-
streamChannel:
|
|
986
|
+
streamChannel: outputChannel,
|
|
963
987
|
persistingChannel,
|
|
964
988
|
channel,
|
|
965
989
|
agentMiddlewares,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
|
|
2
2
|
import type { CorePikkuMiddleware, MiddlewareMetadata } from '../../middleware/middleware.types.js';
|
|
3
|
+
import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
3
4
|
import type { PermissionMetadata } from '../../function/function-meta.types.js';
|
|
4
5
|
import type { AIProviderOptions } from '../../services/agent-runner-service.js';
|
|
5
6
|
import type { PikkuChannel } from '../channel/channel.types.js';
|
|
@@ -169,8 +170,8 @@ export interface AgentToolDef extends Partial<ApprovalPolicy> {
|
|
|
169
170
|
*/
|
|
170
171
|
forwardsApproval?: boolean;
|
|
171
172
|
}
|
|
172
|
-
export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>,
|
|
173
|
-
modifyInput?: (services:
|
|
173
|
+
export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, SingletonServices extends CoreSingletonServices = CoreSingletonServices> {
|
|
174
|
+
modifyInput?: (services: SingletonServices, ctx: {
|
|
174
175
|
messages: AgentMessage[];
|
|
175
176
|
instructions: string;
|
|
176
177
|
/**
|
|
@@ -197,7 +198,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
197
198
|
messages: AgentMessage[];
|
|
198
199
|
instructions: string;
|
|
199
200
|
};
|
|
200
|
-
modifyOutputStream?: (services:
|
|
201
|
+
modifyOutputStream?: (services: SingletonServices, ctx: {
|
|
201
202
|
event: AgentStreamEvent;
|
|
202
203
|
allEvents: readonly AgentStreamEvent[];
|
|
203
204
|
/** Private to this middleware, for this run. Cross-middleware facts go in `shared`. */
|
|
@@ -241,7 +242,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
241
242
|
* and handed to anything that grades the run, and scrubbing the reply alone
|
|
242
243
|
* leaves them untouched.
|
|
243
244
|
*/
|
|
244
|
-
modifyOutput?: (services:
|
|
245
|
+
modifyOutput?: (services: SingletonServices, ctx: {
|
|
245
246
|
text: string;
|
|
246
247
|
messages: AgentMessage[];
|
|
247
248
|
usage: {
|
|
@@ -258,7 +259,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
258
259
|
messages: AgentMessage[];
|
|
259
260
|
toolCalls?: NonNullable<AgentStep['toolCalls']>;
|
|
260
261
|
};
|
|
261
|
-
beforeToolCall?: (services:
|
|
262
|
+
beforeToolCall?: (services: SingletonServices, ctx: {
|
|
262
263
|
toolName: string;
|
|
263
264
|
toolCallId: string;
|
|
264
265
|
args: Record<string, unknown>;
|
|
@@ -267,7 +268,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
267
268
|
} | void> | {
|
|
268
269
|
args: Record<string, unknown>;
|
|
269
270
|
} | void;
|
|
270
|
-
afterToolCall?: (services:
|
|
271
|
+
afterToolCall?: (services: SingletonServices, ctx: {
|
|
271
272
|
toolName: string;
|
|
272
273
|
toolCallId: string;
|
|
273
274
|
args: Record<string, unknown>;
|
|
@@ -278,7 +279,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
278
279
|
} | void> | {
|
|
279
280
|
result: unknown;
|
|
280
281
|
} | void;
|
|
281
|
-
afterStep?: (services:
|
|
282
|
+
afterStep?: (services: SingletonServices, ctx: {
|
|
282
283
|
stepNumber: number;
|
|
283
284
|
text: string;
|
|
284
285
|
toolCalls: {
|
|
@@ -299,7 +300,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
|
|
|
299
300
|
};
|
|
300
301
|
finishReason: string;
|
|
301
302
|
}) => Promise<void> | void;
|
|
302
|
-
onError?: (services:
|
|
303
|
+
onError?: (services: SingletonServices, ctx: {
|
|
303
304
|
error: Error;
|
|
304
305
|
stepNumber: number;
|
|
305
306
|
messages: AgentMessage[];
|
|
@@ -24,7 +24,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
|
|
|
24
24
|
description: def.description,
|
|
25
25
|
variableId: def.variableId,
|
|
26
26
|
schema: def.schema,
|
|
27
|
-
|
|
27
|
+
optional: def.optional,
|
|
28
28
|
docsUrl: def.docsUrl,
|
|
29
29
|
sourceFile: def.sourceFile,
|
|
30
30
|
};
|
|
@@ -39,7 +39,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
|
|
|
39
39
|
description: def.description,
|
|
40
40
|
variableId: def.variableId,
|
|
41
41
|
schema: def.schema,
|
|
42
|
-
|
|
42
|
+
optional: def.optional,
|
|
43
43
|
docsUrl: def.docsUrl,
|
|
44
44
|
sourceFile: def.sourceFile,
|
|
45
45
|
};
|
|
@@ -5,13 +5,19 @@ export type CoreVariable<T = unknown> = {
|
|
|
5
5
|
variableId: string;
|
|
6
6
|
schema: T;
|
|
7
7
|
/**
|
|
8
|
-
* A variable is
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* A variable is REQUIRED by default, and marking it `optional` is how a
|
|
9
|
+
* declaration says its absence is a supported state. Same flag, same
|
|
10
|
+
* polarity and same meaning as `CoreSecret.optional` — one word to learn
|
|
11
|
+
* rather than two with opposite senses.
|
|
12
|
+
*
|
|
13
|
+
* Defaulting to required rather than following `variables.get`'s
|
|
14
|
+
* `T | undefined` return is deliberate. That signature describes what a
|
|
15
|
+
* caller must HANDLE, not whether a deployment is correct without the value:
|
|
16
|
+
* an undefined feature flag is fine, an undefined API base URL is an outage
|
|
17
|
+
* that the type system cannot tell apart. Declaring the difference is the
|
|
18
|
+
* point of the flag, and the safe default for an undeclared one is to ask.
|
|
13
19
|
*/
|
|
14
|
-
|
|
20
|
+
optional?: boolean;
|
|
15
21
|
docsUrl?: string;
|
|
16
22
|
};
|
|
17
23
|
export type VariableDefinitionMeta = {
|
|
@@ -20,7 +26,7 @@ export type VariableDefinitionMeta = {
|
|
|
20
26
|
description?: string;
|
|
21
27
|
variableId: string;
|
|
22
28
|
schema?: Record<string, unknown> | string;
|
|
23
|
-
|
|
29
|
+
optional?: boolean;
|
|
24
30
|
docsUrl?: string;
|
|
25
31
|
sourceFile?: string;
|
|
26
32
|
};
|
|
@@ -6,8 +6,8 @@ import type { ScenarioPersonas } from '../../services/personas-service.js';
|
|
|
6
6
|
import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } from './scenario-step.types.js';
|
|
7
7
|
import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
|
|
8
8
|
export { addFeature, resolveFeatureScenarios } from './feature.js';
|
|
9
|
-
export type
|
|
10
|
-
export type
|
|
9
|
+
export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
|
|
10
|
+
export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, } from './scenario-run.types.js';
|
|
11
11
|
export { SCENARIO_SURFACES } from './scenario-step.types.js';
|
|
12
12
|
export { resolveScenarioSurfaces } from './scenario-surface.js';
|
|
13
13
|
export { pollUntil, type PollOptions } from './scenario-poll.js';
|
|
@@ -105,12 +105,12 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
105
105
|
*
|
|
106
106
|
* Returns nothing by default so a store that cannot express the query keeps
|
|
107
107
|
* working unchanged — and, because it does not opt in, gains no re-dispatches
|
|
108
|
-
* either. A store must have an atomic `
|
|
109
|
-
* or no concurrency for one to exclude: the relay makes
|
|
110
|
-
* routine, and the claim
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
108
|
+
* either. A store must have an atomic `claimStepForExecution` before
|
|
109
|
+
* overriding this, or no concurrency for one to exclude: the relay makes
|
|
110
|
+
* duplicate dispatch routine, and the claim is what keeps a duplicate from
|
|
111
|
+
* becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
|
|
112
|
+
* status-guarded claim, `in-memory` on being inline and single-process;
|
|
113
|
+
* `mongodb` still qualifies on neither.
|
|
114
114
|
*/
|
|
115
115
|
protected findUndispatchedSteps(_before: Date, _limit: number): Promise<Array<{
|
|
116
116
|
runId: string;
|
|
@@ -176,6 +176,20 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
176
176
|
protected onChildWorkflowFailed(childRun: WorkflowRun, error: Error): Promise<void>;
|
|
177
177
|
private runVersionMismatchFallback;
|
|
178
178
|
executeWorkflowStep(runId: string, stepName: string, rpcName: string, data: any, rpcService: PikkuRPC): Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Take sole ownership of a step before it runs, returning the state to run
|
|
181
|
+
* under — or `null` when another dispatch already owns it.
|
|
182
|
+
*
|
|
183
|
+
* Dispatch is at-least-once by design: the relay re-dispatches steps it
|
|
184
|
+
* believes were dropped, and a queue can redeliver a job it already handed
|
|
185
|
+
* out. This is the one place that keeps a duplicate dispatch from becoming a
|
|
186
|
+
* second execution of a side-effecting step, so it is only as strong as the
|
|
187
|
+
* exclusion it is built on — and `withStepLock` excludes nothing unless the
|
|
188
|
+
* store backs it with a real primitive. A store able to express the decision
|
|
189
|
+
* as one conditional write should override this rather than reach for a lock,
|
|
190
|
+
* which is what `@pikku/kysely` does with a status-guarded `UPDATE`.
|
|
191
|
+
*/
|
|
192
|
+
protected claimStepForExecution(runId: string, stepName: string, rpcName: string): Promise<StepState | null>;
|
|
179
193
|
private executeWorkflowStepInner;
|
|
180
194
|
orchestrateWorkflow(runId: string, rpcService: PikkuRPC): Promise<void>;
|
|
181
195
|
private verifyQueueService;
|