@deepstrike/sdk 0.2.41 → 0.2.43
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/README.md +2 -2
- package/dist/governance.d.ts +4 -5
- package/dist/harness/harness.d.ts +7 -0
- package/dist/harness/harness.js +1 -0
- package/dist/kernel.d.ts +5 -0
- package/dist/runtime/kernel-step.d.ts +3 -0
- package/dist/runtime/runner.d.ts +28 -1
- package/dist/runtime/runner.js +199 -77
- package/dist/types/agent.d.ts +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -245,7 +245,7 @@ A node's `kind` selects the control-flow shape; the same executor drives them al
|
|
|
245
245
|
|
|
246
246
|
### 0.2.11 capabilities
|
|
247
247
|
|
|
248
|
-
- **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`.
|
|
248
|
+
- **Runtime fan-out** — give a node the `submitWorkflowNodesTool` and its agent can append nodes to the live DAG mid-run (true loop-until-done; one verifier per claim it discovers). Recorded and replayed on `resumeWorkflow`. Governance rejection fails the submitting node instead of acknowledging work that was never appended.
|
|
249
249
|
- **Quarantine, no escape** — set `trust: "quarantined"` on a node that reads untrusted content; it's denied write-capable isolation in-kernel, and any nodes it submits are coerced to quarantined too (no privilege escalation).
|
|
250
250
|
- **Structured output** — set `outputSchema` on a node; the runner instructs the agent, validates the result against the JSON-Schema subset, and re-runs once with the errors on mismatch. A node that never conforms fails (its dependents starve).
|
|
251
251
|
- **Budget as signal** — with a `maxWorkflowNodes` / `maxConcurrentSubagents` quota installed, each spawned node's goal carries its remaining headroom so a coordinator can size its fan-out to fit.
|
|
@@ -719,7 +719,7 @@ for await (const evt of runner.spawnSubAgent({
|
|
|
719
719
|
}
|
|
720
720
|
```
|
|
721
721
|
|
|
722
|
-
Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back.
|
|
722
|
+
Requires an active parent run (`run()` / `wake()` in progress). The kernel emits `agent_process_changed`; the default `SubAgentOrchestrator` runs the child with a filtered execution plane and feeds `sub_agent_completed` back. If governance rejects the spawn before execution, the iterator yields one `error` event containing the denial reason; the parent turn is not rolled back.
|
|
723
723
|
|
|
724
724
|
---
|
|
725
725
|
|
package/dist/governance.d.ts
CHANGED
|
@@ -30,11 +30,10 @@ export interface GovernancePolicy {
|
|
|
30
30
|
}[];
|
|
31
31
|
constraints?: GovernanceConstraint[];
|
|
32
32
|
/** I5: when true (default), the runner pre-filters denied tools out of the schema passed to the
|
|
33
|
-
* provider — the model never sees them and never tries to call them
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* agent should learn the denial via a real attempt). */
|
|
33
|
+
* provider — the model never sees them and never tries to call them. The denied tool names are
|
|
34
|
+
* also surfaced as a single line on the system slot so the model knows not to plan around them.
|
|
35
|
+
* Set to false when the agent should learn the denial through a real attempted call and its
|
|
36
|
+
* visible error tool result. */
|
|
38
37
|
surfaceDeniedInSystem?: boolean;
|
|
39
38
|
}
|
|
40
39
|
/** I5: walk the tool list and bucket each tool into `allowed` / `denied` based on a declarative
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RuntimeRunner } from "../runtime/runner.js";
|
|
2
2
|
import type { SessionEvent } from "../runtime/session-log.js";
|
|
3
|
+
import type { ContentPart } from "../types.js";
|
|
3
4
|
import type { WorkflowNodeSpec } from "../types/agent.js";
|
|
4
5
|
import type { Criterion, Verdict } from "../runtime/eval.js";
|
|
5
6
|
import type { AttemptJudge, JudgeResult } from "./judge.js";
|
|
@@ -8,6 +9,12 @@ export interface AttemptRequest {
|
|
|
8
9
|
sessionId?: string;
|
|
9
10
|
goal: string;
|
|
10
11
|
criteria?: Criterion[];
|
|
12
|
+
/**
|
|
13
|
+
* Multimodal inputs (images / audio) attached to the task. Forwarded to every attempt
|
|
14
|
+
* unconditionally; the runner seeds them per session idempotently, so fresh-session carries
|
|
15
|
+
* re-seed while same-session carries do not double.
|
|
16
|
+
*/
|
|
17
|
+
attachments?: ContentPart[];
|
|
11
18
|
extensions?: Record<string, unknown>;
|
|
12
19
|
/** Parent transcript inherited by the first attempt only. */
|
|
13
20
|
inheritEvents?: Array<{
|
package/dist/harness/harness.js
CHANGED
|
@@ -14,6 +14,7 @@ export class RuntimeAttemptBody {
|
|
|
14
14
|
sessionId: context.sessionId,
|
|
15
15
|
goal: context.goal,
|
|
16
16
|
criteria: (context.criteria ?? []).map(criterion => criterion.text),
|
|
17
|
+
...(context.attachments?.length ? { attachments: context.attachments } : {}),
|
|
17
18
|
extensions: context.extensions,
|
|
18
19
|
...(context.attempt === 1 && context.inheritEvents
|
|
19
20
|
? { inheritEvents: context.inheritEvents }
|
package/dist/kernel.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface ResourceQuota {
|
|
|
25
25
|
maxTotalSubagents?: number;
|
|
26
26
|
/** Max sub-agent nesting depth (direct children of the root loop are depth 1). */
|
|
27
27
|
maxSpawnDepth?: number;
|
|
28
|
+
/** Max nodes in one in-kernel workflow DAG, including dynamically submitted nodes. */
|
|
29
|
+
maxWorkflowNodes?: number;
|
|
28
30
|
/** Rolling-window memory-write rate limit: at most `maxWrites` per any `windowMs` span. */
|
|
29
31
|
memoryWritesPerWindow?: MemoryWriteRateLimit;
|
|
30
32
|
}
|
|
@@ -50,6 +52,9 @@ export interface MemoryPolicy {
|
|
|
50
52
|
maxContentBytes?: number;
|
|
51
53
|
/** Override the kernel's `write_memory` name-length limit. */
|
|
52
54
|
maxNameLength?: number;
|
|
55
|
+
/** M4: recall count at which the kernel emits an advisory (edge-triggered)
|
|
56
|
+
* `promotion_suggested` for a recalled record. Omitted = suggestions disabled. */
|
|
57
|
+
promotionRecallThreshold?: number;
|
|
53
58
|
}
|
|
54
59
|
export interface GovernanceInstance {
|
|
55
60
|
setIdentity(agentId: string, sessionId: string): void;
|
|
@@ -157,6 +157,9 @@ export type KernelRunnerAction = {
|
|
|
157
157
|
};
|
|
158
158
|
export interface KernelObservation {
|
|
159
159
|
kind: string;
|
|
160
|
+
/** control_request_rejected: stable control-plane operation name and optional subject id. */
|
|
161
|
+
operation?: string;
|
|
162
|
+
subject?: string;
|
|
160
163
|
action?: string;
|
|
161
164
|
rho_after?: number;
|
|
162
165
|
sprint?: number;
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -374,6 +374,23 @@ export declare class RuntimeRunner {
|
|
|
374
374
|
private commitKernelAction;
|
|
375
375
|
private persistMemoryToStore;
|
|
376
376
|
private retrieveMemoryFromStore;
|
|
377
|
+
/**
|
|
378
|
+
* T5: run one memory query through the kernel's `query_memory → memory_query_result`
|
|
379
|
+
* effect lifecycle on the given runtime. The kernel injects each routed hit into history
|
|
380
|
+
* itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
|
|
381
|
+
* `promotion_suggested`) from the routed hits — the store stays a pure query.
|
|
382
|
+
*
|
|
383
|
+
* `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
|
|
384
|
+
* prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
|
|
385
|
+
* each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
|
|
386
|
+
*
|
|
387
|
+
* The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
|
|
388
|
+
* must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
|
|
389
|
+
* drain loop where queued observations would not be consumed until the next boundary.
|
|
390
|
+
* Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
|
|
391
|
+
* and discarded for detached syscall runtimes (their kernel is throwaway).
|
|
392
|
+
*/
|
|
393
|
+
private queryMemoryThroughKernel;
|
|
377
394
|
writeMemory(memory: MemoryRecord, opts?: {
|
|
378
395
|
sessionId?: string;
|
|
379
396
|
agentId?: string;
|
|
@@ -394,7 +411,17 @@ export declare class RuntimeRunner {
|
|
|
394
411
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
395
412
|
*/
|
|
396
413
|
private applyKernelPolicies;
|
|
397
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
|
|
416
|
+
* Shared by the main run drain, the prefetch path, and the host memory syscalls so every
|
|
417
|
+
* query route has identical recall + promotion semantics (T5).
|
|
418
|
+
*
|
|
419
|
+
* M3: `memory_recalled` carries the kernel-derived count — the runner never computes
|
|
420
|
+
* `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
|
|
421
|
+
* edge-triggered by the kernel; the runner surfaces it and never auto-pins.
|
|
422
|
+
*/
|
|
423
|
+
private mirrorMemoryLifecycle;
|
|
424
|
+
private consumeMemoryLifecycleObservations;
|
|
398
425
|
/** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
|
|
399
426
|
mountTool(schema: ToolSchema): Promise<void>;
|
|
400
427
|
/** Mount a skill capability on the currently-running kernel runtime. No-op if not running. */
|
package/dist/runtime/runner.js
CHANGED
|
@@ -34,6 +34,25 @@ export function schedulerPolicyToKernel(policy) {
|
|
|
34
34
|
token_cost_weight: policy.tokenCostWeight,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
/** Kernel observation kinds owned by the memory lifecycle consumer (journal + store mirror). */
|
|
38
|
+
function isMemoryLifecycleObservation(obs) {
|
|
39
|
+
return obs.kind === "memory_written"
|
|
40
|
+
|| obs.kind === "memory_queried"
|
|
41
|
+
|| obs.kind === "memory_validation_failed"
|
|
42
|
+
|| obs.kind === "memory_recalled"
|
|
43
|
+
|| obs.kind === "promotion_suggested";
|
|
44
|
+
}
|
|
45
|
+
function controlRequestRejection(observations, operation) {
|
|
46
|
+
const rejected = observations.find(observation => observation.kind === "control_request_rejected"
|
|
47
|
+
&& (!operation || observation.operation === operation));
|
|
48
|
+
if (!rejected)
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
operation: rejected.operation ?? operation ?? "control_request",
|
|
52
|
+
...(rejected.subject ? { subject: rejected.subject } : {}),
|
|
53
|
+
reason: typeof rejected.reason === "string" ? rejected.reason : "request denied",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
37
56
|
function pendingCallIds(action) {
|
|
38
57
|
switch (action.kind) {
|
|
39
58
|
case "call_provider":
|
|
@@ -126,6 +145,66 @@ export class RuntimeRunner {
|
|
|
126
145
|
return (await this.opts.dreamStore.search(agentId, { ...query, top_k: requestedK }))
|
|
127
146
|
.slice(0, requestedK);
|
|
128
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* T5: run one memory query through the kernel's `query_memory → memory_query_result`
|
|
150
|
+
* effect lifecycle on the given runtime. The kernel injects each routed hit into history
|
|
151
|
+
* itself and derives the recall lifecycle (`memory_recalled`, edge-triggered
|
|
152
|
+
* `promotion_suggested`) from the routed hits — the store stays a pure query.
|
|
153
|
+
*
|
|
154
|
+
* `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
|
|
155
|
+
* prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
|
|
156
|
+
* each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
|
|
157
|
+
*
|
|
158
|
+
* The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
|
|
159
|
+
* must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
|
|
160
|
+
* drain loop where queued observations would not be consumed until the next boundary.
|
|
161
|
+
* Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
|
|
162
|
+
* and discarded for detached syscall runtimes (their kernel is throwaway).
|
|
163
|
+
*/
|
|
164
|
+
async queryMemoryThroughKernel(runtime, query, agentId, sessionId, seenRecordIds, leftovers) {
|
|
165
|
+
const observations = [];
|
|
166
|
+
const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, sessionId);
|
|
167
|
+
if (action.kind !== "query_memory") {
|
|
168
|
+
throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
|
|
169
|
+
}
|
|
170
|
+
let hits = [];
|
|
171
|
+
let ioError;
|
|
172
|
+
try {
|
|
173
|
+
hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
|
|
174
|
+
if (seenRecordIds) {
|
|
175
|
+
hits = hits.filter(hit => {
|
|
176
|
+
const id = hit.record.record_id;
|
|
177
|
+
if (seenRecordIds.has(id))
|
|
178
|
+
return false;
|
|
179
|
+
seenRecordIds.add(id);
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
ioError = cause;
|
|
186
|
+
}
|
|
187
|
+
// Close the kernel effect even when the store failed — never leave a dangling query.
|
|
188
|
+
// The result commit resumes the kernel's reasoning path (`resume_after_preload`), which
|
|
189
|
+
// re-emits the next loop action with the routed hits now in history — callers on a live
|
|
190
|
+
// run kernel must adopt it (a query is a preload, not a detached side-channel).
|
|
191
|
+
const resumed = await this.commitKernelMaybeAction(runtime, observations, {
|
|
192
|
+
kind: "memory_query_result",
|
|
193
|
+
effect_id: action.effectId,
|
|
194
|
+
hits,
|
|
195
|
+
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
196
|
+
}, sessionId);
|
|
197
|
+
await this.consumeMemoryLifecycleObservations(sessionId, observations);
|
|
198
|
+
if (leftovers) {
|
|
199
|
+
for (const obs of observations) {
|
|
200
|
+
if (!isMemoryLifecycleObservation(obs))
|
|
201
|
+
leftovers.push(obs);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (ioError)
|
|
205
|
+
throw ioError;
|
|
206
|
+
return { hits, action: resumed };
|
|
207
|
+
}
|
|
129
208
|
async writeMemory(memory, opts = {}) {
|
|
130
209
|
const sessionId = opts.sessionId ?? this.currentSessionId;
|
|
131
210
|
const agentId = opts.agentId ?? this.opts.agentId;
|
|
@@ -136,7 +215,7 @@ export class RuntimeRunner {
|
|
|
136
215
|
const runtime = this.createSyscallRuntime();
|
|
137
216
|
const action = await this.commitKernelMaybeAction(runtime, observations, { kind: "write_memory", memory }, durableSessionId);
|
|
138
217
|
if (!action) {
|
|
139
|
-
await this.
|
|
218
|
+
await this.consumeMemoryLifecycleObservations(sessionId, observations);
|
|
140
219
|
return;
|
|
141
220
|
}
|
|
142
221
|
if (action.kind !== "persist_memory") {
|
|
@@ -154,7 +233,7 @@ export class RuntimeRunner {
|
|
|
154
233
|
effect_id: action.effectId,
|
|
155
234
|
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
156
235
|
}, durableSessionId);
|
|
157
|
-
await this.
|
|
236
|
+
await this.consumeMemoryLifecycleObservations(durableSessionId, observations);
|
|
158
237
|
if (ioError)
|
|
159
238
|
throw ioError;
|
|
160
239
|
}
|
|
@@ -164,29 +243,11 @@ export class RuntimeRunner {
|
|
|
164
243
|
if (!this.opts.dreamStore || !agentId)
|
|
165
244
|
return [];
|
|
166
245
|
const durableSessionId = this.durableSessionId(sessionId);
|
|
167
|
-
|
|
246
|
+
// Detached syscall runtime: the kernel's history injection and resumed action are inert
|
|
247
|
+
// (the kernel is throwaway), but the recall lifecycle is identical to an in-run query
|
|
248
|
+
// (T5 parity).
|
|
168
249
|
const runtime = this.createSyscallRuntime();
|
|
169
|
-
const
|
|
170
|
-
if (action.kind !== "query_memory") {
|
|
171
|
-
throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
|
|
172
|
-
}
|
|
173
|
-
let hits = [];
|
|
174
|
-
let ioError;
|
|
175
|
-
try {
|
|
176
|
-
hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
|
|
177
|
-
}
|
|
178
|
-
catch (cause) {
|
|
179
|
-
ioError = cause;
|
|
180
|
-
}
|
|
181
|
-
await this.commitKernelApply(runtime, observations, {
|
|
182
|
-
kind: "memory_query_result",
|
|
183
|
-
effect_id: action.effectId,
|
|
184
|
-
hits,
|
|
185
|
-
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
186
|
-
}, durableSessionId);
|
|
187
|
-
await this.appendMemorySyscallObservations(durableSessionId, observations);
|
|
188
|
-
if (ioError)
|
|
189
|
-
throw ioError;
|
|
250
|
+
const { hits } = await this.queryMemoryThroughKernel(runtime, query, agentId, durableSessionId);
|
|
190
251
|
await this.logMemoryRetrievalResult(durableSessionId, hits);
|
|
191
252
|
return hits;
|
|
192
253
|
}
|
|
@@ -322,6 +383,7 @@ export class RuntimeRunner {
|
|
|
322
383
|
...(q.maxConcurrentSubagents !== undefined ? { max_concurrent_subagents: q.maxConcurrentSubagents } : {}),
|
|
323
384
|
...(q.maxTotalSubagents !== undefined ? { max_total_subagents: q.maxTotalSubagents } : {}),
|
|
324
385
|
...(q.maxSpawnDepth !== undefined ? { max_spawn_depth: q.maxSpawnDepth } : {}),
|
|
386
|
+
...(q.maxWorkflowNodes !== undefined ? { max_workflow_nodes: q.maxWorkflowNodes } : {}),
|
|
325
387
|
...(q.memoryWritesPerWindow !== undefined
|
|
326
388
|
? { memory_writes_per_window: [q.memoryWritesPerWindow.maxWrites, q.memoryWritesPerWindow.windowMs] }
|
|
327
389
|
: {}),
|
|
@@ -371,14 +433,36 @@ export class RuntimeRunner {
|
|
|
371
433
|
}
|
|
372
434
|
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
373
435
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Mirror one kernel memory-lifecycle observation into the durable store / host callbacks.
|
|
438
|
+
* Shared by the main run drain, the prefetch path, and the host memory syscalls so every
|
|
439
|
+
* query route has identical recall + promotion semantics (T5).
|
|
440
|
+
*
|
|
441
|
+
* M3: `memory_recalled` carries the kernel-derived count — the runner never computes
|
|
442
|
+
* `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
|
|
443
|
+
* edge-triggered by the kernel; the runner surfaces it and never auto-pins.
|
|
444
|
+
*/
|
|
445
|
+
async mirrorMemoryLifecycle(obs) {
|
|
446
|
+
if (obs.kind === "memory_recalled" && obs.recalls?.length) {
|
|
447
|
+
const agentId = this.opts.agentId;
|
|
448
|
+
if (agentId && this.opts.dreamStore?.recordRecall) {
|
|
449
|
+
await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (obs.kind === "promotion_suggested" && obs.record_id) {
|
|
453
|
+
this.opts.onPromotionSuggested?.({
|
|
454
|
+
recordId: obs.record_id,
|
|
455
|
+
recallCount: obs.recall_count ?? 0,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async consumeMemoryLifecycleObservations(sessionId, observations) {
|
|
377
460
|
const turn = this.activeKernel?.turn() ?? 0;
|
|
378
461
|
for (const obs of observations) {
|
|
379
|
-
if (obs
|
|
380
|
-
|
|
381
|
-
|
|
462
|
+
if (!isMemoryLifecycleObservation(obs))
|
|
463
|
+
continue;
|
|
464
|
+
await this.mirrorMemoryLifecycle(obs);
|
|
465
|
+
if (!sessionId)
|
|
382
466
|
continue;
|
|
383
467
|
const event = kernelObservationToSessionEvent(obs, turn);
|
|
384
468
|
if (event)
|
|
@@ -459,8 +543,14 @@ export class RuntimeRunner {
|
|
|
459
543
|
});
|
|
460
544
|
this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
|
|
461
545
|
const spawned = findSpawnProcessObservation(observations);
|
|
462
|
-
if (!spawned)
|
|
546
|
+
if (!spawned) {
|
|
547
|
+
const rejected = controlRequestRejection(observations, "spawn_sub_agent");
|
|
548
|
+
if (rejected) {
|
|
549
|
+
yield { type: "error", message: `spawn_sub_agent denied: ${rejected.reason}` };
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
463
552
|
throw new Error("spawn_sub_agent did not emit agent_process_changed");
|
|
553
|
+
}
|
|
464
554
|
const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
|
|
465
555
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
466
556
|
const result = await orchestrator.run({
|
|
@@ -856,6 +946,11 @@ export class RuntimeRunner {
|
|
|
856
946
|
}
|
|
857
947
|
if (!initialAction)
|
|
858
948
|
return { nodeOutcomes: [], outputs: {} };
|
|
949
|
+
const workflowRejection = controlRequestRejection(observations);
|
|
950
|
+
if (initialAction.kind === "call_provider" && workflowRejection) {
|
|
951
|
+
this.workflowContinuation = initialAction;
|
|
952
|
+
return { nodeOutcomes: [], outputs: {}, rejection: workflowRejection };
|
|
953
|
+
}
|
|
859
954
|
if (initialAction.kind !== "spawn_workflow") {
|
|
860
955
|
throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
|
|
861
956
|
}
|
|
@@ -915,6 +1010,21 @@ export class RuntimeRunner {
|
|
|
915
1010
|
const observationStart = this.pendingObservations.length;
|
|
916
1011
|
const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
917
1012
|
const subObs = this.pendingObservations.slice(observationStart);
|
|
1013
|
+
const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
|
|
1014
|
+
?? (subObs.find(o => o.kind === "nodes_rejected")
|
|
1015
|
+
? { operation: "submit_workflow_nodes", reason: String(subObs.find(o => o.kind === "nodes_rejected")?.reason ?? "request denied") }
|
|
1016
|
+
: undefined);
|
|
1017
|
+
if (rejected) {
|
|
1018
|
+
const denial = `workflow node submission denied: ${rejected.reason}`;
|
|
1019
|
+
result.result = {
|
|
1020
|
+
...result.result,
|
|
1021
|
+
termination: "error",
|
|
1022
|
+
finalMessage: { role: "assistant", content: denial, toolCalls: [] },
|
|
1023
|
+
};
|
|
1024
|
+
outputs.set(result.agentId, denial);
|
|
1025
|
+
if (stableId !== result.agentId)
|
|
1026
|
+
outputs.set(stableId, denial);
|
|
1027
|
+
}
|
|
918
1028
|
if (submitAction?.kind === "spawn_workflow") {
|
|
919
1029
|
nextNodes.push(...submitAction.nodes);
|
|
920
1030
|
budget = submitAction.budget ?? budget;
|
|
@@ -1098,6 +1208,12 @@ export class RuntimeRunner {
|
|
|
1098
1208
|
const runId = midRun && resumedStart?.event.kind === "run_started"
|
|
1099
1209
|
? resumedStart.event.run_id
|
|
1100
1210
|
: crypto.randomUUID();
|
|
1211
|
+
// Idempotent per session: an earlier run's `run_started` already carries these attachments
|
|
1212
|
+
// (same-session retry attempt), so replay reconstructs them — recording and seeding again
|
|
1213
|
+
// would double them in history. Deduping at the append keeps live and replay in agreement.
|
|
1214
|
+
const attachments = req.attachments?.length && !attachmentsAlreadySeeded(prior, req.attachments)
|
|
1215
|
+
? req.attachments
|
|
1216
|
+
: undefined;
|
|
1101
1217
|
if (!midRun) {
|
|
1102
1218
|
await this.opts.sessionLog.append(req.sessionId, {
|
|
1103
1219
|
kind: "run_started",
|
|
@@ -1106,10 +1222,10 @@ export class RuntimeRunner {
|
|
|
1106
1222
|
criteria: req.criteria ?? [],
|
|
1107
1223
|
agent_id: this.opts.agentId,
|
|
1108
1224
|
system_prompt: this.opts.systemPrompt,
|
|
1109
|
-
...(
|
|
1225
|
+
...(attachments ? { attachments } : {}),
|
|
1110
1226
|
});
|
|
1111
1227
|
}
|
|
1112
|
-
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun,
|
|
1228
|
+
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments, runId);
|
|
1113
1229
|
}
|
|
1114
1230
|
async *wake(sessionId, extensions) {
|
|
1115
1231
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
@@ -1356,6 +1472,9 @@ export class RuntimeRunner {
|
|
|
1356
1472
|
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1357
1473
|
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1358
1474
|
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1475
|
+
...(m.promotionRecallThreshold !== undefined
|
|
1476
|
+
? { promotion_recall_threshold: m.promotionRecallThreshold }
|
|
1477
|
+
: {}),
|
|
1359
1478
|
});
|
|
1360
1479
|
}
|
|
1361
1480
|
if (this.opts.knowledgeSource) {
|
|
@@ -1465,21 +1584,24 @@ export class RuntimeRunner {
|
|
|
1465
1584
|
message: attachmentsToKernelMessage(attachments),
|
|
1466
1585
|
});
|
|
1467
1586
|
}
|
|
1468
|
-
// I4: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead of
|
|
1469
|
-
// discovering it via the `memory` tool on turn 3+. Skipped on resumes (already in prior
|
|
1470
|
-
// context) and when dreamStore/agentId is absent.
|
|
1471
|
-
//
|
|
1472
|
-
// Strict dynamic context control: this is single-use retrieval content (facts relevant to
|
|
1473
|
-
// THIS run's goal right now), not a stable method/skill — so it lands in `history` as an
|
|
1474
|
-
// ordinary turn, exactly like a real `memory` tool result would, and decays with the
|
|
1475
|
-
// compression pyramid over subsequent turns instead of pinning itself in `knowledge` forever.
|
|
1476
1587
|
this.currentGoal = goal;
|
|
1477
|
-
if (!resumeMidRun) {
|
|
1478
|
-
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1479
|
-
}
|
|
1480
1588
|
let action = resumeMidRun
|
|
1481
1589
|
? await this.commitKernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
1482
1590
|
: await this.commitKernelAction(runtime, this.pendingObservations, startPayload);
|
|
1591
|
+
// I4/T5: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead
|
|
1592
|
+
// of discovering it via the `memory` tool on turn 3+. It routes through the kernel's
|
|
1593
|
+
// query_memory lifecycle and therefore runs AFTER start_run — a memory query is a kernel
|
|
1594
|
+
// preload whose result resumes the reasoning path, so issuing it pre-run would leave the
|
|
1595
|
+
// kernel Running and start_run would fault. The resumed action from the last query
|
|
1596
|
+
// supersedes start_run's (same pull contract; it renders the injected hits). Hits land in
|
|
1597
|
+
// `history` as ordinary turns — single-use retrieval content that decays with the
|
|
1598
|
+
// compression pyramid, never pinned into `knowledge`. Skipped on resumes (already in
|
|
1599
|
+
// prior context) and when dreamStore/agentId is absent.
|
|
1600
|
+
if (!resumeMidRun) {
|
|
1601
|
+
const resumed = await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1602
|
+
if (resumed)
|
|
1603
|
+
action = resumed;
|
|
1604
|
+
}
|
|
1483
1605
|
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
1484
1606
|
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
1485
1607
|
let activeSkill;
|
|
@@ -1842,7 +1964,7 @@ export class RuntimeRunner {
|
|
|
1842
1964
|
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1843
1965
|
if (spec) {
|
|
1844
1966
|
this.pendingAuthoredWorkflows.push(spec);
|
|
1845
|
-
const out = "workflow
|
|
1967
|
+
const out = "workflow submitted for governance adjudication";
|
|
1846
1968
|
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1847
1969
|
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1848
1970
|
continue;
|
|
@@ -1853,13 +1975,13 @@ export class RuntimeRunner {
|
|
|
1853
1975
|
? parseStartWorkflowArgs(call.arguments)
|
|
1854
1976
|
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
1855
1977
|
yield { type: "workflow_nodes_submitted", nodes };
|
|
1856
|
-
const result = { callId: call.id, output: "submitted", isError: false };
|
|
1978
|
+
const result = { callId: call.id, output: "workflow nodes submitted for parent governance adjudication", isError: false };
|
|
1857
1979
|
toolResults.push(result);
|
|
1858
|
-
yield { type: "tool_result", callId: call.id, content:
|
|
1980
|
+
yield { type: "tool_result", callId: call.id, content: result.output, isError: false };
|
|
1859
1981
|
}
|
|
1860
1982
|
// O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
|
|
1861
|
-
// call. A blocked call never executes; its reason reaches the model as a
|
|
1862
|
-
// tool result
|
|
1983
|
+
// call. A blocked call never executes; its reason reaches the model as a committed
|
|
1984
|
+
// governance-denied tool result. Decision failures are closed
|
|
1863
1985
|
// unless the host explicitly marks this hook advisory with `onToolCallFailure: "open"`.
|
|
1864
1986
|
let executableCalls = normalCalls;
|
|
1865
1987
|
if (this.opts.onToolCall) {
|
|
@@ -2195,7 +2317,7 @@ export class RuntimeRunner {
|
|
|
2195
2317
|
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
2196
2318
|
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
2197
2319
|
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
2198
|
-
return;
|
|
2320
|
+
return undefined;
|
|
2199
2321
|
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
2200
2322
|
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
2201
2323
|
const preQuery = this.opts.preQueryMemory
|
|
@@ -2211,23 +2333,27 @@ export class RuntimeRunner {
|
|
|
2211
2333
|
runSpec: this.opts.runSpec,
|
|
2212
2334
|
phase,
|
|
2213
2335
|
});
|
|
2214
|
-
|
|
2336
|
+
// T5: route each query through the kernel's query_memory lifecycle instead of calling
|
|
2337
|
+
// the store directly — the kernel injects each routed hit into history itself and the
|
|
2338
|
+
// recall lifecycle (recordRecall / promotion) fires exactly like an in-run query.
|
|
2339
|
+
//
|
|
2340
|
+
// One prefetch = one dedupe horizon: a record hit by several short queries recalls and
|
|
2341
|
+
// injects once. A renewal prefetch starts a fresh horizon — renewal dropped the earlier
|
|
2342
|
+
// injection with the old history, so re-exposure is a genuine new recall.
|
|
2343
|
+
const seenRecordIds = new Set();
|
|
2344
|
+
let resumed;
|
|
2215
2345
|
for (const q of queries ?? []) {
|
|
2216
2346
|
if (!q.query.trim())
|
|
2217
2347
|
continue;
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
if (lines.length > 0) {
|
|
2224
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2225
|
-
kind: "add_history_message",
|
|
2226
|
-
message: { role: "user", content: lines.join("\n") },
|
|
2227
|
-
});
|
|
2348
|
+
const { action } = await this.queryMemoryThroughKernel(runtime, q, this.opts.agentId, this.durableSessionId(this.currentSessionId), seenRecordIds, this.pendingObservations);
|
|
2349
|
+
resumed = action ?? resumed;
|
|
2228
2350
|
}
|
|
2351
|
+
// Each memory_query_result resumes the reasoning path and re-emits the pending loop
|
|
2352
|
+
// action; the caller must continue from the LAST one (it renders the injected hits).
|
|
2353
|
+
return resumed;
|
|
2229
2354
|
}
|
|
2230
2355
|
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
2356
|
+
return undefined;
|
|
2231
2357
|
}
|
|
2232
2358
|
async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
|
|
2233
2359
|
const turn = runtime.turn();
|
|
@@ -2248,22 +2374,9 @@ export class RuntimeRunner {
|
|
|
2248
2374
|
});
|
|
2249
2375
|
this.activeGroupBudgetScope = undefined;
|
|
2250
2376
|
}
|
|
2251
|
-
// M3: mirror the kernel's journaled recall lifecycle into the durable store
|
|
2252
|
-
//
|
|
2253
|
-
|
|
2254
|
-
const agentId = this.opts.agentId;
|
|
2255
|
-
if (agentId && this.opts.dreamStore?.recordRecall) {
|
|
2256
|
-
await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
// M4: a recall crossed the promotion threshold. Advisory — surface it for the host/model to
|
|
2260
|
-
// act on (pin or promote to knowledge); the runner does not auto-pin.
|
|
2261
|
-
if (obs.kind === "promotion_suggested" && obs.record_id) {
|
|
2262
|
-
this.opts.onPromotionSuggested?.({
|
|
2263
|
-
recordId: obs.record_id,
|
|
2264
|
-
recallCount: obs.recall_count ?? 0,
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2377
|
+
// M3/M4: mirror the kernel's journaled recall lifecycle into the durable store /
|
|
2378
|
+
// host callbacks (shared consumer — identical semantics on every query route).
|
|
2379
|
+
await this.mirrorMemoryLifecycle(obs);
|
|
2267
2380
|
const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
|
|
2268
2381
|
const event = kernelObservationToSessionEvent(obs, turn, {
|
|
2269
2382
|
nextArchiveStart,
|
|
@@ -2360,6 +2473,15 @@ function isMidRun(events) {
|
|
|
2360
2473
|
}
|
|
2361
2474
|
return lastStarted >= 0 && lastStarted > lastTerminal;
|
|
2362
2475
|
}
|
|
2476
|
+
/**
|
|
2477
|
+
* True when an earlier run in this session already seeded the same attachments. Replay
|
|
2478
|
+
* reconstructs the attachment message from that run's `run_started`, so recording and
|
|
2479
|
+
* live-seeding them again (a same-session retry attempt) would double them in history.
|
|
2480
|
+
*/
|
|
2481
|
+
function attachmentsAlreadySeeded(prior, attachments) {
|
|
2482
|
+
const wanted = JSON.stringify(attachments);
|
|
2483
|
+
return prior.some(({ event }) => event.kind === "run_started" && JSON.stringify(event.attachments ?? []) === wanted);
|
|
2484
|
+
}
|
|
2363
2485
|
/**
|
|
2364
2486
|
* Build a kernel `add_history_message` payload from user attachments: a `user`
|
|
2365
2487
|
* message whose content is the multimodal parts in the kernel's serde shape
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -210,9 +210,17 @@ export interface WorkflowNodeOutcome {
|
|
|
210
210
|
termination?: TerminationReason;
|
|
211
211
|
output?: Message;
|
|
212
212
|
}
|
|
213
|
+
/** A control-plane request rejected before any workflow effect started. */
|
|
214
|
+
export interface ControlRequestRejection {
|
|
215
|
+
operation: string;
|
|
216
|
+
subject?: string;
|
|
217
|
+
reason: string;
|
|
218
|
+
}
|
|
213
219
|
export interface WorkflowOutcome {
|
|
214
220
|
nodeOutcomes: WorkflowNodeOutcome[];
|
|
215
221
|
outputs: Record<string, string>;
|
|
222
|
+
/** Present when the workflow itself was rejected before any node ran. */
|
|
223
|
+
rejection?: ControlRequestRejection;
|
|
216
224
|
}
|
|
217
225
|
export declare function workflowNodeOutcomeFromKernel(raw: KernelWorkflowNodeOutcome): WorkflowNodeOutcome;
|
|
218
226
|
/** Per-node spawn descriptor carried in the `workflow_batch_spawned` observation. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
75
|
-
"@deepstrike/core": "0.2.
|
|
75
|
+
"@deepstrike/core": "0.2.43",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|