@deepstrike/sdk 0.2.34 → 0.2.36
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 +12 -6
- package/dist/providers/anthropic.js +4 -0
- package/dist/providers/openai.js +8 -1
- package/dist/runtime/kernel-step.d.ts +2 -0
- package/dist/runtime/large-result-spool.d.ts +7 -0
- package/dist/runtime/large-result-spool.js +25 -0
- package/dist/runtime/runner.d.ts +123 -7
- package/dist/runtime/runner.js +333 -113
- package/dist/runtime/sub-agent-orchestrator.js +9 -1
- package/dist/types/agent.d.ts +12 -1
- package/dist/types.d.ts +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/kongusen/deepstrike">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/kongusen/deepstrike/main/docs/public/banner.png" alt="DeepStrike" width="420" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# DeepStrike Node.js SDK
|
|
2
8
|
|
|
3
9
|
Runtime framework built on a Rust kernel. The kernel owns loop control, context compression, governance, signal routing, and memory paging — the SDK owns all I/O (LLM calls, tool execution, disk, long-term memory).
|
|
@@ -181,7 +187,7 @@ The mechanisms above are not internal refactors — they change what you can bui
|
|
|
181
187
|
Tool calls, spawns, compression, and signals pass through one kernel gate with an explicit lifecycle (Ready / Running / Blocked / Suspended). You implement I/O; the kernel decides *when* and *whether*. Node, Python, and Rust share the same decision path, so `wake(sessionId)` and cross-language tooling see consistent behavior.
|
|
182
188
|
|
|
183
189
|
**Longer, sturdier sessions (Layer-1 spool + semantic page-out)**
|
|
184
|
-
Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore
|
|
190
|
+
Oversized tool results (> 50 KB) stay in context as a preview plus a `.spool/` reference — the model reads the full payload on demand via ordinary file tools. When pressure triggers semantic eviction, the SDK summarizes archived content into `DreamStore`. Long tasks survive token pressure instead of failing mid-run.
|
|
185
191
|
|
|
186
192
|
**Safety and governance by default (OS native profile)**
|
|
187
193
|
Every run loads declarative `governancePolicy` (deny / ask_user / rate-limit / param rules) and in-kernel signal routing (`attentionPolicy`, default queue 64). Dangerous tools, external interrupts, and approval flows are policy — not ad-hoc `if` checks in your handlers.
|
|
@@ -305,7 +311,7 @@ The kernel renders context as four LLM API slots — only **history** is compres
|
|
|
305
311
|
| Slot | Source | Role |
|
|
306
312
|
|------|--------|------|
|
|
307
313
|
| `systemStable` | system partition | Identity, rules — never changes within a run |
|
|
308
|
-
| `systemKnowledge` | knowledge partition |
|
|
314
|
+
| `systemKnowledge` | knowledge partition | Skill bodies, `initialMemory`, host-pinned durable refs — keyed, boundary-evicted, budgeted |
|
|
309
315
|
| `turns[0]` | `task_state` + signals | Goal, plan, progress, compression log, runtime signals |
|
|
310
316
|
| `turns[1..N]` | history | Conversation transcript |
|
|
311
317
|
|
|
@@ -489,15 +495,15 @@ effort: 1
|
|
|
489
495
|
2. Express each as a concise bullet
|
|
490
496
|
```
|
|
491
497
|
|
|
498
|
+
A loaded skill's body is pinned into the durable `knowledge` slot (keyed `skill:<name>`) and its `allowed_tools` narrow the exposed toolset. Activation is **not** permanent: `runner.deactivateSkill(name)` re-widens the toolset at the next provider call and unpins the body at the next boundary, and `skillLeaseTurns` auto-deactivates every skill N turns after it loads — so a long multi-phase run doesn't monotonically accumulate early-phase skills. There is deliberately no model-facing unload (deactivation is host-driven only).
|
|
499
|
+
|
|
492
500
|
---
|
|
493
501
|
|
|
494
502
|
## Knowledge
|
|
495
503
|
|
|
496
|
-
Implement `KnowledgeSource` to connect any RAG system. The kernel injects a `knowledge` meta-tool that the LLM calls on demand. Runtime retrieval results land in **history** as tool results.
|
|
497
|
-
|
|
498
|
-
To inject durable knowledge at startup (Slot 2, cacheable on Anthropic), use `initialMemory` or `runner.pushKnowledge()`.
|
|
504
|
+
Implement `KnowledgeSource` to connect any RAG system. The kernel injects a `knowledge` meta-tool that the LLM calls on demand. Runtime retrieval results land in **history** as tool results (single-use fact content that decays with the compression pyramid) — not in the durable `knowledge` partition.
|
|
499
505
|
|
|
500
|
-
|
|
506
|
+
To inject durable knowledge at startup (Slot 2, cacheable on Anthropic), use `initialMemory` or `runner.pushKnowledge(content, tokens?, { key, pinned })`. A **keyed** entry upserts on a repeated key and can be removed with `runner.removeKnowledge(key)`; both take effect at the next compaction/renewal boundary (where the `system[1]` cache prefix is rewritten anyway). Set `knowledgeBudgetRatio` (default 0.25 of `maxTokens`, 0 disables) to cap the partition — over budget, the oldest unpinned, non-skill entries are evicted at boundaries while `pinned: true` entries survive.
|
|
501
507
|
|
|
502
508
|
```typescript
|
|
503
509
|
const runner = new RuntimeRunner({
|
|
@@ -182,6 +182,9 @@ export class AnthropicProvider {
|
|
|
182
182
|
// cache-heavy turn look tiny and suppress compaction until a 413.
|
|
183
183
|
const inputTokens = uncachedInput + cacheReadTokens + cacheCreationTokens;
|
|
184
184
|
const bySlot = estimateCacheReadBySlot(cacheReadTokens, slotBp);
|
|
185
|
+
// stop_reason is only present on message_delta (the closing frame). `max_tokens` drives
|
|
186
|
+
// the kernel's output-cap recovery; other reasons (end_turn/tool_use) are informational.
|
|
187
|
+
const stopReason = evt.delta?.stop_reason;
|
|
185
188
|
yield {
|
|
186
189
|
type: "usage",
|
|
187
190
|
totalTokens: inputTokens + outputTokens,
|
|
@@ -190,6 +193,7 @@ export class AnthropicProvider {
|
|
|
190
193
|
cacheReadInputTokens: cacheReadTokens,
|
|
191
194
|
cacheCreationInputTokens: cacheCreationTokens,
|
|
192
195
|
...(bySlot ? { cacheReadInputTokensBySlot: bySlot } : {}),
|
|
196
|
+
...(stopReason ? { stopReason } : {}),
|
|
193
197
|
};
|
|
194
198
|
}
|
|
195
199
|
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -250,6 +250,11 @@ export class OpenAIChatProvider {
|
|
|
250
250
|
let inputTokens = 0;
|
|
251
251
|
let outputTokens = 0;
|
|
252
252
|
let cacheReadTokens = 0;
|
|
253
|
+
// Phase 4: OpenAI signals an output-cap truncation via finish_reason="length", which arrives on
|
|
254
|
+
// a `choices` frame separate from the trailing `usage` frame — so capture it and attach it to the
|
|
255
|
+
// usage event the runner reads. The kernel treats "length" as a truncation (== Anthropic
|
|
256
|
+
// "max_tokens"); other reasons ("stop"/"tool_calls") pass through harmlessly.
|
|
257
|
+
let finishReason;
|
|
253
258
|
for await (const chunk of stream) {
|
|
254
259
|
if (chunk.usage) {
|
|
255
260
|
totalTokens = chunk.usage.total_tokens;
|
|
@@ -261,6 +266,8 @@ export class OpenAIChatProvider {
|
|
|
261
266
|
const choice = chunk.choices[0];
|
|
262
267
|
if (!choice)
|
|
263
268
|
continue;
|
|
269
|
+
if (choice.finish_reason)
|
|
270
|
+
finishReason = choice.finish_reason;
|
|
264
271
|
const delta = choice.delta;
|
|
265
272
|
if (!delta)
|
|
266
273
|
continue;
|
|
@@ -317,7 +324,7 @@ export class OpenAIChatProvider {
|
|
|
317
324
|
rememberStream();
|
|
318
325
|
yield* emitPendingToolCalls();
|
|
319
326
|
if (totalTokens > 0)
|
|
320
|
-
yield { type: "usage", totalTokens, inputTokens, outputTokens, ...(cacheReadTokens > 0 ? { cacheReadInputTokens: cacheReadTokens } : {}) };
|
|
327
|
+
yield { type: "usage", totalTokens, inputTokens, outputTokens, ...(cacheReadTokens > 0 ? { cacheReadInputTokens: cacheReadTokens } : {}), ...(finishReason ? { stopReason: finishReason } : {}) };
|
|
321
328
|
}
|
|
322
329
|
/**
|
|
323
330
|
* Default `prompt_cache_key` derived from the cacheable prefix (system prompt +
|
|
@@ -65,6 +65,8 @@ export interface KernelObservation {
|
|
|
65
65
|
phase_id?: string;
|
|
66
66
|
capabilities_unlocked?: string[];
|
|
67
67
|
evidence?: string[];
|
|
68
|
+
removed_keys?: string[];
|
|
69
|
+
tokens_freed?: number;
|
|
68
70
|
reason?: RollbackReason | string;
|
|
69
71
|
agent_id?: string;
|
|
70
72
|
parent_session_id?: string;
|
|
@@ -77,6 +77,13 @@ export declare class LargeResultSpool {
|
|
|
77
77
|
* Read a spooled result back from disk.
|
|
78
78
|
*/
|
|
79
79
|
readSpooledResult(spoolRef: string): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
82
|
+
* `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
|
|
83
|
+
* for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
|
|
84
|
+
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
85
|
+
*/
|
|
86
|
+
findByCallId(callId: string): Promise<string | undefined>;
|
|
80
87
|
/**
|
|
81
88
|
* Clean up old spool files (optional maintenance).
|
|
82
89
|
*/
|
|
@@ -140,6 +140,31 @@ omitted: ${omitted} chars
|
|
|
140
140
|
throw new Error(`Failed to read spooled result: ${error}`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
145
|
+
* `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
|
|
146
|
+
* for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
|
|
147
|
+
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
148
|
+
*/
|
|
149
|
+
async findByCallId(callId) {
|
|
150
|
+
let files;
|
|
151
|
+
try {
|
|
152
|
+
files = await fs.readdir(this.spoolDir);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const prefix = `${callId}-`;
|
|
158
|
+
const match = files.find(f => f.startsWith(prefix) && f.endsWith('.txt'));
|
|
159
|
+
if (!match)
|
|
160
|
+
return undefined;
|
|
161
|
+
try {
|
|
162
|
+
return await fs.readFile(path.join(this.spoolDir, match), 'utf-8');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
143
168
|
/**
|
|
144
169
|
* Clean up old spool files (optional maintenance).
|
|
145
170
|
*/
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LLMProvider, Message, ContentPart, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionRequestEvent, PermissionResponse, AsyncSummarizer, DreamSummarizer } from "../types.js";
|
|
2
2
|
import type { DreamStore, MemoryEntry, MemoryQuery, MemoryWriteRequest } from "../memory/protocols.js";
|
|
3
3
|
import type { KnowledgeSource } from "../knowledge/source.js";
|
|
4
|
-
import type { SignalSource } from "../signals/types.js";
|
|
4
|
+
import type { SignalSource, RuntimeSignalUrgency } from "../signals/types.js";
|
|
5
5
|
import type { SessionLog, SessionEvent } from "./session-log.js";
|
|
6
6
|
import type { ArchiveStore } from "./archive.js";
|
|
7
7
|
import type { ExecutionPlane } from "./execution-plane.js";
|
|
@@ -50,6 +50,18 @@ export interface TurnMetrics {
|
|
|
50
50
|
/** Tokens written to the prompt cache this turn (Anthropic `cache_creation_input_tokens`). */
|
|
51
51
|
cacheCreationTokens: number;
|
|
52
52
|
}
|
|
53
|
+
/** O5: decision returned by `onToolCall` — `block: true` denies this call before it executes; the
|
|
54
|
+
* `reason` is fed back to the model as a governance-denied tool result (so it can redirect). */
|
|
55
|
+
export interface ToolCallHookDecision {
|
|
56
|
+
block?: boolean;
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
/** O5: decision returned by `onToolResult` — `replaceOutput` swaps the result the model (and the
|
|
60
|
+
* session log) sees; `note` is injected into the signal stream (see `injectNote`). */
|
|
61
|
+
export interface ToolResultHookDecision {
|
|
62
|
+
replaceOutput?: string;
|
|
63
|
+
note?: string;
|
|
64
|
+
}
|
|
53
65
|
export interface RuntimeOptions {
|
|
54
66
|
provider: LLMProvider;
|
|
55
67
|
/** M4/G5: cumulative token cap for this run (the kernel's `max_total_tokens`). A workflow node's
|
|
@@ -81,6 +93,10 @@ export interface RuntimeOptions {
|
|
|
81
93
|
preQueryMemory?: (ctx: {
|
|
82
94
|
goal: string;
|
|
83
95
|
runSpec?: AgentRunSpec;
|
|
96
|
+
/** K4: `"initial"` = the once-per-run pre-turn-1 fetch; `"renewal"` = re-fired after a sprint
|
|
97
|
+
* renewal (renewal drops the old history INCLUDING earlier memory hits, so the new sprint
|
|
98
|
+
* gets a fresh recall pass). Hooks that ignore it keep the pre-K4 behavior. */
|
|
99
|
+
phase?: "initial" | "renewal";
|
|
84
100
|
}) => Promise<string[] | undefined> | string[] | undefined;
|
|
85
101
|
systemPrompt?: string;
|
|
86
102
|
initialMemory?: string[];
|
|
@@ -118,6 +134,63 @@ export interface RuntimeOptions {
|
|
|
118
134
|
* and memory-write syscalls are admitted unconditionally (pre-M2 behavior).
|
|
119
135
|
*/
|
|
120
136
|
resourceQuota?: ResourceQuota;
|
|
137
|
+
/**
|
|
138
|
+
* O6: the in-kernel repeat fuse — the hard rungs above the soft no-progress STOP. When the model
|
|
139
|
+
* re-issues the IDENTICAL tool call (same name AND args) `denyAfter` turns in a row, the kernel
|
|
140
|
+
* denies it and feeds a directive note back; at `terminateAfter` the run ends `no_progress`.
|
|
141
|
+
* Same-tool/different-args loops never trip it. Defaults: enabled, denyAfter 5, terminateAfter 8.
|
|
142
|
+
* Pass `false` to disable (e.g. legit fixed-argument polling loops).
|
|
143
|
+
*/
|
|
144
|
+
repeatFuse?: {
|
|
145
|
+
denyAfter?: number;
|
|
146
|
+
terminateAfter?: number;
|
|
147
|
+
} | false;
|
|
148
|
+
/**
|
|
149
|
+
* O4: the turn-end criteria gate (the Stop-hook analog). When the model tries to finish while the
|
|
150
|
+
* run's `criteria` stand, the kernel injects ONE self-check turn ("verify each criterion; continue
|
|
151
|
+
* if any is unmet") before accepting completion. Fires at most once per run; runs without criteria
|
|
152
|
+
* are untouched. Default enabled — set `false` to accept the first finish unconditionally.
|
|
153
|
+
*/
|
|
154
|
+
criteriaGate?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* K2: max share of `maxTokens` the durable knowledge partition may occupy. Exceeding it emits a
|
|
157
|
+
* `knowledge_budget_exceeded` observation (once per cache generation) and evicts the OLDEST
|
|
158
|
+
* unpinned, non-skill entries at the next compaction/renewal boundary until usage fits. Pinned
|
|
159
|
+
* entries and skill pins are never budget-evicted. `0` disables. Default: kernel's 0.25.
|
|
160
|
+
*/
|
|
161
|
+
knowledgeBudgetRatio?: number;
|
|
162
|
+
/**
|
|
163
|
+
* K3: default lease (in turns) for every skill activation. After that many turns the kernel
|
|
164
|
+
* auto-deactivates the skill — toolset re-widens, knowledge pin boundary-swept — exactly like
|
|
165
|
+
* an explicit `deactivateSkill()`. Absent ⇒ activations are permanent (default). A repeat
|
|
166
|
+
* `skill(name)` call refreshes the lease.
|
|
167
|
+
*/
|
|
168
|
+
skillLeaseTurns?: number;
|
|
169
|
+
/**
|
|
170
|
+
* O5 (the PreToolUse-hook analog): called for each kernel-APPROVED tool call just before it
|
|
171
|
+
* executes. Return `{ block: true, reason }` to veto — the call never runs and the reason is fed
|
|
172
|
+
* back to the model as a denied tool result. This is the seam for STATEFUL host policy (count
|
|
173
|
+
* repeats, budget writes per resource, project-specific rules); keep static allow/deny in
|
|
174
|
+
* `governancePolicy`. Errs-open: a throwing hook never blocks the run.
|
|
175
|
+
*/
|
|
176
|
+
onToolCall?: (call: {
|
|
177
|
+
callId: string;
|
|
178
|
+
name: string;
|
|
179
|
+
arguments: string;
|
|
180
|
+
}) => Promise<ToolCallHookDecision | undefined | void> | ToolCallHookDecision | undefined | void;
|
|
181
|
+
/**
|
|
182
|
+
* O5 (the PostToolUse-hook analog): called for each executed tool result before it reaches the
|
|
183
|
+
* kernel. Return `{ replaceOutput }` to swap the result the model sees (redact / annotate), and/or
|
|
184
|
+
* `{ note }` to push a contextual note into the signal stream (same channel as `injectNote` —
|
|
185
|
+
* e.g. "that write was a no-op, stop repeating it"). Errs-open: a throwing hook changes nothing.
|
|
186
|
+
*/
|
|
187
|
+
onToolResult?: (result: {
|
|
188
|
+
callId: string;
|
|
189
|
+
name: string;
|
|
190
|
+
arguments: string;
|
|
191
|
+
output: string;
|
|
192
|
+
isError: boolean;
|
|
193
|
+
}) => Promise<ToolResultHookDecision | undefined | void> | ToolResultHookDecision | undefined | void;
|
|
121
194
|
/**
|
|
122
195
|
* L1 (RunGroup): bind this runner to a governance domain shared by N peer sessions of one logical
|
|
123
196
|
* run. Members pass the same `id` + `budgetStore`; the kernel's run-level token cap is then enforced
|
|
@@ -217,11 +290,17 @@ export declare class RuntimeRunner {
|
|
|
217
290
|
private activeKernel;
|
|
218
291
|
private pendingObservations;
|
|
219
292
|
private currentSessionId;
|
|
293
|
+
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
294
|
+
private injectedSignals;
|
|
295
|
+
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
296
|
+
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
297
|
+
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
298
|
+
private knowledgePushedSkills;
|
|
220
299
|
private nextArchiveStart;
|
|
300
|
+
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
301
|
+
private currentGoal;
|
|
221
302
|
/** Full tool outputs keyed by call_id until Layer-1 spool observations are logged. */
|
|
222
303
|
private pendingSpoolOutputs;
|
|
223
|
-
/** Local cache of paged-out/archived messages for priority memory retrieval. */
|
|
224
|
-
private localPageOutCache;
|
|
225
304
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
226
305
|
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
227
306
|
private pendingAuthoredWorkflows;
|
|
@@ -256,10 +335,22 @@ export declare class RuntimeRunner {
|
|
|
256
335
|
mountMarker(kind: string, id: string, description: string): void;
|
|
257
336
|
/** Unmount a capability by kind + id from the active run. No-op if not running. */
|
|
258
337
|
unmountCapability(kind: string, id: string): void;
|
|
259
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
338
|
+
/** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts).
|
|
339
|
+
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
340
|
+
* compaction/renewal boundary, where the cached system[1] block is rewritten anyway) instead
|
|
341
|
+
* of appending a duplicate. `opts.pinned` exempts the entry from the knowledge-budget sweep. */
|
|
342
|
+
pushKnowledge(message: Message, tokens?: number, opts?: {
|
|
343
|
+
key?: string;
|
|
344
|
+
pinned?: boolean;
|
|
345
|
+
}): void;
|
|
346
|
+
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
347
|
+
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
348
|
+
removeKnowledge(key: string): void;
|
|
349
|
+
/** K3: host-driven skill deactivation (there is deliberately no model-facing unload — it
|
|
350
|
+
* invites thrash). The toolset re-widens at the next provider call; the skill's knowledge pin
|
|
351
|
+
* drops at the next compaction/renewal boundary. A later `skill(name)` call re-activates and
|
|
352
|
+
* re-pins fresh content. Errs-open: not-active is a kernel-side no-op. */
|
|
353
|
+
deactivateSkill(name: string): void;
|
|
263
354
|
/**
|
|
264
355
|
* Spawn an isolated sub-agent via the kernel, run it on the host, and feed the result back.
|
|
265
356
|
* Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
|
|
@@ -359,6 +450,16 @@ export declare class RuntimeRunner {
|
|
|
359
450
|
failed: string[];
|
|
360
451
|
}>;
|
|
361
452
|
interrupt(): void;
|
|
453
|
+
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
454
|
+
* the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
|
|
455
|
+
* as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. Use it to feed
|
|
456
|
+
* host-detected events back to the model mid-run (e.g. "that write was a no-op — stop repeating it")
|
|
457
|
+
* without wiring a full `SignalSource`. `urgency` maps to the kernel disposition ladder: `"normal"`
|
|
458
|
+
* queues for the next boundary (default), `"high"` soft-interrupts, `"critical"` preempts. */
|
|
459
|
+
injectNote(text: string, urgency?: RuntimeSignalUrgency): void;
|
|
460
|
+
/** Injected-note drain shared by the main loop's per-turn poll: injected notes first (FIFO), then
|
|
461
|
+
* the configured `signalSource`. Keeps the two inbound channels on one code path so they never drift. */
|
|
462
|
+
private nextInboundSignal;
|
|
362
463
|
run(req: {
|
|
363
464
|
sessionId: string;
|
|
364
465
|
goal: string;
|
|
@@ -376,7 +477,22 @@ export declare class RuntimeRunner {
|
|
|
376
477
|
dream(agentId: string, nowMs?: number): AsyncIterable<StreamEvent>;
|
|
377
478
|
/** Resolve in-kernel AskUser suspend; returns resume lists and stream events to yield. */
|
|
378
479
|
private resolveKernelSuspend;
|
|
480
|
+
/**
|
|
481
|
+
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
482
|
+
* output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map (a call spooled
|
|
483
|
+
* earlier in the SAME tool-turn, before the session-log write lands), (b) the on-disk result
|
|
484
|
+
* spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
|
|
485
|
+
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
486
|
+
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
487
|
+
*/
|
|
488
|
+
private resolveReadResult;
|
|
379
489
|
private execute;
|
|
490
|
+
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
491
|
+
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
492
|
+
* never pinned into `knowledge`. Called once before turn 1 (`phase: "initial"`) and re-fired
|
|
493
|
+
* after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
|
|
494
|
+
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
495
|
+
private prefetchMemoryIntoHistory;
|
|
380
496
|
private appendObservations;
|
|
381
497
|
private archiveSemanticPageOut;
|
|
382
498
|
private upgradeCompressedSummary;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -5,14 +5,14 @@ import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-rep
|
|
|
5
5
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
6
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
7
7
|
import { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
8
|
-
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, kernelAction, kernelApply, kernelMaybeAction,
|
|
8
|
+
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
9
9
|
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
10
10
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
11
11
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
12
12
|
import { resolveReducer } from "./reducers.js";
|
|
13
13
|
import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
14
14
|
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
15
|
-
import { kernelObservationToSessionEvent
|
|
15
|
+
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
16
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
17
17
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
18
18
|
import { formatToolError } from "../tools/errors.js";
|
|
@@ -25,11 +25,17 @@ export class RuntimeRunner {
|
|
|
25
25
|
activeKernel = null;
|
|
26
26
|
pendingObservations = [];
|
|
27
27
|
currentSessionId = null;
|
|
28
|
+
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
29
|
+
injectedSignals = [];
|
|
30
|
+
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
31
|
+
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
32
|
+
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
33
|
+
knowledgePushedSkills = new Set();
|
|
28
34
|
nextArchiveStart = 0;
|
|
35
|
+
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
36
|
+
currentGoal = "";
|
|
29
37
|
/** Full tool outputs keyed by call_id until Layer-1 spool observations are logged. */
|
|
30
38
|
pendingSpoolOutputs = new Map();
|
|
31
|
-
/** Local cache of paged-out/archived messages for priority memory retrieval. */
|
|
32
|
-
localPageOutCache = [];
|
|
33
39
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
34
40
|
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
35
41
|
pendingAuthoredWorkflows = [];
|
|
@@ -181,6 +187,22 @@ export class RuntimeRunner {
|
|
|
181
187
|
if (groupSpawnsBase !== undefined && groupSpawnsBase > 0) {
|
|
182
188
|
config.group_spawns_base = groupSpawnsBase;
|
|
183
189
|
}
|
|
190
|
+
// O6: tune/disable the in-kernel repeat fuse. `false` disables; an object overrides thresholds.
|
|
191
|
+
// Absent ⇒ kernel defaults (enabled, deny_after=5, terminate_after=8).
|
|
192
|
+
if (this.opts.repeatFuse !== undefined) {
|
|
193
|
+
const rf = this.opts.repeatFuse;
|
|
194
|
+
config.repeat_fuse = rf === false
|
|
195
|
+
? { enabled: false, deny_after: 0, terminate_after: 0 }
|
|
196
|
+
: { enabled: true, deny_after: rf.denyAfter ?? 5, terminate_after: rf.terminateAfter ?? 8 };
|
|
197
|
+
}
|
|
198
|
+
// O4: turn-end criteria gate toggle (absent ⇒ kernel default: enabled).
|
|
199
|
+
if (this.opts.criteriaGate !== undefined) {
|
|
200
|
+
config.criteria_gate = this.opts.criteriaGate;
|
|
201
|
+
}
|
|
202
|
+
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
203
|
+
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
204
|
+
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
205
|
+
}
|
|
184
206
|
kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
185
207
|
}
|
|
186
208
|
async appendMemorySyscallObservations(sessionId, observations) {
|
|
@@ -221,62 +243,39 @@ export class RuntimeRunner {
|
|
|
221
243
|
return;
|
|
222
244
|
kernelApply(this.activeKernel, this.pendingObservations, capabilityCommandUnmount(kind, id));
|
|
223
245
|
}
|
|
224
|
-
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const entries = [];
|
|
230
|
-
for (const req of requests) {
|
|
231
|
-
const query = typeof req.query === "string" ? req.query : "";
|
|
232
|
-
const topK = typeof req.top_k === "number" ? req.top_k : 5;
|
|
233
|
-
if (req.tool === "memory") {
|
|
234
|
-
// Priority search: Local Page-Out Cache (lexical/keyword filter)
|
|
235
|
-
const localHits = this.localPageOutCache.filter(m => typeof m.content === "string" && m.content.toLowerCase().includes(query.toLowerCase())).slice(0, topK);
|
|
236
|
-
for (const hit of localHits) {
|
|
237
|
-
entries.push({
|
|
238
|
-
content: `[local semantic cache] ${hit.role}: ${hit.content}`,
|
|
239
|
-
source: "semantic_cache",
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
// Fall back to dreamStore for the remainder if needed
|
|
243
|
-
const remainingK = topK - entries.length;
|
|
244
|
-
if (remainingK > 0 && this.opts.dreamStore && this.opts.agentId) {
|
|
245
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, query, remainingK);
|
|
246
|
-
for (const hit of hits) {
|
|
247
|
-
entries.push({
|
|
248
|
-
content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`,
|
|
249
|
-
source: "memory",
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
else if (req.tool === "knowledge" && this.opts.knowledgeSource) {
|
|
255
|
-
const snippets = await this.opts.knowledgeSource.retrieve(query, topK);
|
|
256
|
-
for (const snippet of snippets) {
|
|
257
|
-
entries.push({ content: snippet, source: "knowledge" });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (entries.length === 0)
|
|
262
|
-
return;
|
|
263
|
-
kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
|
|
264
|
-
await this.opts.sessionLog.append(sessionId, withCategory({
|
|
265
|
-
kind: "page_in",
|
|
266
|
-
turn: runtime.turn(),
|
|
267
|
-
entry_count: entries.length,
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
/** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts). */
|
|
271
|
-
pushKnowledge(message, tokens) {
|
|
246
|
+
/** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts).
|
|
247
|
+
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
248
|
+
* compaction/renewal boundary, where the cached system[1] block is rewritten anyway) instead
|
|
249
|
+
* of appending a duplicate. `opts.pinned` exempts the entry from the knowledge-budget sweep. */
|
|
250
|
+
pushKnowledge(message, tokens, opts) {
|
|
272
251
|
if (!this.activeKernel)
|
|
273
252
|
return;
|
|
274
253
|
kernelApply(this.activeKernel, this.pendingObservations, {
|
|
275
254
|
kind: "add_knowledge_message",
|
|
276
255
|
content: message.content ?? "",
|
|
277
256
|
tokens: tokens ?? Math.max(1, Math.ceil((message.content?.length ?? 0) / 4)),
|
|
257
|
+
...(opts?.key !== undefined ? { key: opts.key } : {}),
|
|
258
|
+
...(opts?.pinned ? { pinned: true } : {}),
|
|
278
259
|
});
|
|
279
260
|
}
|
|
261
|
+
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
262
|
+
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
263
|
+
removeKnowledge(key) {
|
|
264
|
+
if (!this.activeKernel)
|
|
265
|
+
return;
|
|
266
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "remove_knowledge", key });
|
|
267
|
+
}
|
|
268
|
+
/** K3: host-driven skill deactivation (there is deliberately no model-facing unload — it
|
|
269
|
+
* invites thrash). The toolset re-widens at the next provider call; the skill's knowledge pin
|
|
270
|
+
* drops at the next compaction/renewal boundary. A later `skill(name)` call re-activates and
|
|
271
|
+
* re-pins fresh content. Errs-open: not-active is a kernel-side no-op. */
|
|
272
|
+
deactivateSkill(name) {
|
|
273
|
+
if (!this.activeKernel)
|
|
274
|
+
return;
|
|
275
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
276
|
+
// Re-arm the SDK-side push guard so a re-activation re-pins the content.
|
|
277
|
+
this.knowledgePushedSkills.delete(name);
|
|
278
|
+
}
|
|
280
279
|
/**
|
|
281
280
|
* Spawn an isolated sub-agent via the kernel, run it on the host, and feed the result back.
|
|
282
281
|
* Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
|
|
@@ -571,7 +570,10 @@ export class RuntimeRunner {
|
|
|
571
570
|
if (!source)
|
|
572
571
|
return null;
|
|
573
572
|
while (!batchState.settled) {
|
|
574
|
-
|
|
573
|
+
// O2: injected notes participate in the monitor too, so a host `injectNote` mid-batch is not
|
|
574
|
+
// stranded until the batch settles (the drain order matches `nextInboundSignal`).
|
|
575
|
+
const sig = this.injectedSignals.shift()
|
|
576
|
+
?? await source.nextSignal(this.currentSessionId ?? undefined);
|
|
575
577
|
if (batchState.settled)
|
|
576
578
|
break;
|
|
577
579
|
if (!sig) {
|
|
@@ -699,6 +701,30 @@ export class RuntimeRunner {
|
|
|
699
701
|
return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions, sessionId });
|
|
700
702
|
}
|
|
701
703
|
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
704
|
+
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
705
|
+
* the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
|
|
706
|
+
* as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. Use it to feed
|
|
707
|
+
* host-detected events back to the model mid-run (e.g. "that write was a no-op — stop repeating it")
|
|
708
|
+
* without wiring a full `SignalSource`. `urgency` maps to the kernel disposition ladder: `"normal"`
|
|
709
|
+
* queues for the next boundary (default), `"high"` soft-interrupts, `"critical"` preempts. */
|
|
710
|
+
injectNote(text, urgency = "normal") {
|
|
711
|
+
this.injectedSignals.push({
|
|
712
|
+
source: "custom",
|
|
713
|
+
signalType: "event",
|
|
714
|
+
urgency,
|
|
715
|
+
payload: { goal: text },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/** Injected-note drain shared by the main loop's per-turn poll: injected notes first (FIFO), then
|
|
719
|
+
* the configured `signalSource`. Keeps the two inbound channels on one code path so they never drift. */
|
|
720
|
+
async nextInboundSignal() {
|
|
721
|
+
const injected = this.injectedSignals.shift();
|
|
722
|
+
if (injected)
|
|
723
|
+
return injected;
|
|
724
|
+
if (!this.opts.signalSource)
|
|
725
|
+
return null;
|
|
726
|
+
return this.opts.signalSource.nextSignal(this.currentSessionId ?? undefined);
|
|
727
|
+
}
|
|
702
728
|
async *run(req) {
|
|
703
729
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
704
730
|
const midRun = isMidRun(prior);
|
|
@@ -876,6 +902,65 @@ export class RuntimeRunner {
|
|
|
876
902
|
}
|
|
877
903
|
return { approved, denied, events };
|
|
878
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
907
|
+
* output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map (a call spooled
|
|
908
|
+
* earlier in the SAME tool-turn, before the session-log write lands), (b) the on-disk result
|
|
909
|
+
* spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
|
|
910
|
+
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
911
|
+
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
912
|
+
*/
|
|
913
|
+
async resolveReadResult(sessionId, argsJson) {
|
|
914
|
+
let callId = "";
|
|
915
|
+
let offset = 0;
|
|
916
|
+
let maxBytes = 4000;
|
|
917
|
+
try {
|
|
918
|
+
const args = JSON.parse(argsJson || "{}");
|
|
919
|
+
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
920
|
+
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
921
|
+
offset = args.offset;
|
|
922
|
+
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
923
|
+
maxBytes = args.max_bytes;
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
927
|
+
}
|
|
928
|
+
let full = this.pendingSpoolOutputs.get(callId)?.output;
|
|
929
|
+
if (full === undefined) {
|
|
930
|
+
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
931
|
+
try {
|
|
932
|
+
full = await spool.findByCallId(callId);
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
full = undefined;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (full === undefined) {
|
|
939
|
+
try {
|
|
940
|
+
const events = await this.opts.sessionLog.read(sessionId);
|
|
941
|
+
for (const { event } of events) {
|
|
942
|
+
if (event.kind !== "tool_completed")
|
|
943
|
+
continue;
|
|
944
|
+
const match = event.results.find(r => r.call_id === callId);
|
|
945
|
+
if (match)
|
|
946
|
+
full = match.output;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
full = undefined;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (full === undefined) {
|
|
954
|
+
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
955
|
+
}
|
|
956
|
+
const start = Math.max(0, offset);
|
|
957
|
+
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
958
|
+
const slice = full.slice(start, end);
|
|
959
|
+
return {
|
|
960
|
+
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
961
|
+
isError: false,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
879
964
|
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
880
965
|
this.interrupted = false;
|
|
881
966
|
this.abortController = new AbortController();
|
|
@@ -999,14 +1084,36 @@ export class RuntimeRunner {
|
|
|
999
1084
|
// P1-B B3: rebuild active-skill gating after a wake by re-emitting SkillActivated for each
|
|
1000
1085
|
// `skill` tool call in the replayed history (active_skills is not snapshotted — graceful).
|
|
1001
1086
|
// The catalog (set_available_skills) was already fed above, so allowed_tools resolves.
|
|
1087
|
+
// `knowledge` isn't snapshotted either (same graceful-reset philosophy) — best-effort re-push
|
|
1088
|
+
// the skill's content from its replayed tool_result so the durable copy survives a wake too.
|
|
1089
|
+
const toolResultByCallId = new Map();
|
|
1090
|
+
for (const m of replayed) {
|
|
1091
|
+
for (const part of m.contentParts ?? []) {
|
|
1092
|
+
if (part.type === "tool_result")
|
|
1093
|
+
toolResultByCallId.set(part.callId, part.output);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1002
1096
|
for (const m of replayed) {
|
|
1003
1097
|
for (const tc of m.toolCalls ?? []) {
|
|
1004
1098
|
if (tc.name !== "skill")
|
|
1005
1099
|
continue;
|
|
1006
1100
|
try {
|
|
1007
1101
|
const name = JSON.parse(tc.arguments || "{}").name;
|
|
1008
|
-
if (name)
|
|
1009
|
-
|
|
1102
|
+
if (!name)
|
|
1103
|
+
continue;
|
|
1104
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1105
|
+
kind: "skill_activated",
|
|
1106
|
+
name,
|
|
1107
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1108
|
+
});
|
|
1109
|
+
const output = toolResultByCallId.get(tc.id);
|
|
1110
|
+
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
1111
|
+
this.knowledgePushedSkills.add(name);
|
|
1112
|
+
// K1: keyed — the kernel-side upsert is the authoritative dedup, so a wake re-push
|
|
1113
|
+
// of a skill already pinned live can never double-pin (the in-run Set resets with
|
|
1114
|
+
// each runner instance; the key does not).
|
|
1115
|
+
this.pushKnowledge({ role: "system", content: output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1116
|
+
}
|
|
1010
1117
|
}
|
|
1011
1118
|
catch { /* malformed skill args — skip */ }
|
|
1012
1119
|
}
|
|
@@ -1054,30 +1161,21 @@ export class RuntimeRunner {
|
|
|
1054
1161
|
message: attachmentsToKernelMessage(attachments),
|
|
1055
1162
|
});
|
|
1056
1163
|
}
|
|
1057
|
-
// I4: pre-fetch memory
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
entries.push({ content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`, source: "memory" });
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
if (entries.length > 0) {
|
|
1072
|
-
kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
1164
|
+
// I4: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead of
|
|
1165
|
+
// discovering it via the `memory` tool on turn 3+. Skipped on resumes (already in prior
|
|
1166
|
+
// context) and when dreamStore/agentId is absent.
|
|
1167
|
+
//
|
|
1168
|
+
// Strict dynamic context control: this is single-use retrieval content (facts relevant to
|
|
1169
|
+
// THIS run's goal right now), not a stable method/skill — so it lands in `history` as an
|
|
1170
|
+
// ordinary turn, exactly like a real `memory` tool result would, and decays with the
|
|
1171
|
+
// compression pyramid over subsequent turns instead of pinning itself in `knowledge` forever.
|
|
1172
|
+
this.currentGoal = goal;
|
|
1173
|
+
if (!resumeMidRun) {
|
|
1174
|
+
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1076
1175
|
}
|
|
1077
1176
|
let action = resumeMidRun
|
|
1078
1177
|
? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
1079
1178
|
: kernelAction(runtime, this.pendingObservations, startPayload);
|
|
1080
|
-
let hasAttemptedReactiveCompact = false;
|
|
1081
1179
|
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
1082
1180
|
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
1083
1181
|
let activeSkill;
|
|
@@ -1089,18 +1187,14 @@ export class RuntimeRunner {
|
|
|
1089
1187
|
// an input" from "the run is still in progress."
|
|
1090
1188
|
try {
|
|
1091
1189
|
while (!runtime.isTerminal()) {
|
|
1092
|
-
// Page-in must run before appendObservations drains pending kernel observations.
|
|
1093
|
-
if (action.kind === "execute_tool") {
|
|
1094
|
-
await this.applyKernelPageIn(runtime, sessionId);
|
|
1095
|
-
}
|
|
1096
1190
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1097
1191
|
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1098
1192
|
if (this.interrupted) {
|
|
1099
1193
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1100
1194
|
break;
|
|
1101
1195
|
}
|
|
1102
|
-
if (this.opts.signalSource) {
|
|
1103
|
-
const sig = await this.
|
|
1196
|
+
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
1197
|
+
const sig = await this.nextInboundSignal();
|
|
1104
1198
|
if (sig) {
|
|
1105
1199
|
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
1106
1200
|
// `signal_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
@@ -1158,7 +1252,7 @@ export class RuntimeRunner {
|
|
|
1158
1252
|
let turnCacheReadTokens = 0;
|
|
1159
1253
|
let turnCacheCreationTokens = 0;
|
|
1160
1254
|
let turnCacheReadBySlot;
|
|
1161
|
-
let
|
|
1255
|
+
let turnStopReason;
|
|
1162
1256
|
const abortSignal = this.abortController?.signal;
|
|
1163
1257
|
try {
|
|
1164
1258
|
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
@@ -1178,6 +1272,10 @@ export class RuntimeRunner {
|
|
|
1178
1272
|
// I1: per-slot attribution forwarded into TurnMetrics. Undefined when the provider
|
|
1179
1273
|
// doesn't honor cache_control (OpenAI-family auto-cache).
|
|
1180
1274
|
turnCacheReadBySlot = usageEvt.cacheReadInputTokensBySlot;
|
|
1275
|
+
// Phase 4: stop_reason drives the kernel's max-output-tokens recovery. The closing
|
|
1276
|
+
// usage frame carries it; keep the last non-empty value seen this turn.
|
|
1277
|
+
if (usageEvt.stopReason)
|
|
1278
|
+
turnStopReason = usageEvt.stopReason;
|
|
1181
1279
|
continue;
|
|
1182
1280
|
}
|
|
1183
1281
|
yield evt;
|
|
@@ -1190,24 +1288,32 @@ export class RuntimeRunner {
|
|
|
1190
1288
|
}
|
|
1191
1289
|
}
|
|
1192
1290
|
catch (err) {
|
|
1193
|
-
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat it as an interrupt
|
|
1194
|
-
// (the loop-top `interrupted` check converts it to a clean `timeout`/UserAbort), not a crash.
|
|
1195
1291
|
if (abortSignal?.aborted) {
|
|
1292
|
+
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat it as an
|
|
1293
|
+
// interrupt (the post-stream `aborted` check below converts it to a clean
|
|
1294
|
+
// timeout/UserAbort), not a crash or a provider error.
|
|
1196
1295
|
this.interrupted = true;
|
|
1197
1296
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1297
|
+
else {
|
|
1298
|
+
// Reactive recovery is now a kernel decision. Forward the raw provider error and
|
|
1299
|
+
// dispatch whatever the kernel returns: `call_provider` to retry with a freshly
|
|
1300
|
+
// compacted context, or `done` to terminate with an honest `ContextOverflow`. The
|
|
1301
|
+
// classify + compact + retry + give-up policy lives in the kernel (one place), not
|
|
1302
|
+
// duplicated across the four SDK runners. `continue` re-enters the loop: a recovered
|
|
1303
|
+
// turn persists its compaction archive via the loop-top appendObservations, and a
|
|
1304
|
+
// terminal `done` exits through `isTerminal()` into the run_terminal emit below.
|
|
1305
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
1306
|
+
kind: "provider_error",
|
|
1307
|
+
message: formatToolError(err),
|
|
1308
|
+
});
|
|
1309
|
+
// Withholding (query.ts parity): surface the raw provider error only when the kernel
|
|
1310
|
+
// could NOT recover (it returned a terminal). On a recovered retry (`call_provider`)
|
|
1311
|
+
// the error stays hidden, so embedders that terminate on `error` events don't see a
|
|
1312
|
+
// phantom failure mid-recovery.
|
|
1313
|
+
if (action.kind === "done") {
|
|
1314
|
+
yield { type: "error", message: formatToolError(err) };
|
|
1205
1315
|
}
|
|
1206
|
-
|
|
1207
|
-
if (!shouldRetry) {
|
|
1208
|
-
yield { type: "error", message: formatToolError(err) };
|
|
1209
|
-
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1210
|
-
break;
|
|
1316
|
+
continue;
|
|
1211
1317
|
}
|
|
1212
1318
|
}
|
|
1213
1319
|
// #2-B-ii: stream aborted (preempt/interrupt) via the break path (provider yielded no error) —
|
|
@@ -1217,14 +1323,6 @@ export class RuntimeRunner {
|
|
|
1217
1323
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1218
1324
|
break;
|
|
1219
1325
|
}
|
|
1220
|
-
if (shouldRetry) {
|
|
1221
|
-
action = {
|
|
1222
|
-
kind: "call_provider",
|
|
1223
|
-
context: runtime.render(),
|
|
1224
|
-
tools,
|
|
1225
|
-
};
|
|
1226
|
-
continue;
|
|
1227
|
-
}
|
|
1228
1326
|
const assistantMessage = {
|
|
1229
1327
|
role: "assistant",
|
|
1230
1328
|
content: finalText,
|
|
@@ -1237,6 +1335,7 @@ export class RuntimeRunner {
|
|
|
1237
1335
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1238
1336
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1239
1337
|
now_ms: Date.now(),
|
|
1338
|
+
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
1240
1339
|
};
|
|
1241
1340
|
let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
|
|
1242
1341
|
if (!nextAction && this.pendingObservations.some(o => o.kind === "suspended")) {
|
|
@@ -1299,11 +1398,17 @@ export class RuntimeRunner {
|
|
|
1299
1398
|
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1300
1399
|
};
|
|
1301
1400
|
const toolResults = [];
|
|
1302
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1401
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1402
|
+
&& c.name !== "read_result");
|
|
1303
1403
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1304
1404
|
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1305
1405
|
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1306
1406
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1407
|
+
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
1408
|
+
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool
|
|
1409
|
+
// (persisted once the kernel observes `large_result_spooled`), (c) a session-log scan for
|
|
1410
|
+
// the original `tool_completed` event. The kernel only advertises the capability.
|
|
1411
|
+
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
1307
1412
|
for (const call of planCalls) {
|
|
1308
1413
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
1309
1414
|
kernelApply(runtime, this.pendingObservations, {
|
|
@@ -1314,6 +1419,11 @@ export class RuntimeRunner {
|
|
|
1314
1419
|
toolResults.push(result);
|
|
1315
1420
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1316
1421
|
}
|
|
1422
|
+
for (const call of readResultCalls) {
|
|
1423
|
+
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
1424
|
+
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
1425
|
+
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
1426
|
+
}
|
|
1317
1427
|
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
1318
1428
|
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
1319
1429
|
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
@@ -1343,8 +1453,37 @@ export class RuntimeRunner {
|
|
|
1343
1453
|
toolResults.push(result);
|
|
1344
1454
|
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
1345
1455
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1456
|
+
// O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
|
|
1457
|
+
// call. A blocked call never executes; its reason reaches the model as a governance-denied
|
|
1458
|
+
// tool result (the kernel rolls the turn back with the note). Errs-open on hook throw.
|
|
1459
|
+
let executableCalls = normalCalls;
|
|
1460
|
+
if (this.opts.onToolCall) {
|
|
1461
|
+
const allowed = [];
|
|
1462
|
+
for (const call of normalCalls) {
|
|
1463
|
+
let decision;
|
|
1464
|
+
try {
|
|
1465
|
+
decision = await this.opts.onToolCall({ callId: call.id, name: call.name, arguments: call.arguments });
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
decision = undefined;
|
|
1469
|
+
}
|
|
1470
|
+
if (decision?.block) {
|
|
1471
|
+
const reason = decision.reason ?? "blocked by host onToolCall hook";
|
|
1472
|
+
yield { type: "tool_denied", callId: call.id, toolName: call.name, reason };
|
|
1473
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1474
|
+
kind: "tool_denied", turn: runtime.turn(), call_id: call.id, tool_name: call.name, reason,
|
|
1475
|
+
});
|
|
1476
|
+
const out = `blocked by host hook: ${reason}`;
|
|
1477
|
+
toolResults.push({ callId: call.id, output: out, isError: true, errorKind: "governance_denied" });
|
|
1478
|
+
yield { type: "tool_result", callId: call.id, name: call.name, content: out, isError: true };
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
allowed.push(call);
|
|
1482
|
+
}
|
|
1483
|
+
executableCalls = allowed;
|
|
1484
|
+
}
|
|
1485
|
+
if (executableCalls.length > 0) {
|
|
1486
|
+
for await (const evt of this.opts.executionPlane.executeAll(executableCalls, runCtx)) {
|
|
1348
1487
|
yield evt;
|
|
1349
1488
|
if (evt.type === "tool_result") {
|
|
1350
1489
|
const tre = evt;
|
|
@@ -1398,12 +1537,38 @@ export class RuntimeRunner {
|
|
|
1398
1537
|
});
|
|
1399
1538
|
}
|
|
1400
1539
|
}
|
|
1401
|
-
const names =
|
|
1540
|
+
const names = executableCalls.map(c => c.name).join(", ");
|
|
1402
1541
|
kernelApply(runtime, this.pendingObservations, {
|
|
1403
1542
|
kind: "update_task",
|
|
1404
1543
|
update: taskUpdateToKernel({ progress: `Executed tools: ${names}` }),
|
|
1405
1544
|
});
|
|
1406
1545
|
}
|
|
1546
|
+
// O5 (PostToolUse-hook analog): let the host inspect each executed result BEFORE it
|
|
1547
|
+
// reaches the kernel/session-log — replace the output (redact/annotate) and/or push a
|
|
1548
|
+
// contextual note into the signal stream. Errs-open on hook throw.
|
|
1549
|
+
if (this.opts.onToolResult) {
|
|
1550
|
+
for (const r of toolResults) {
|
|
1551
|
+
const call = executableCalls.find(c => c.id === r.callId);
|
|
1552
|
+
if (!call)
|
|
1553
|
+
continue; // plan/submit synthetics and hook-blocked calls are not host results
|
|
1554
|
+
let decision;
|
|
1555
|
+
try {
|
|
1556
|
+
decision = await this.opts.onToolResult({
|
|
1557
|
+
callId: r.callId, name: call.name, arguments: call.arguments,
|
|
1558
|
+
output: r.output, isError: r.isError,
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
catch {
|
|
1562
|
+
decision = undefined;
|
|
1563
|
+
}
|
|
1564
|
+
if (!decision)
|
|
1565
|
+
continue;
|
|
1566
|
+
if (typeof decision.replaceOutput === "string")
|
|
1567
|
+
r.output = decision.replaceOutput;
|
|
1568
|
+
if (decision.note)
|
|
1569
|
+
this.injectNote(decision.note);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1407
1572
|
await this.opts.sessionLog.append(sessionId, {
|
|
1408
1573
|
kind: "tool_completed",
|
|
1409
1574
|
turn: runtime.turn(),
|
|
@@ -1423,6 +1588,13 @@ export class RuntimeRunner {
|
|
|
1423
1588
|
// P1-B B3: a `skill` call that resolved successfully activates that skill in the kernel, so
|
|
1424
1589
|
// the next `call_provider` narrows the toolset to its declared tools. Fed before `tool_results`
|
|
1425
1590
|
// (which computes the next action). Errs-open: a failed/missing skill load doesn't activate.
|
|
1591
|
+
//
|
|
1592
|
+
// Strict dynamic context control: a skill is METHOD content — how to do something — reused
|
|
1593
|
+
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
1594
|
+
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
1595
|
+
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
1596
|
+
// with the compression pyramid like any other tool output — that's fine, the permanent copy
|
|
1597
|
+
// now lives in `knowledge`). First activation only (see `knowledgePushedSkills`).
|
|
1426
1598
|
for (const call of allCalls) {
|
|
1427
1599
|
if (call.name !== "skill")
|
|
1428
1600
|
continue;
|
|
@@ -1431,8 +1603,21 @@ export class RuntimeRunner {
|
|
|
1431
1603
|
continue;
|
|
1432
1604
|
try {
|
|
1433
1605
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
1434
|
-
if (name)
|
|
1435
|
-
|
|
1606
|
+
if (!name)
|
|
1607
|
+
continue;
|
|
1608
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1609
|
+
kind: "skill_activated",
|
|
1610
|
+
name,
|
|
1611
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1612
|
+
});
|
|
1613
|
+
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances
|
|
1614
|
+
// (wake re-push of an already-pinned skill upserts instead of duplicating). With a
|
|
1615
|
+
// lease configured, the Set optimization is skipped: an expired-then-reloaded skill
|
|
1616
|
+
// must re-pin, and only the kernel knows the lease state — its upsert dedupes anyway.
|
|
1617
|
+
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
1618
|
+
this.knowledgePushedSkills.add(name);
|
|
1619
|
+
this.pushKnowledge({ role: "system", content: res.output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1620
|
+
}
|
|
1436
1621
|
}
|
|
1437
1622
|
catch { /* malformed skill args — skip activation */ }
|
|
1438
1623
|
}
|
|
@@ -1558,6 +1743,38 @@ export class RuntimeRunner {
|
|
|
1558
1743
|
this.currentSessionId = null;
|
|
1559
1744
|
this.dashboard = null;
|
|
1560
1745
|
}
|
|
1746
|
+
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
1747
|
+
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
1748
|
+
* never pinned into `knowledge`. Called once before turn 1 (`phase: "initial"`) and re-fired
|
|
1749
|
+
* after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
|
|
1750
|
+
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
1751
|
+
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1752
|
+
if (!this.opts.preQueryMemory || !this.opts.dreamStore || !this.opts.agentId)
|
|
1753
|
+
return;
|
|
1754
|
+
try {
|
|
1755
|
+
const queries = await this.opts.preQueryMemory({
|
|
1756
|
+
goal: this.currentGoal,
|
|
1757
|
+
runSpec: this.opts.runSpec,
|
|
1758
|
+
phase,
|
|
1759
|
+
});
|
|
1760
|
+
const lines = [];
|
|
1761
|
+
for (const q of queries ?? []) {
|
|
1762
|
+
if (typeof q !== "string" || !q.trim())
|
|
1763
|
+
continue;
|
|
1764
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
1765
|
+
for (const hit of hits) {
|
|
1766
|
+
lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.text}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (lines.length > 0) {
|
|
1770
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1771
|
+
kind: "add_history_message",
|
|
1772
|
+
message: { role: "user", content: lines.join("\n") },
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
1777
|
+
}
|
|
1561
1778
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
1562
1779
|
const turn = runtime.turn();
|
|
1563
1780
|
const preservedRefs = runtime.preservedRefs();
|
|
@@ -1580,9 +1797,6 @@ export class RuntimeRunner {
|
|
|
1580
1797
|
}
|
|
1581
1798
|
}
|
|
1582
1799
|
}
|
|
1583
|
-
if (obs.kind === "page_out" && obs.archived) {
|
|
1584
|
-
this.localPageOutCache.push(...obs.archived);
|
|
1585
|
-
}
|
|
1586
1800
|
if (obs.kind === "large_result_spooled") {
|
|
1587
1801
|
const pending = this.pendingSpoolOutputs.get(obs.call_id ?? "");
|
|
1588
1802
|
if (pending) {
|
|
@@ -1624,6 +1838,12 @@ export class RuntimeRunner {
|
|
|
1624
1838
|
&& obs.archived.length > 0) {
|
|
1625
1839
|
void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
|
|
1626
1840
|
}
|
|
1841
|
+
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1842
|
+
// re-run the preQueryMemory prefetch for the new sprint (live observations only: this
|
|
1843
|
+
// consumer sits on the live drain path, same placement as the semantic page-out archival).
|
|
1844
|
+
if (obs.kind === "renewed") {
|
|
1845
|
+
await this.prefetchMemoryIntoHistory(runtime, "renewal");
|
|
1846
|
+
}
|
|
1627
1847
|
}
|
|
1628
1848
|
return nextArchiveStart;
|
|
1629
1849
|
}
|
|
@@ -10,7 +10,9 @@ function terminationFromStatus(status) {
|
|
|
10
10
|
normalized === "timeout" ||
|
|
11
11
|
normalized === "user_abort" ||
|
|
12
12
|
normalized === "error" ||
|
|
13
|
-
normalized === "milestone_exceeded"
|
|
13
|
+
normalized === "milestone_exceeded" ||
|
|
14
|
+
normalized === "context_overflow" ||
|
|
15
|
+
normalized === "no_progress") {
|
|
14
16
|
return normalized;
|
|
15
17
|
}
|
|
16
18
|
return status;
|
|
@@ -87,6 +89,9 @@ export class SubAgentOrchestrator {
|
|
|
87
89
|
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
88
90
|
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
89
91
|
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
92
|
+
// O3: per-child turn / wall-clock caps (fall back to the inherited limits).
|
|
93
|
+
maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
|
|
94
|
+
timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
|
|
90
95
|
executionPlane: execPlane,
|
|
91
96
|
agentId: ctx.spec.identity.agentId,
|
|
92
97
|
systemPrompt,
|
|
@@ -127,6 +132,9 @@ export class SubAgentOrchestrator {
|
|
|
127
132
|
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
128
133
|
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
129
134
|
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
135
|
+
// O3: per-child turn / wall-clock caps (fall back to the inherited limits).
|
|
136
|
+
maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
|
|
137
|
+
timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
|
|
130
138
|
executionPlane: execPlane,
|
|
131
139
|
agentId: ctx.spec.identity.agentId,
|
|
132
140
|
sessionLog: ctx.sessionLog,
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type { Message, ToolSchema } from "../types.js";
|
|
|
2
2
|
export type KernelAgentRole = "explore" | "plan" | "implement" | "verify" | "custom";
|
|
3
3
|
export type AgentIsolation = "shared" | "read_only" | "worktree" | "remote";
|
|
4
4
|
export type ContextInheritance = "none" | "system_only" | "full";
|
|
5
|
-
export type TerminationReason = "completed" | "max_turns" | "token_budget" | "timeout" | "user_abort" | "error" | "milestone_exceeded"
|
|
5
|
+
export type TerminationReason = "completed" | "max_turns" | "token_budget" | "timeout" | "user_abort" | "error" | "milestone_exceeded"
|
|
6
|
+
/** v0.2.35 recovery ladder: compaction exhausted and the prompt still exceeds the provider window. */
|
|
7
|
+
| "context_overflow"
|
|
8
|
+
/** Repeat-fuse escalation: the same tool call (name AND args) re-issued past `terminateAfter` —
|
|
9
|
+
* a stall, distinct from `max_turns` which productive runs can also hit. */
|
|
10
|
+
| "no_progress";
|
|
6
11
|
export type MilestonePolicy = "require_verifier" | "terminate" | "auto_pass";
|
|
7
12
|
export interface AgentIdentity {
|
|
8
13
|
agentId: string;
|
|
@@ -28,6 +33,12 @@ export interface AgentRunSpec {
|
|
|
28
33
|
modelHint?: string;
|
|
29
34
|
/** M4/G5: cumulative token cap for this sub-agent's run (sets the child kernel's `maxTotalTokens`). */
|
|
30
35
|
tokenBudget?: number;
|
|
36
|
+
/** O3: per-child turn cap (sets the child runner's `maxTurns`; falls back to the parent's). A child
|
|
37
|
+
* that exhausts it terminates `max_turns` — the parent reads the termination and decides retry/skip. */
|
|
38
|
+
maxTurns?: number;
|
|
39
|
+
/** O3: per-child wall-clock cap in milliseconds (sets the child runner's `timeoutMs`; falls back to
|
|
40
|
+
* the parent's). A hung child terminates `timeout` instead of stalling the parent indefinitely. */
|
|
41
|
+
maxWallMs?: number;
|
|
31
42
|
}
|
|
32
43
|
/** Kernel process-table observation (Phase 3 canonical spawn signal). */
|
|
33
44
|
export interface AgentProcessChangedObservation {
|
package/dist/types.d.ts
CHANGED
|
@@ -93,6 +93,10 @@ export interface UsageEvent extends StreamEvent {
|
|
|
93
93
|
tools?: number;
|
|
94
94
|
messages?: number;
|
|
95
95
|
};
|
|
96
|
+
/** Provider stop reason for the response — `max_tokens` (Anthropic) / `length` (OpenAI) flag an
|
|
97
|
+
* output-cap truncation, which drives the kernel's max-output-tokens recovery. Absent when the
|
|
98
|
+
* provider doesn't report one. */
|
|
99
|
+
stopReason?: string;
|
|
96
100
|
}
|
|
97
101
|
export type ToolChunk = string | {
|
|
98
102
|
type: "text";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
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.36",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|