@deepstrike/wasm 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 +6 -0
- package/dist/providers/anthropic.js +3 -0
- package/dist/providers/openai.js +11 -1
- 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 +91 -6
- package/dist/runtime/runner.js +296 -102
- package/dist/runtime/sub-agent-orchestrator.js +6 -1
- package/dist/runtime/types/agent.d.ts +10 -1
- package/dist/types.d.ts +2 -1
- 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 WASM SDK
|
|
2
8
|
|
|
3
9
|
Runtime framework built on a Rust kernel compiled to WebAssembly. Runs in browsers, Cloudflare Workers, Deno Deploy, and Vercel Edge — anywhere that supports `fetch` and WASM.
|
|
@@ -250,6 +250,8 @@ export class AnthropicProvider {
|
|
|
250
250
|
const inputTokens = uncachedInput + cacheReadTokens + cacheCreationTokens;
|
|
251
251
|
if (inputTokens > 0 || outputTokens > 0) {
|
|
252
252
|
const bySlot = estimateCacheReadBySlot(cacheReadTokens, slotBp);
|
|
253
|
+
// stop_reason rides on message_delta; "max_tokens" drives output-cap recovery.
|
|
254
|
+
const stopReason = evt.delta?.stop_reason;
|
|
253
255
|
yield {
|
|
254
256
|
type: "usage",
|
|
255
257
|
totalTokens: inputTokens + outputTokens,
|
|
@@ -258,6 +260,7 @@ export class AnthropicProvider {
|
|
|
258
260
|
cacheReadInputTokens: cacheReadTokens,
|
|
259
261
|
cacheCreationInputTokens: cacheCreationTokens,
|
|
260
262
|
...(bySlot ? { cacheReadInputTokensBySlot: bySlot } : {}),
|
|
263
|
+
...(stopReason ? { stopReason } : {}),
|
|
261
264
|
};
|
|
262
265
|
}
|
|
263
266
|
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -37,6 +37,9 @@ export class OpenAIProvider {
|
|
|
37
37
|
const reader = resp.body.getReader();
|
|
38
38
|
const decoder = new TextDecoder();
|
|
39
39
|
let buf = "";
|
|
40
|
+
// Phase 4: OpenAI signals an output-cap truncation via finish_reason="length"; the kernel
|
|
41
|
+
// treats it as a truncation (== Anthropic "max_tokens") and drives output-cap recovery.
|
|
42
|
+
let finishReason;
|
|
40
43
|
while (true) {
|
|
41
44
|
const { done, value } = await reader.read();
|
|
42
45
|
if (done)
|
|
@@ -48,10 +51,17 @@ export class OpenAIProvider {
|
|
|
48
51
|
if (!line.startsWith("data: "))
|
|
49
52
|
continue;
|
|
50
53
|
const data = line.slice(6).trim();
|
|
51
|
-
if (data === "[DONE]")
|
|
54
|
+
if (data === "[DONE]") {
|
|
55
|
+
// Surface an output-cap truncation (finish_reason="length") to the runner/kernel. Emitted
|
|
56
|
+
// as a usage frame (the channel the runner reads stopReason from) before the early return.
|
|
57
|
+
if (finishReason)
|
|
58
|
+
yield { type: "usage", totalTokens: 0, stopReason: finishReason };
|
|
52
59
|
return;
|
|
60
|
+
}
|
|
53
61
|
try {
|
|
54
62
|
const chunk = JSON.parse(data);
|
|
63
|
+
if (chunk.choices?.[0]?.finish_reason)
|
|
64
|
+
finishReason = chunk.choices[0].finish_reason ?? undefined;
|
|
55
65
|
const delta = chunk.choices?.[0]?.delta;
|
|
56
66
|
if (!delta)
|
|
57
67
|
continue;
|
|
@@ -45,5 +45,12 @@ export declare class LargeResultSpool {
|
|
|
45
45
|
processToolResult(result: ToolResult): Promise<SpooledToolResult>;
|
|
46
46
|
persistOutput(callId: string, content: string): Promise<string>;
|
|
47
47
|
readSpooledResult(spoolRef: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
50
|
+
* `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
|
|
51
|
+
* the `.spool/${callId}-*.txt` naming convention; returns `undefined` if nothing was ever
|
|
52
|
+
* spooled for that call.
|
|
53
|
+
*/
|
|
54
|
+
findByCallId(callId: string): Promise<string | undefined>;
|
|
48
55
|
cleanup(maxAgeMs?: number): Promise<number>;
|
|
49
56
|
}
|
|
@@ -113,6 +113,31 @@ omitted: ${omitted} chars
|
|
|
113
113
|
async readSpooledResult(spoolRef) {
|
|
114
114
|
return this.driver.read(spoolRef);
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
118
|
+
* `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
|
|
119
|
+
* the `.spool/${callId}-*.txt` naming convention; returns `undefined` if nothing was ever
|
|
120
|
+
* spooled for that call.
|
|
121
|
+
*/
|
|
122
|
+
async findByCallId(callId) {
|
|
123
|
+
let keys;
|
|
124
|
+
try {
|
|
125
|
+
keys = await this.driver.list();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const prefix = `.spool/${callId}-`;
|
|
131
|
+
const match = keys.find(k => k.startsWith(prefix) && k.endsWith('.txt'));
|
|
132
|
+
if (!match)
|
|
133
|
+
return undefined;
|
|
134
|
+
try {
|
|
135
|
+
return await this.driver.read(match);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
116
141
|
async cleanup(maxAgeMs) {
|
|
117
142
|
const limit = maxAgeMs ?? this.config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
118
143
|
try {
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { LLMProvider, Message, StreamEvent, PermissionRequestEvent, Permiss
|
|
|
2
2
|
import type { ToolSuspendEvent } from "./execution-plane.js";
|
|
3
3
|
import type { DreamStore, DreamResult } from "../memory/index.js";
|
|
4
4
|
import type { KnowledgeSource } from "../knowledge/index.js";
|
|
5
|
-
import type { SignalSource } from "../signals/index.js";
|
|
5
|
+
import type { SignalSource, RuntimeSignal } from "../signals/index.js";
|
|
6
6
|
import type { SessionLog, SessionEvent } from "./session-log.js";
|
|
7
7
|
import type { ExecutionPlane } from "./execution-plane.js";
|
|
8
8
|
import { type GovernancePolicy } from "../governance.js";
|
|
@@ -59,6 +59,16 @@ export interface TurnMetrics {
|
|
|
59
59
|
};
|
|
60
60
|
cacheCreationTokens: number;
|
|
61
61
|
}
|
|
62
|
+
/** O5: decision returned by `onToolCall` — `block: true` denies this call before it executes. */
|
|
63
|
+
export interface ToolCallHookDecision {
|
|
64
|
+
block?: boolean;
|
|
65
|
+
reason?: string;
|
|
66
|
+
}
|
|
67
|
+
/** O5: decision returned by `onToolResult` — replace the output and/or inject a signal note. */
|
|
68
|
+
export interface ToolResultHookDecision {
|
|
69
|
+
replaceOutput?: string;
|
|
70
|
+
note?: string;
|
|
71
|
+
}
|
|
62
72
|
export interface RuntimeOptions {
|
|
63
73
|
provider: LLMProvider;
|
|
64
74
|
/** M1/G3 intelligence routing: resolve a per-node provider from a workflow node's `modelHint`.
|
|
@@ -78,6 +88,8 @@ export interface RuntimeOptions {
|
|
|
78
88
|
* the knowledge partition before turn 1. Requires dreamStore + agentId. */
|
|
79
89
|
preQueryMemory?: (ctx: {
|
|
80
90
|
goal: string;
|
|
91
|
+
/** K4: `"initial"` = pre-turn-1 fetch; `"renewal"` = re-fired after a sprint renewal. */
|
|
92
|
+
phase?: "initial" | "renewal";
|
|
81
93
|
}) => Promise<string[] | undefined> | string[] | undefined;
|
|
82
94
|
systemPrompt?: string;
|
|
83
95
|
initialMemory?: string[];
|
|
@@ -95,6 +107,39 @@ export interface RuntimeOptions {
|
|
|
95
107
|
};
|
|
96
108
|
schedulerBudget?: SchedulerBudget;
|
|
97
109
|
resourceQuota?: ResourceQuota;
|
|
110
|
+
/** O6: the in-kernel repeat fuse — identical tool call (same name AND args) `denyAfter` turns in a
|
|
111
|
+
* row ⇒ deny + directive note; `terminateAfter` ⇒ run ends `no_progress`. Defaults 5/8; `false`
|
|
112
|
+
* disables. Same-tool/different-args loops never trip it. */
|
|
113
|
+
repeatFuse?: {
|
|
114
|
+
denyAfter?: number;
|
|
115
|
+
terminateAfter?: number;
|
|
116
|
+
} | false;
|
|
117
|
+
/** O4: turn-end criteria gate — one kernel-injected self-check turn before accepting completion
|
|
118
|
+
* while `criteria` stand. Default enabled; `false` accepts the first finish unconditionally. */
|
|
119
|
+
criteriaGate?: boolean;
|
|
120
|
+
/** K2: max share of `maxTokens` the durable knowledge partition may occupy. Over budget ⇒
|
|
121
|
+
* warn-once observation + oldest unpinned non-skill entries evicted at the next boundary.
|
|
122
|
+
* Pinned/skill entries are exempt. `0` disables. Default: kernel's 0.25. */
|
|
123
|
+
knowledgeBudgetRatio?: number;
|
|
124
|
+
/** K3: default lease (in turns) for every skill activation — auto-deactivates after N turns
|
|
125
|
+
* (toolset re-widens, knowledge pin boundary-swept). Absent ⇒ permanent (default). */
|
|
126
|
+
skillLeaseTurns?: number;
|
|
127
|
+
/** O5 (PreToolUse-hook analog): stateful host veto over each kernel-approved call; return
|
|
128
|
+
* `{ block: true, reason }` to deny — the reason reaches the model as a denied result. Errs-open. */
|
|
129
|
+
onToolCall?: (call: {
|
|
130
|
+
callId: string;
|
|
131
|
+
name: string;
|
|
132
|
+
arguments: string;
|
|
133
|
+
}) => Promise<ToolCallHookDecision | undefined | void> | ToolCallHookDecision | undefined | void;
|
|
134
|
+
/** O5 (PostToolUse-hook analog): inspect each executed result; `{ replaceOutput }` swaps what the
|
|
135
|
+
* model sees, `{ note }` injects a signal note (the `injectNote` channel). Errs-open. */
|
|
136
|
+
onToolResult?: (result: {
|
|
137
|
+
callId: string;
|
|
138
|
+
name: string;
|
|
139
|
+
arguments: string;
|
|
140
|
+
output: string;
|
|
141
|
+
isError: boolean;
|
|
142
|
+
}) => Promise<ToolResultHookDecision | undefined | void> | ToolResultHookDecision | undefined | void;
|
|
98
143
|
memoryPolicy?: MemoryPolicy;
|
|
99
144
|
tokenizer?: string;
|
|
100
145
|
enablePlanTool?: boolean;
|
|
@@ -140,8 +185,15 @@ export declare class RuntimeRunner {
|
|
|
140
185
|
private pendingObservations;
|
|
141
186
|
private activeKernel;
|
|
142
187
|
private currentSessionId;
|
|
188
|
+
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
189
|
+
private injectedSignals;
|
|
190
|
+
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
191
|
+
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
192
|
+
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
193
|
+
private knowledgePushedSkills;
|
|
194
|
+
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
195
|
+
private currentGoal;
|
|
143
196
|
private nextArchiveStart;
|
|
144
|
-
private localPageOutCache;
|
|
145
197
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
146
198
|
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
147
199
|
private pendingAuthoredWorkflows;
|
|
@@ -149,6 +201,15 @@ export declare class RuntimeRunner {
|
|
|
149
201
|
constructor(opts: RuntimeOptions);
|
|
150
202
|
get hostOptions(): RuntimeOptions;
|
|
151
203
|
interrupt(): void;
|
|
204
|
+
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
205
|
+
* the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
|
|
206
|
+
* as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. `urgency` maps to
|
|
207
|
+
* the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
|
|
208
|
+
* preempts. */
|
|
209
|
+
injectNote(text: string, urgency?: RuntimeSignal["urgency"]): void;
|
|
210
|
+
/** Injected-note drain shared with the main loop's per-turn poll: injected notes first (FIFO), then
|
|
211
|
+
* the configured `signalSource` — one code path so the two inbound channels never drift. */
|
|
212
|
+
private nextInboundSignal;
|
|
152
213
|
run(req: {
|
|
153
214
|
sessionId: string;
|
|
154
215
|
goal: string;
|
|
@@ -160,11 +221,29 @@ export declare class RuntimeRunner {
|
|
|
160
221
|
}>;
|
|
161
222
|
}): AsyncIterable<StreamEvent>;
|
|
162
223
|
wake(sessionId: string, extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
163
|
-
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
224
|
+
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
225
|
+
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
226
|
+
* compaction/renewal boundary) instead of appending a duplicate. `opts.pinned` exempts the
|
|
227
|
+
* entry from the knowledge-budget sweep. */
|
|
228
|
+
pushKnowledge(message: Message, tokens?: number, opts?: {
|
|
229
|
+
key?: string;
|
|
230
|
+
pinned?: boolean;
|
|
231
|
+
}): void;
|
|
232
|
+
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
233
|
+
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
234
|
+
removeKnowledge(key: string): void;
|
|
235
|
+
/** K3: host-driven skill deactivation — toolset re-widens at the next provider call, the
|
|
236
|
+
* skill's knowledge pin drops at the next boundary. Errs-open: not-active is a no-op. */
|
|
237
|
+
deactivateSkill(name: string): void;
|
|
167
238
|
private resolveKernelSuspend;
|
|
239
|
+
/**
|
|
240
|
+
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
241
|
+
* output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map, (b) the result
|
|
242
|
+
* spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
|
|
243
|
+
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
244
|
+
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
245
|
+
*/
|
|
246
|
+
private resolveReadResult;
|
|
168
247
|
dream(agentId: string, nowMs?: number): Promise<DreamResult>;
|
|
169
248
|
private execute;
|
|
170
249
|
spawnSubAgent(spec: AgentRunSpec): Promise<SubAgentResult>;
|
|
@@ -256,6 +335,12 @@ export declare class RuntimeRunner {
|
|
|
256
335
|
failed: string[];
|
|
257
336
|
}>;
|
|
258
337
|
private appendObservations;
|
|
338
|
+
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
339
|
+
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
340
|
+
* never pinned into `knowledge`. `phase: "initial"` = once before turn 1; `phase: "renewal"` =
|
|
341
|
+
* re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
|
|
342
|
+
* hits). Errs-open throughout. */
|
|
343
|
+
private prefetchMemoryIntoHistory;
|
|
259
344
|
private archiveSemanticPageOut;
|
|
260
345
|
}
|
|
261
346
|
export declare function collectText(stream: AsyncIterable<StreamEvent>): Promise<string>;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -5,13 +5,13 @@ import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-rep
|
|
|
5
5
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
6
|
import { formatToolError } from "../tools/errors.js";
|
|
7
7
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
8
|
-
import {
|
|
8
|
+
import { kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, 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
|
-
import { kernelObservationToSessionEvent
|
|
14
|
+
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
15
15
|
import { assertNativeProfile } from "./os-profile.js";
|
|
16
16
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
17
17
|
export class RuntimeRunner {
|
|
@@ -22,8 +22,15 @@ export class RuntimeRunner {
|
|
|
22
22
|
pendingObservations = [];
|
|
23
23
|
activeKernel = null;
|
|
24
24
|
currentSessionId = null;
|
|
25
|
+
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
26
|
+
injectedSignals = [];
|
|
27
|
+
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
28
|
+
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
29
|
+
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
30
|
+
knowledgePushedSkills = new Set();
|
|
31
|
+
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
32
|
+
currentGoal = "";
|
|
25
33
|
nextArchiveStart = 0;
|
|
26
|
-
localPageOutCache = [];
|
|
27
34
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
28
35
|
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
29
36
|
pendingAuthoredWorkflows = [];
|
|
@@ -33,6 +40,24 @@ export class RuntimeRunner {
|
|
|
33
40
|
}
|
|
34
41
|
get hostOptions() { return this.opts; }
|
|
35
42
|
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
43
|
+
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
44
|
+
* the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
|
|
45
|
+
* as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. `urgency` maps to
|
|
46
|
+
* the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
|
|
47
|
+
* preempts. */
|
|
48
|
+
injectNote(text, urgency = "normal") {
|
|
49
|
+
this.injectedSignals.push({ source: "custom", signalType: "event", urgency, payload: { goal: text } });
|
|
50
|
+
}
|
|
51
|
+
/** Injected-note drain shared with the main loop's per-turn poll: injected notes first (FIFO), then
|
|
52
|
+
* the configured `signalSource` — one code path so the two inbound channels never drift. */
|
|
53
|
+
async nextInboundSignal() {
|
|
54
|
+
const injected = this.injectedSignals.shift();
|
|
55
|
+
if (injected)
|
|
56
|
+
return injected;
|
|
57
|
+
if (!this.opts.signalSource)
|
|
58
|
+
return null;
|
|
59
|
+
return this.opts.signalSource.nextSignal();
|
|
60
|
+
}
|
|
36
61
|
async *run(req) {
|
|
37
62
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
38
63
|
const midRun = isMidRun(prior);
|
|
@@ -58,59 +83,35 @@ export class RuntimeRunner {
|
|
|
58
83
|
const start = startEntry.event;
|
|
59
84
|
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
|
|
60
85
|
}
|
|
61
|
-
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
62
|
-
|
|
86
|
+
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
87
|
+
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
88
|
+
* compaction/renewal boundary) instead of appending a duplicate. `opts.pinned` exempts the
|
|
89
|
+
* entry from the knowledge-budget sweep. */
|
|
90
|
+
pushKnowledge(message, tokens, opts) {
|
|
63
91
|
if (!this.activeKernel)
|
|
64
92
|
return;
|
|
65
93
|
kernelApply(this.activeKernel, this.pendingObservations, {
|
|
66
94
|
kind: "add_knowledge_message",
|
|
67
95
|
content: message.content ?? "",
|
|
68
96
|
tokens: tokens ?? Math.max(1, Math.ceil((message.content?.length ?? 0) / 4)),
|
|
97
|
+
...(opts?.key !== undefined ? { key: opts.key } : {}),
|
|
98
|
+
...(opts?.pinned ? { pinned: true } : {}),
|
|
69
99
|
});
|
|
70
100
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
101
|
+
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
102
|
+
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
103
|
+
removeKnowledge(key) {
|
|
104
|
+
if (!this.activeKernel)
|
|
75
105
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const hit of localHits) {
|
|
83
|
-
entries.push({
|
|
84
|
-
content: `[local semantic cache] ${hit.role}: ${hit.content}`,
|
|
85
|
-
source: "semantic_cache",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const remainingK = topK - entries.length;
|
|
89
|
-
if (remainingK > 0 && this.opts.dreamStore && this.opts.agentId) {
|
|
90
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, query, remainingK);
|
|
91
|
-
for (const hit of hits) {
|
|
92
|
-
entries.push({
|
|
93
|
-
content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`,
|
|
94
|
-
source: "memory",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
else if (req.tool === "knowledge" && this.opts.knowledgeSource) {
|
|
100
|
-
const snippets = await this.opts.knowledgeSource.retrieve(query, topK);
|
|
101
|
-
for (const snippet of snippets) {
|
|
102
|
-
entries.push({ content: snippet, source: "knowledge" });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (entries.length === 0)
|
|
106
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "remove_knowledge", key });
|
|
107
|
+
}
|
|
108
|
+
/** K3: host-driven skill deactivation — toolset re-widens at the next provider call, the
|
|
109
|
+
* skill's knowledge pin drops at the next boundary. Errs-open: not-active is a no-op. */
|
|
110
|
+
deactivateSkill(name) {
|
|
111
|
+
if (!this.activeKernel)
|
|
107
112
|
return;
|
|
108
|
-
kernelApply(
|
|
109
|
-
|
|
110
|
-
kind: "page_in",
|
|
111
|
-
turn: runtime.turn(),
|
|
112
|
-
entry_count: entries.length,
|
|
113
|
-
}));
|
|
113
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
114
|
+
this.knowledgePushedSkills.delete(name);
|
|
114
115
|
}
|
|
115
116
|
async resolveKernelSuspend(runtime, sessionId) {
|
|
116
117
|
const gated = this.pendingObservations.filter((o) => o.kind === "tool_gated" && typeof o.call_id === "string" && typeof o.tool === "string");
|
|
@@ -190,6 +191,63 @@ export class RuntimeRunner {
|
|
|
190
191
|
}
|
|
191
192
|
return { approved, denied, events };
|
|
192
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
196
|
+
* output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map, (b) the result
|
|
197
|
+
* spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
|
|
198
|
+
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
199
|
+
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
200
|
+
*/
|
|
201
|
+
async resolveReadResult(sessionId, argsJson) {
|
|
202
|
+
let callId = "";
|
|
203
|
+
let offset = 0;
|
|
204
|
+
let maxBytes = 4000;
|
|
205
|
+
try {
|
|
206
|
+
const args = JSON.parse(argsJson || "{}");
|
|
207
|
+
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
208
|
+
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
209
|
+
offset = args.offset;
|
|
210
|
+
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
211
|
+
maxBytes = args.max_bytes;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
215
|
+
}
|
|
216
|
+
let full = this.pendingSpoolOutputs.get(callId)?.output;
|
|
217
|
+
if (full === undefined && this.opts.resultSpool) {
|
|
218
|
+
try {
|
|
219
|
+
full = await this.opts.resultSpool.findByCallId(callId);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
full = undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (full === undefined) {
|
|
226
|
+
try {
|
|
227
|
+
const events = await this.opts.sessionLog.read(sessionId);
|
|
228
|
+
for (const { event } of events) {
|
|
229
|
+
if (event.kind !== "tool_completed")
|
|
230
|
+
continue;
|
|
231
|
+
const match = event.results.find(r => r.call_id === callId);
|
|
232
|
+
if (match)
|
|
233
|
+
full = match.output;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
full = undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (full === undefined) {
|
|
241
|
+
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
242
|
+
}
|
|
243
|
+
const start = Math.max(0, offset);
|
|
244
|
+
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
245
|
+
const slice = full.slice(start, end);
|
|
246
|
+
return {
|
|
247
|
+
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
248
|
+
isError: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
193
251
|
async dream(agentId, nowMs = Date.now()) {
|
|
194
252
|
if (!this.opts.dreamStore)
|
|
195
253
|
throw new Error("dreamStore not configured");
|
|
@@ -351,14 +409,35 @@ export class RuntimeRunner {
|
|
|
351
409
|
messages: replayed.map(messageToKernelMessage),
|
|
352
410
|
});
|
|
353
411
|
// P1-B B3: rebuild active-skill gating after a wake (active_skills is not snapshotted).
|
|
412
|
+
// `knowledge` isn't snapshotted either (same graceful-reset philosophy) — best-effort re-push
|
|
413
|
+
// the skill's content from its replayed tool_result so the durable copy survives a wake too.
|
|
414
|
+
const toolResultByCallId = new Map();
|
|
415
|
+
for (const m of replayed) {
|
|
416
|
+
for (const part of m.contentParts ?? []) {
|
|
417
|
+
if (part.type === "tool_result" && part.callId && part.output !== undefined) {
|
|
418
|
+
toolResultByCallId.set(part.callId, part.output);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
354
422
|
for (const m of replayed) {
|
|
355
423
|
for (const tc of m.toolCalls ?? []) {
|
|
356
424
|
if (tc.name !== "skill")
|
|
357
425
|
continue;
|
|
358
426
|
try {
|
|
359
427
|
const name = JSON.parse(tc.arguments || "{}").name;
|
|
360
|
-
if (name)
|
|
361
|
-
|
|
428
|
+
if (!name)
|
|
429
|
+
continue;
|
|
430
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
431
|
+
kind: "skill_activated",
|
|
432
|
+
name,
|
|
433
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
434
|
+
});
|
|
435
|
+
const output = toolResultByCallId.get(tc.id);
|
|
436
|
+
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
437
|
+
this.knowledgePushedSkills.add(name);
|
|
438
|
+
// K1: keyed — the kernel-side upsert is the authoritative dedup across wake replays.
|
|
439
|
+
this.pushKnowledge({ role: "system", content: output }, undefined, { key: `skill:${name}` });
|
|
440
|
+
}
|
|
362
441
|
}
|
|
363
442
|
catch { /* skip */ }
|
|
364
443
|
}
|
|
@@ -386,44 +465,29 @@ export class RuntimeRunner {
|
|
|
386
465
|
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
387
466
|
}
|
|
388
467
|
this.applyKernelPolicies(runtime);
|
|
389
|
-
// I4: pre-fetch memory
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
continue;
|
|
397
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
398
|
-
for (const hit of hits) {
|
|
399
|
-
entries.push({ content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`, source: "memory" });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (entries.length > 0) {
|
|
403
|
-
kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
catch { /* errs-open */ }
|
|
468
|
+
// I4: pre-fetch memory before the first LLM turn (mirrors Node). Strict dynamic context
|
|
469
|
+
// control: single-use retrieval content, not a stable skill — lands in `history` like an
|
|
470
|
+
// ordinary `memory` tool result, so it decays with the compression pyramid instead of
|
|
471
|
+
// pinning itself in `knowledge` forever.
|
|
472
|
+
this.currentGoal = goal;
|
|
473
|
+
if (!resumeMidRun) {
|
|
474
|
+
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
407
475
|
}
|
|
408
476
|
let action = resumeMidRun
|
|
409
477
|
? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
410
478
|
: kernelAction(runtime, this.pendingObservations, startPayload);
|
|
411
|
-
let hasAttemptedReactiveCompact = false;
|
|
412
479
|
// P0-C: the skill loaded and in effect going into the current turn → per-turn `activeSkill` metric.
|
|
413
480
|
let activeSkill;
|
|
414
481
|
// I0b: kernel-throw safety net — see Node runner for full rationale.
|
|
415
482
|
try {
|
|
416
483
|
while (!runtime.isTerminal()) {
|
|
417
|
-
if (action.kind === "execute_tool") {
|
|
418
|
-
await this.applyKernelPageIn(runtime, sessionId);
|
|
419
|
-
}
|
|
420
484
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
421
485
|
if (this.interrupted) {
|
|
422
486
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
423
487
|
break;
|
|
424
488
|
}
|
|
425
|
-
if (this.opts.signalSource) {
|
|
426
|
-
const sig = await this.
|
|
489
|
+
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
490
|
+
const sig = await this.nextInboundSignal();
|
|
427
491
|
if (sig) {
|
|
428
492
|
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
429
493
|
if (sigAction)
|
|
@@ -466,7 +530,7 @@ export class RuntimeRunner {
|
|
|
466
530
|
let turnCacheReadTokens = 0;
|
|
467
531
|
let turnCacheCreationTokens = 0;
|
|
468
532
|
let turnCacheReadBySlot;
|
|
469
|
-
let
|
|
533
|
+
let turnStopReason;
|
|
470
534
|
const abortSignal = this.abortController?.signal;
|
|
471
535
|
try {
|
|
472
536
|
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
@@ -483,6 +547,9 @@ export class RuntimeRunner {
|
|
|
483
547
|
turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
|
|
484
548
|
// I1: per-slot attribution forwarded to TurnMetrics; undefined on non-Anthropic providers.
|
|
485
549
|
turnCacheReadBySlot = usageEvt.cacheReadInputTokensBySlot;
|
|
550
|
+
// Phase 4: stop_reason drives the kernel's max-output-tokens recovery.
|
|
551
|
+
if (usageEvt.stopReason)
|
|
552
|
+
turnStopReason = usageEvt.stopReason;
|
|
486
553
|
continue;
|
|
487
554
|
}
|
|
488
555
|
yield evt;
|
|
@@ -495,23 +562,31 @@ export class RuntimeRunner {
|
|
|
495
562
|
}
|
|
496
563
|
}
|
|
497
564
|
catch (err) {
|
|
498
|
-
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat as an interrupt.
|
|
499
565
|
if (abortSignal?.aborted) {
|
|
566
|
+
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat it as an
|
|
567
|
+
// interrupt (the post-stream `aborted` check below converts it to a clean
|
|
568
|
+
// timeout/UserAbort), not a crash or a provider error.
|
|
500
569
|
this.interrupted = true;
|
|
501
570
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
571
|
+
else {
|
|
572
|
+
// Reactive recovery is now a kernel decision. Forward the raw provider error and
|
|
573
|
+
// dispatch whatever the kernel returns: `call_provider` to retry with a freshly
|
|
574
|
+
// compacted context, or `done` to terminate with an honest `ContextOverflow`. The
|
|
575
|
+
// classify + compact + retry + give-up policy lives in the kernel (one place), not
|
|
576
|
+
// duplicated across the four SDK runners.
|
|
577
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
578
|
+
kind: "provider_error",
|
|
579
|
+
message: formatToolError(err),
|
|
580
|
+
});
|
|
581
|
+
// Withholding (query.ts parity): surface the raw provider error only when the kernel
|
|
582
|
+
// could NOT recover (it returned a terminal). On a recovered retry (`call_provider`)
|
|
583
|
+
// the error stays hidden. `continue` re-enters the loop: a recovered turn persists its
|
|
584
|
+
// compaction archive via the loop-top appendObservations, and a terminal `done` exits
|
|
585
|
+
// through `isTerminal()`.
|
|
586
|
+
if (action.kind === "done") {
|
|
587
|
+
yield { type: "error", message: formatToolError(err) };
|
|
509
588
|
}
|
|
510
|
-
|
|
511
|
-
if (!shouldRetry) {
|
|
512
|
-
yield { type: "error", message: formatToolError(err) };
|
|
513
|
-
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
514
|
-
break;
|
|
589
|
+
continue;
|
|
515
590
|
}
|
|
516
591
|
}
|
|
517
592
|
// #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
|
|
@@ -519,14 +594,6 @@ export class RuntimeRunner {
|
|
|
519
594
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
520
595
|
break;
|
|
521
596
|
}
|
|
522
|
-
if (shouldRetry) {
|
|
523
|
-
action = {
|
|
524
|
-
kind: "call_provider",
|
|
525
|
-
context: runtime.render(),
|
|
526
|
-
tools,
|
|
527
|
-
};
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
597
|
const assistantMessage = {
|
|
531
598
|
role: "assistant",
|
|
532
599
|
content: finalText,
|
|
@@ -539,6 +606,7 @@ export class RuntimeRunner {
|
|
|
539
606
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
540
607
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
541
608
|
now_ms: Date.now(),
|
|
609
|
+
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
542
610
|
};
|
|
543
611
|
let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
|
|
544
612
|
const hasSuspended = this.pendingObservations.some(o => o.kind === "suspended");
|
|
@@ -605,7 +673,16 @@ export class RuntimeRunner {
|
|
|
605
673
|
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
606
674
|
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
|
|
607
675
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
608
|
-
|
|
676
|
+
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
677
|
+
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool,
|
|
678
|
+
// (c) a session-log scan for the original `tool_completed` event.
|
|
679
|
+
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
680
|
+
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow" && c.name !== "read_result");
|
|
681
|
+
for (const call of readResultCalls) {
|
|
682
|
+
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
683
|
+
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
684
|
+
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
685
|
+
}
|
|
609
686
|
for (const call of submitCalls) {
|
|
610
687
|
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
611
688
|
// spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
|
|
@@ -627,7 +704,35 @@ export class RuntimeRunner {
|
|
|
627
704
|
toolResults.push({ callId: call.id, output: "submitted", isError: false });
|
|
628
705
|
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
629
706
|
}
|
|
630
|
-
|
|
707
|
+
// O5 (PreToolUse-hook analog): stateful host veto over each kernel-approved call.
|
|
708
|
+
// A blocked call never executes; its reason reaches the model as a denied result.
|
|
709
|
+
let executableCalls = normalCalls;
|
|
710
|
+
if (this.opts.onToolCall) {
|
|
711
|
+
const allowed = [];
|
|
712
|
+
for (const call of normalCalls) {
|
|
713
|
+
let decision;
|
|
714
|
+
try {
|
|
715
|
+
decision = await this.opts.onToolCall({ callId: call.id, name: call.name, arguments: call.arguments });
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
decision = undefined;
|
|
719
|
+
}
|
|
720
|
+
if (decision?.block) {
|
|
721
|
+
const reason = decision.reason ?? "blocked by host onToolCall hook";
|
|
722
|
+
yield { type: "tool_denied", callId: call.id, toolName: call.name, reason };
|
|
723
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
724
|
+
kind: "tool_denied", turn: runtime.turn(), call_id: call.id, tool_name: call.name, reason,
|
|
725
|
+
});
|
|
726
|
+
const out = `blocked by host hook: ${reason}`;
|
|
727
|
+
toolResults.push({ callId: call.id, output: out, isError: true, errorKind: "governance_denied" });
|
|
728
|
+
yield { type: "tool_result", callId: call.id, name: call.name, content: out, isError: true };
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
allowed.push(call);
|
|
732
|
+
}
|
|
733
|
+
executableCalls = allowed;
|
|
734
|
+
}
|
|
735
|
+
for await (const evt of this.opts.executionPlane.executeAll(executableCalls, runCtx)) {
|
|
631
736
|
yield evt;
|
|
632
737
|
if (evt.type === "tool_result") {
|
|
633
738
|
const tre = evt;
|
|
@@ -681,6 +786,31 @@ export class RuntimeRunner {
|
|
|
681
786
|
});
|
|
682
787
|
}
|
|
683
788
|
}
|
|
789
|
+
// O5 (PostToolUse-hook analog): host inspection of each executed result before it reaches
|
|
790
|
+
// the kernel/session-log — replace the output and/or inject a signal note. Errs-open.
|
|
791
|
+
if (this.opts.onToolResult) {
|
|
792
|
+
for (const r of toolResults) {
|
|
793
|
+
const call = executableCalls.find(c => c.id === r.callId);
|
|
794
|
+
if (!call)
|
|
795
|
+
continue;
|
|
796
|
+
let decision;
|
|
797
|
+
try {
|
|
798
|
+
decision = await this.opts.onToolResult({
|
|
799
|
+
callId: r.callId, name: call.name, arguments: call.arguments,
|
|
800
|
+
output: r.output, isError: r.isError,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
decision = undefined;
|
|
805
|
+
}
|
|
806
|
+
if (!decision)
|
|
807
|
+
continue;
|
|
808
|
+
if (typeof decision.replaceOutput === "string")
|
|
809
|
+
r.output = decision.replaceOutput;
|
|
810
|
+
if (decision.note)
|
|
811
|
+
this.injectNote(decision.note);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
684
814
|
await this.opts.sessionLog.append(sessionId, {
|
|
685
815
|
kind: "tool_completed",
|
|
686
816
|
turn: runtime.turn(),
|
|
@@ -698,6 +828,12 @@ export class RuntimeRunner {
|
|
|
698
828
|
}
|
|
699
829
|
}
|
|
700
830
|
// P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
|
|
831
|
+
//
|
|
832
|
+
// Strict dynamic context control: a skill is METHOD content — how to do something — reused
|
|
833
|
+
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
834
|
+
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
835
|
+
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
836
|
+
// with the compression pyramid like any other tool output). First activation only.
|
|
701
837
|
for (const call of allCalls) {
|
|
702
838
|
if (call.name !== "skill")
|
|
703
839
|
continue;
|
|
@@ -706,8 +842,20 @@ export class RuntimeRunner {
|
|
|
706
842
|
continue;
|
|
707
843
|
try {
|
|
708
844
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
709
|
-
if (name)
|
|
710
|
-
|
|
845
|
+
if (!name)
|
|
846
|
+
continue;
|
|
847
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
848
|
+
kind: "skill_activated",
|
|
849
|
+
name,
|
|
850
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
851
|
+
});
|
|
852
|
+
// With a lease configured, skip the Set optimization: an expired-then-reloaded skill
|
|
853
|
+
// must re-pin — only the kernel knows the lease state; its upsert dedupes anyway.
|
|
854
|
+
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
855
|
+
this.knowledgePushedSkills.add(name);
|
|
856
|
+
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances.
|
|
857
|
+
this.pushKnowledge({ role: "system", content: res.output }, undefined, { key: `skill:${name}` });
|
|
858
|
+
}
|
|
711
859
|
}
|
|
712
860
|
catch { /* skip */ }
|
|
713
861
|
}
|
|
@@ -988,6 +1136,21 @@ export class RuntimeRunner {
|
|
|
988
1136
|
: {}),
|
|
989
1137
|
};
|
|
990
1138
|
}
|
|
1139
|
+
// O6: tune/disable the in-kernel repeat fuse (absent ⇒ kernel defaults: enabled, 5/8).
|
|
1140
|
+
if (this.opts.repeatFuse !== undefined) {
|
|
1141
|
+
const rf = this.opts.repeatFuse;
|
|
1142
|
+
config.repeat_fuse = rf === false
|
|
1143
|
+
? { enabled: false, deny_after: 0, terminate_after: 0 }
|
|
1144
|
+
: { enabled: true, deny_after: rf.denyAfter ?? 5, terminate_after: rf.terminateAfter ?? 8 };
|
|
1145
|
+
}
|
|
1146
|
+
// O4: turn-end criteria gate toggle (absent ⇒ kernel default: enabled).
|
|
1147
|
+
if (this.opts.criteriaGate !== undefined) {
|
|
1148
|
+
config.criteria_gate = this.opts.criteriaGate;
|
|
1149
|
+
}
|
|
1150
|
+
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
1151
|
+
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
1152
|
+
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
1153
|
+
}
|
|
991
1154
|
kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
992
1155
|
if (this.opts.memoryPolicy) {
|
|
993
1156
|
const m = this.opts.memoryPolicy;
|
|
@@ -1110,7 +1273,8 @@ export class RuntimeRunner {
|
|
|
1110
1273
|
if (!source)
|
|
1111
1274
|
return null;
|
|
1112
1275
|
while (!batchState.settled) {
|
|
1113
|
-
|
|
1276
|
+
// O2: injected notes participate in the monitor too (drain order matches nextInboundSignal).
|
|
1277
|
+
const sig = this.injectedSignals.shift() ?? await source.nextSignal();
|
|
1114
1278
|
if (batchState.settled)
|
|
1115
1279
|
break;
|
|
1116
1280
|
if (!sig) {
|
|
@@ -1255,9 +1419,6 @@ export class RuntimeRunner {
|
|
|
1255
1419
|
});
|
|
1256
1420
|
if (!event)
|
|
1257
1421
|
continue;
|
|
1258
|
-
if (obs.kind === "page_out" && obs.archived) {
|
|
1259
|
-
this.localPageOutCache.push(...obs.archived);
|
|
1260
|
-
}
|
|
1261
1422
|
const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
|
|
1262
1423
|
if (event.kind === "compressed") {
|
|
1263
1424
|
nextArchiveStart = compressedSeq + 1;
|
|
@@ -1268,9 +1429,42 @@ export class RuntimeRunner {
|
|
|
1268
1429
|
&& obs.archived.length > 0) {
|
|
1269
1430
|
void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
|
|
1270
1431
|
}
|
|
1432
|
+
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1433
|
+
// re-run the preQueryMemory prefetch for the new sprint (live observations only).
|
|
1434
|
+
if (obs.kind === "renewed") {
|
|
1435
|
+
await this.prefetchMemoryIntoHistory(runtime, "renewal");
|
|
1436
|
+
}
|
|
1271
1437
|
}
|
|
1272
1438
|
return nextArchiveStart;
|
|
1273
1439
|
}
|
|
1440
|
+
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
1441
|
+
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
1442
|
+
* never pinned into `knowledge`. `phase: "initial"` = once before turn 1; `phase: "renewal"` =
|
|
1443
|
+
* re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
|
|
1444
|
+
* hits). Errs-open throughout. */
|
|
1445
|
+
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1446
|
+
if (!this.opts.preQueryMemory || !this.opts.dreamStore || !this.opts.agentId)
|
|
1447
|
+
return;
|
|
1448
|
+
try {
|
|
1449
|
+
const queries = await this.opts.preQueryMemory({ goal: this.currentGoal, phase });
|
|
1450
|
+
const lines = [];
|
|
1451
|
+
for (const q of queries ?? []) {
|
|
1452
|
+
if (typeof q !== "string" || !q.trim())
|
|
1453
|
+
continue;
|
|
1454
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
1455
|
+
for (const hit of hits) {
|
|
1456
|
+
lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.text}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (lines.length > 0) {
|
|
1460
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1461
|
+
kind: "add_history_message",
|
|
1462
|
+
message: { role: "user", content: lines.join("\n") },
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
catch { /* errs-open */ }
|
|
1467
|
+
}
|
|
1274
1468
|
async archiveSemanticPageOut(archived, action) {
|
|
1275
1469
|
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1276
1470
|
return;
|
|
@@ -19,7 +19,9 @@ function terminationFromStatus(status) {
|
|
|
19
19
|
normalized === "timeout" ||
|
|
20
20
|
normalized === "user_abort" ||
|
|
21
21
|
normalized === "error" ||
|
|
22
|
-
normalized === "milestone_exceeded"
|
|
22
|
+
normalized === "milestone_exceeded" ||
|
|
23
|
+
normalized === "context_overflow" ||
|
|
24
|
+
normalized === "no_progress") {
|
|
23
25
|
return normalized;
|
|
24
26
|
}
|
|
25
27
|
return status;
|
|
@@ -72,6 +74,9 @@ export class SubAgentOrchestrator {
|
|
|
72
74
|
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
73
75
|
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
74
76
|
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
77
|
+
// O3: per-child turn / wall-clock caps (fall back to the inherited limits).
|
|
78
|
+
maxTurns: ctx.spec.maxTurns ?? ctx.parentOpts.maxTurns,
|
|
79
|
+
timeoutMs: ctx.spec.maxWallMs ?? ctx.parentOpts.timeoutMs,
|
|
75
80
|
executionPlane: filteredPlane,
|
|
76
81
|
agentId: ctx.spec.identity.agentId,
|
|
77
82
|
systemPrompt,
|
|
@@ -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,10 @@ 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). */
|
|
37
|
+
maxTurns?: number;
|
|
38
|
+
/** O3: per-child wall-clock cap in ms (sets the child runner's `timeoutMs`; falls back to the parent's). */
|
|
39
|
+
maxWallMs?: number;
|
|
31
40
|
}
|
|
32
41
|
/** Kernel process-table observation (Phase 3 canonical spawn signal). */
|
|
33
42
|
export interface AgentProcessChangedObservation {
|
package/dist/types.d.ts
CHANGED
|
@@ -73,7 +73,8 @@ export interface UsageEvent extends StreamEvent {
|
|
|
73
73
|
system?: number;
|
|
74
74
|
tools?: number;
|
|
75
75
|
messages?: number;
|
|
76
|
-
};
|
|
76
|
+
}; /** Provider stop reason — `max_tokens` (Anthropic) / `length` (OpenAI) flag an output-cap truncation driving the kernel's max-output-tokens recovery. */
|
|
77
|
+
stopReason?: string;
|
|
77
78
|
}
|
|
78
79
|
export interface ThinkingDelta extends StreamEvent {
|
|
79
80
|
type: "thinking_delta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@deepstrike/wasm-kernel": "0.2.
|
|
18
|
+
"@deepstrike/wasm-kernel": "0.2.36"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/jest": "^30.0.0",
|