@deepstrike/sdk 0.2.45 → 0.2.47
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/dist/harness/manifest.d.ts +71 -0
- package/dist/harness/manifest.js +304 -0
- package/dist/harness/nudge.d.ts +61 -0
- package/dist/harness/nudge.js +176 -0
- package/dist/harness/public.d.ts +4 -0
- package/dist/harness/public.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/runtime/runner.d.ts +24 -0
- package/dist/runtime/runner.js +42 -7
- package/dist/runtime/sub-agent-orchestrator.js +13 -0
- package/dist/types/agent.d.ts +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RuntimeOptions } from "../runtime/runner.js";
|
|
2
|
+
import type { NudgeRule } from "./nudge.js";
|
|
3
|
+
export interface InstructionProfile {
|
|
4
|
+
/** Start-up protocol (paper: build_bootstrap_instruction). */
|
|
5
|
+
bootstrap?: string;
|
|
6
|
+
/** Execution protocol. */
|
|
7
|
+
execution?: string;
|
|
8
|
+
/** Closing verification protocol. */
|
|
9
|
+
verification?: string;
|
|
10
|
+
/** Failure-recovery protocol. */
|
|
11
|
+
failureRecovery?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compose the four instruction slots onto `base` in the fixed order base → bootstrap → execution →
|
|
15
|
+
* verification → failureRecovery, joined with `"\n\n"`, skipping empty slots. All-empty ⇒ `base`
|
|
16
|
+
* unchanged (identity — the zero-instructions run is byte-for-byte the pre-feature run). The order is
|
|
17
|
+
* fixed and empty slots are dropped so the composed prefix is byte-stable (prefix-cache axiom).
|
|
18
|
+
*/
|
|
19
|
+
export declare function composeSystemPrompt(base: string | undefined, instructions?: InstructionProfile): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The exact `RuntimeOptions` fields a manifest may drive. Derived via `Pick` so field names and types
|
|
22
|
+
* track `RuntimeOptions` verbatim; anything outside this set is rejected by `applyManifest`/`applyPatch`.
|
|
23
|
+
*/
|
|
24
|
+
export type HarnessRuntimePatch = Pick<RuntimeOptions, "maxTurns" | "maxTotalTokens" | "criteriaGate" | "repeatFuse" | "entropyWatch" | "knowledgeBudgetRatio" | "skillLeaseTurns">;
|
|
25
|
+
export interface HarnessManifest {
|
|
26
|
+
manifestVersion: 1;
|
|
27
|
+
/** Parent manifest digest; `null` for a seed. */
|
|
28
|
+
parent: string | null;
|
|
29
|
+
/** Target-model identifier (per-model profile scenarios). */
|
|
30
|
+
modelProfile?: string;
|
|
31
|
+
instructions?: InstructionProfile;
|
|
32
|
+
nudges?: NudgeRule[];
|
|
33
|
+
runtime?: HarnessRuntimePatch;
|
|
34
|
+
/** The proposer's edit whitelist — patches may only target a surface listed here. */
|
|
35
|
+
editableSurfaces: string[];
|
|
36
|
+
audit?: {
|
|
37
|
+
round: number;
|
|
38
|
+
createdBy: "seed" | "proposer";
|
|
39
|
+
targetCluster?: string;
|
|
40
|
+
rationale?: string;
|
|
41
|
+
deltaHeldIn?: number;
|
|
42
|
+
deltaHeldOut?: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface HarnessPatch {
|
|
46
|
+
/** Surface path — must be in the manifest's `editableSurfaces`. */
|
|
47
|
+
targetSurface: string;
|
|
48
|
+
/** `append` applies only to nudges; `remove` clears a slot or drops a nudge by id. */
|
|
49
|
+
op: "set" | "append" | "remove";
|
|
50
|
+
value?: unknown;
|
|
51
|
+
rationale: string;
|
|
52
|
+
/** Failure-cluster key this edit is bound to (paper: one edit per failure mechanism). */
|
|
53
|
+
targetCluster: string;
|
|
54
|
+
expectedEffect: string;
|
|
55
|
+
}
|
|
56
|
+
/** sha-256 hex over the manifest's canonical JSON — the manifest's stable identity. */
|
|
57
|
+
export declare function manifestDigest(manifest: HarnessManifest): string;
|
|
58
|
+
/** Structural load check — throws on anything a manifest is forbidden to carry. */
|
|
59
|
+
export declare function validateManifest(manifest: HarnessManifest): void;
|
|
60
|
+
/**
|
|
61
|
+
* Fold a validated manifest onto `base` runtime options. Instructions ride through as DATA — the
|
|
62
|
+
* runner composes the system prompt once at option normalization so `run_started` and the kernel's
|
|
63
|
+
* AddSystemMessage stay byte-identical. Runtime keys outside the whitelist throw.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyManifest(manifest: HarnessManifest, base: RuntimeOptions): RuntimeOptions;
|
|
66
|
+
/**
|
|
67
|
+
* Apply one structural edit, returning a NEW manifest whose `parent` is the source's digest. Throws
|
|
68
|
+
* when the surface is off-whitelist, the patch is malformed, or the result violates a bound
|
|
69
|
+
* (instruction ≤4000 chars, nudge load rules). The source manifest is never mutated.
|
|
70
|
+
*/
|
|
71
|
+
export declare function applyPatch(manifest: HarnessManifest, patch: HarnessPatch): HarnessManifest;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Harness H1.1 + H1.3 — the harness face as DATA.
|
|
3
|
+
*
|
|
4
|
+
* A `HarnessManifest` is a versioned, hashable lineage node: the editable surfaces a fixed model may
|
|
5
|
+
* rewrite about its OWN harness — instruction slots, nudge rules, and a whitelisted `RuntimeOptions`
|
|
6
|
+
* subset — plus the audit trail binding each edit to the failure cluster it targets. Every function
|
|
7
|
+
* here is pure and deterministic (no clock, no randomness, no I/O), so a manifest digest is a stable
|
|
8
|
+
* identity across processes and the propose→validate→promote loop replays byte-for-byte.
|
|
9
|
+
*
|
|
10
|
+
* The whitelist is the safety boundary: governance / quota / reliability surfaces are deliberately
|
|
11
|
+
* absent, so a proposer can never rewrite them (spec design principle: conservative promotion).
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { validateNudgeRules } from "./nudge.js";
|
|
15
|
+
const INSTRUCTION_SLOTS = ["bootstrap", "execution", "verification", "failureRecovery"];
|
|
16
|
+
/** Per-slot upper bound enforced at load and on every `applyPatch` set. */
|
|
17
|
+
const MAX_INSTRUCTION_CHARS = 4000;
|
|
18
|
+
/**
|
|
19
|
+
* Compose the four instruction slots onto `base` in the fixed order base → bootstrap → execution →
|
|
20
|
+
* verification → failureRecovery, joined with `"\n\n"`, skipping empty slots. All-empty ⇒ `base`
|
|
21
|
+
* unchanged (identity — the zero-instructions run is byte-for-byte the pre-feature run). The order is
|
|
22
|
+
* fixed and empty slots are dropped so the composed prefix is byte-stable (prefix-cache axiom).
|
|
23
|
+
*/
|
|
24
|
+
export function composeSystemPrompt(base, instructions) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (base)
|
|
27
|
+
parts.push(base);
|
|
28
|
+
for (const slot of INSTRUCTION_SLOTS) {
|
|
29
|
+
const text = instructions?.[slot];
|
|
30
|
+
if (text)
|
|
31
|
+
parts.push(text);
|
|
32
|
+
}
|
|
33
|
+
return parts.length === 0 ? base : parts.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
const RUNTIME_PATCH_KEYS = [
|
|
36
|
+
"maxTurns",
|
|
37
|
+
"maxTotalTokens",
|
|
38
|
+
"criteriaGate",
|
|
39
|
+
"repeatFuse",
|
|
40
|
+
"entropyWatch",
|
|
41
|
+
"knowledgeBudgetRatio",
|
|
42
|
+
"skillLeaseTurns",
|
|
43
|
+
];
|
|
44
|
+
// ── Canonical JSON + digest ──────────────────────────────────────────────────
|
|
45
|
+
/** Deterministic serialization: recursive key sort, undefined-valued keys skipped, arrays ordered. */
|
|
46
|
+
function canonicalJson(value) {
|
|
47
|
+
if (value === null)
|
|
48
|
+
return "null";
|
|
49
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
if (typeof value === "number") {
|
|
52
|
+
if (!Number.isFinite(value))
|
|
53
|
+
throw new TypeError("harness manifest requires finite numbers");
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value))
|
|
57
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
58
|
+
if (typeof value === "object") {
|
|
59
|
+
const obj = value;
|
|
60
|
+
const keys = Object.keys(obj).filter(key => obj[key] !== undefined).sort();
|
|
61
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
|
|
62
|
+
}
|
|
63
|
+
throw new TypeError(`harness manifest holds a non-serializable value: ${typeof value}`);
|
|
64
|
+
}
|
|
65
|
+
/** sha-256 hex over the manifest's canonical JSON — the manifest's stable identity. */
|
|
66
|
+
export function manifestDigest(manifest) {
|
|
67
|
+
return createHash("sha256").update(canonicalJson(manifest), "utf8").digest("hex");
|
|
68
|
+
}
|
|
69
|
+
// ── Load validation ──────────────────────────────────────────────────────────
|
|
70
|
+
function validateInstructionProfile(profile) {
|
|
71
|
+
if (typeof profile !== "object" || profile === null || Array.isArray(profile)) {
|
|
72
|
+
throw new TypeError("instructions must be an object");
|
|
73
|
+
}
|
|
74
|
+
for (const slot of INSTRUCTION_SLOTS) {
|
|
75
|
+
const text = profile[slot];
|
|
76
|
+
if (text === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof text !== "string")
|
|
79
|
+
throw new TypeError(`instructions.${slot} must be a string`);
|
|
80
|
+
if (text.length > MAX_INSTRUCTION_CHARS) {
|
|
81
|
+
throw new RangeError(`instructions.${slot} exceeds ${MAX_INSTRUCTION_CHARS} chars`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function validateRuntimePatch(runtime) {
|
|
86
|
+
if (typeof runtime !== "object" || runtime === null) {
|
|
87
|
+
throw new TypeError("manifest.runtime must be an object");
|
|
88
|
+
}
|
|
89
|
+
for (const [key, value] of Object.entries(runtime)) {
|
|
90
|
+
if (!RUNTIME_PATCH_KEYS.includes(key)) {
|
|
91
|
+
throw new RangeError(`runtime patch key not in the editable whitelist: ${key}`);
|
|
92
|
+
}
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
validateRuntimeValue(key, value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Per-key value typing for runtime patches. An LLM proposer WILL eventually put instruction prose
|
|
98
|
+
* where a boolean belongs; rejecting it here turns a mid-run kernel `InvalidConfig` crash into a
|
|
99
|
+
* discardable candidate. */
|
|
100
|
+
function validateRuntimeValue(key, value) {
|
|
101
|
+
const positiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
102
|
+
switch (key) {
|
|
103
|
+
case "maxTurns":
|
|
104
|
+
case "maxTotalTokens":
|
|
105
|
+
case "skillLeaseTurns":
|
|
106
|
+
if (!positiveInt(value))
|
|
107
|
+
throw new TypeError(`runtime.${key} must be a positive integer`);
|
|
108
|
+
return;
|
|
109
|
+
case "criteriaGate":
|
|
110
|
+
if (typeof value !== "boolean")
|
|
111
|
+
throw new TypeError("runtime.criteriaGate must be a boolean");
|
|
112
|
+
return;
|
|
113
|
+
case "knowledgeBudgetRatio":
|
|
114
|
+
if (typeof value !== "number" || !(value > 0 && value <= 1)) {
|
|
115
|
+
throw new TypeError("runtime.knowledgeBudgetRatio must be a number in (0, 1]");
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
case "repeatFuse": {
|
|
119
|
+
if (value === false)
|
|
120
|
+
return;
|
|
121
|
+
if (typeof value !== "object" || value === null) {
|
|
122
|
+
throw new TypeError("runtime.repeatFuse must be false or { denyAfter?, terminateAfter? }");
|
|
123
|
+
}
|
|
124
|
+
const fuse = value;
|
|
125
|
+
for (const k of Object.keys(fuse)) {
|
|
126
|
+
if (k !== "denyAfter" && k !== "terminateAfter") {
|
|
127
|
+
throw new RangeError(`runtime.repeatFuse has unknown key: ${k}`);
|
|
128
|
+
}
|
|
129
|
+
if (fuse[k] !== undefined && !positiveInt(fuse[k])) {
|
|
130
|
+
throw new TypeError(`runtime.repeatFuse.${k} must be a positive integer`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
case "entropyWatch": {
|
|
136
|
+
if (typeof value !== "object" || value === null) {
|
|
137
|
+
throw new TypeError("runtime.entropyWatch must be an object");
|
|
138
|
+
}
|
|
139
|
+
const watch = value;
|
|
140
|
+
for (const k of Object.keys(watch)) {
|
|
141
|
+
const v = watch[k];
|
|
142
|
+
if (v === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
if (k === "enabled" || k === "notifyModel") {
|
|
145
|
+
if (typeof v !== "boolean")
|
|
146
|
+
throw new TypeError(`runtime.entropyWatch.${k} must be a boolean`);
|
|
147
|
+
}
|
|
148
|
+
else if (k === "threshold" || k === "hysteresis") {
|
|
149
|
+
if (typeof v !== "number" || !(v >= 0 && v <= 1)) {
|
|
150
|
+
throw new TypeError(`runtime.entropyWatch.${k} must be a number in [0, 1]`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (k === "cooldownTurns") {
|
|
154
|
+
if (!positiveInt(v))
|
|
155
|
+
throw new TypeError("runtime.entropyWatch.cooldownTurns must be a positive integer");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
throw new RangeError(`runtime.entropyWatch has unknown key: ${k}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
throw new RangeError(`runtime patch key not in the editable whitelist: ${key}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Structural load check — throws on anything a manifest is forbidden to carry. */
|
|
168
|
+
export function validateManifest(manifest) {
|
|
169
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
170
|
+
throw new TypeError("manifest must be an object");
|
|
171
|
+
if (manifest.manifestVersion !== 1)
|
|
172
|
+
throw new TypeError("manifest.manifestVersion must be 1");
|
|
173
|
+
if (!(manifest.parent === null || typeof manifest.parent === "string")) {
|
|
174
|
+
throw new TypeError("manifest.parent must be a digest string or null");
|
|
175
|
+
}
|
|
176
|
+
if (!Array.isArray(manifest.editableSurfaces) || manifest.editableSurfaces.some(s => typeof s !== "string")) {
|
|
177
|
+
throw new TypeError("manifest.editableSurfaces must be a string[]");
|
|
178
|
+
}
|
|
179
|
+
if (manifest.instructions !== undefined)
|
|
180
|
+
validateInstructionProfile(manifest.instructions);
|
|
181
|
+
if (manifest.nudges !== undefined)
|
|
182
|
+
validateNudgeRules(manifest.nudges);
|
|
183
|
+
if (manifest.runtime !== undefined)
|
|
184
|
+
validateRuntimePatch(manifest.runtime);
|
|
185
|
+
}
|
|
186
|
+
// ── Apply ────────────────────────────────────────────────────────────────────
|
|
187
|
+
/**
|
|
188
|
+
* Fold a validated manifest onto `base` runtime options. Instructions ride through as DATA — the
|
|
189
|
+
* runner composes the system prompt once at option normalization so `run_started` and the kernel's
|
|
190
|
+
* AddSystemMessage stay byte-identical. Runtime keys outside the whitelist throw.
|
|
191
|
+
*/
|
|
192
|
+
export function applyManifest(manifest, base) {
|
|
193
|
+
validateManifest(manifest);
|
|
194
|
+
const out = { ...base };
|
|
195
|
+
if (manifest.instructions !== undefined)
|
|
196
|
+
out.instructions = manifest.instructions;
|
|
197
|
+
if (manifest.nudges !== undefined)
|
|
198
|
+
out.nudges = manifest.nudges;
|
|
199
|
+
if (manifest.runtime !== undefined) {
|
|
200
|
+
for (const [key, value] of Object.entries(manifest.runtime)) {
|
|
201
|
+
if (value !== undefined)
|
|
202
|
+
out[key] = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function validatePatchShape(patch) {
|
|
208
|
+
if (typeof patch !== "object" || patch === null)
|
|
209
|
+
throw new TypeError("patch must be an object");
|
|
210
|
+
if (typeof patch.targetSurface !== "string" || patch.targetSurface.length === 0) {
|
|
211
|
+
throw new TypeError("patch.targetSurface must be a non-empty string");
|
|
212
|
+
}
|
|
213
|
+
if (patch.op !== "set" && patch.op !== "append" && patch.op !== "remove") {
|
|
214
|
+
throw new TypeError(`patch.op must be set|append|remove, got ${String(patch.op)}`);
|
|
215
|
+
}
|
|
216
|
+
for (const field of ["rationale", "targetCluster", "expectedEffect"]) {
|
|
217
|
+
if (typeof patch[field] !== "string" || patch[field].length === 0) {
|
|
218
|
+
throw new TypeError(`patch.${field} must be a non-empty string`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function editInstructionSlot(manifest, slot, patch) {
|
|
223
|
+
if (slot === undefined || !INSTRUCTION_SLOTS.includes(slot)) {
|
|
224
|
+
throw new RangeError(`unknown instruction slot: ${patch.targetSurface}`);
|
|
225
|
+
}
|
|
226
|
+
if (patch.op === "append")
|
|
227
|
+
throw new RangeError("append applies only to nudges");
|
|
228
|
+
const key = slot;
|
|
229
|
+
if (patch.op === "remove") {
|
|
230
|
+
if (manifest.instructions)
|
|
231
|
+
delete manifest.instructions[key];
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (typeof patch.value !== "string")
|
|
235
|
+
throw new TypeError(`instructions.${key} set requires a string value`);
|
|
236
|
+
if (patch.value.length > MAX_INSTRUCTION_CHARS) {
|
|
237
|
+
throw new RangeError(`instructions.${key} exceeds ${MAX_INSTRUCTION_CHARS} chars`);
|
|
238
|
+
}
|
|
239
|
+
manifest.instructions = { ...(manifest.instructions ?? {}), [key]: patch.value };
|
|
240
|
+
}
|
|
241
|
+
function editNudges(manifest, patch) {
|
|
242
|
+
const current = manifest.nudges ?? [];
|
|
243
|
+
if (patch.op === "set") {
|
|
244
|
+
const rules = patch.value;
|
|
245
|
+
validateNudgeRules(rules);
|
|
246
|
+
manifest.nudges = rules;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (patch.op === "append") {
|
|
250
|
+
const additions = Array.isArray(patch.value) ? patch.value : [patch.value];
|
|
251
|
+
const merged = [...current, ...additions];
|
|
252
|
+
validateNudgeRules(merged);
|
|
253
|
+
manifest.nudges = merged;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// remove — by id
|
|
257
|
+
if (typeof patch.value !== "string")
|
|
258
|
+
throw new TypeError("nudges remove requires a rule id string");
|
|
259
|
+
manifest.nudges = current.filter(rule => rule.id !== patch.value);
|
|
260
|
+
}
|
|
261
|
+
function editRuntime(manifest, key, patch) {
|
|
262
|
+
if (key === undefined || !RUNTIME_PATCH_KEYS.includes(key)) {
|
|
263
|
+
throw new RangeError(`runtime patch key not in the editable whitelist: ${patch.targetSurface}`);
|
|
264
|
+
}
|
|
265
|
+
if (patch.op === "append")
|
|
266
|
+
throw new RangeError("append applies only to nudges");
|
|
267
|
+
const runtime = { ...(manifest.runtime ?? {}) };
|
|
268
|
+
if (patch.op === "remove")
|
|
269
|
+
delete runtime[key];
|
|
270
|
+
else {
|
|
271
|
+
validateRuntimeValue(key, patch.value);
|
|
272
|
+
runtime[key] = patch.value;
|
|
273
|
+
}
|
|
274
|
+
manifest.runtime = runtime;
|
|
275
|
+
}
|
|
276
|
+
function applySurfaceEdit(manifest, patch) {
|
|
277
|
+
const [head, sub] = patch.targetSurface.split(".");
|
|
278
|
+
if (head === "instructions")
|
|
279
|
+
return editInstructionSlot(manifest, sub, patch);
|
|
280
|
+
if (head === "nudges") {
|
|
281
|
+
if (sub !== undefined)
|
|
282
|
+
throw new RangeError(`nudges surface takes no sub-path: ${patch.targetSurface}`);
|
|
283
|
+
return editNudges(manifest, patch);
|
|
284
|
+
}
|
|
285
|
+
if (head === "runtime")
|
|
286
|
+
return editRuntime(manifest, sub, patch);
|
|
287
|
+
throw new RangeError(`unknown surface path: ${patch.targetSurface}`);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Apply one structural edit, returning a NEW manifest whose `parent` is the source's digest. Throws
|
|
291
|
+
* when the surface is off-whitelist, the patch is malformed, or the result violates a bound
|
|
292
|
+
* (instruction ≤4000 chars, nudge load rules). The source manifest is never mutated.
|
|
293
|
+
*/
|
|
294
|
+
export function applyPatch(manifest, patch) {
|
|
295
|
+
validatePatchShape(patch);
|
|
296
|
+
if (!manifest.editableSurfaces.includes(patch.targetSurface)) {
|
|
297
|
+
throw new RangeError(`surface not in the editable whitelist: ${patch.targetSurface}`);
|
|
298
|
+
}
|
|
299
|
+
const next = structuredClone(manifest);
|
|
300
|
+
applySurfaceEdit(next, patch);
|
|
301
|
+
next.parent = manifestDigest(manifest);
|
|
302
|
+
validateManifest(next);
|
|
303
|
+
return next;
|
|
304
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Harness H1.2 — declarative event→note rules (the runtime control-policy surface).
|
|
3
|
+
*
|
|
4
|
+
* A `NudgeRule` says "when this session event fires, push this note to the model". It generalizes the
|
|
5
|
+
* two hard-coded precedents (EntropyWatch.notify_model, the RepeatFuse STOP text) into data the
|
|
6
|
+
* self-harness loop can rewrite. `NudgeEngine` is a pure, clock-free class: `observe(event)` folds one
|
|
7
|
+
* session event into its per-rule state and returns the notes that fired, which the runner lowers to
|
|
8
|
+
* the `injectNote` signal channel. No I/O, no randomness — so the trigger matrix is exhaustively
|
|
9
|
+
* unit-testable and two engines never share state.
|
|
10
|
+
*/
|
|
11
|
+
import type { RuntimeSignalUrgency } from "../signals/types.js";
|
|
12
|
+
import type { SessionEvent } from "../runtime/session-log.js";
|
|
13
|
+
export type NudgeTrigger = {
|
|
14
|
+
kind: "tool_error";
|
|
15
|
+
errorKind?: string;
|
|
16
|
+
toolName?: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "tool_denied";
|
|
19
|
+
reasonIncludes?: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "tool_calls_at_least";
|
|
22
|
+
count: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "turns_at_least";
|
|
25
|
+
count: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "entropy_alert";
|
|
28
|
+
};
|
|
29
|
+
export interface NudgeRule {
|
|
30
|
+
id: string;
|
|
31
|
+
on: NudgeTrigger;
|
|
32
|
+
/** Template — supports {{tool_name}} {{error_kind}} {{turn}} only. */
|
|
33
|
+
note: string;
|
|
34
|
+
/** Default "normal" (the injectNote default). */
|
|
35
|
+
urgency?: RuntimeSignalUrgency;
|
|
36
|
+
/** Turns that must pass after a fire before the rule may fire again. Default 3. */
|
|
37
|
+
cooldownTurns?: number;
|
|
38
|
+
/** Total fires allowed for the whole run. Default 2. */
|
|
39
|
+
maxFires?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Structural load check — throws on a malformed / oversized / duplicate-id rule set. */
|
|
42
|
+
export declare function validateNudgeRules(rules: NudgeRule[]): void;
|
|
43
|
+
export interface NudgeOutput {
|
|
44
|
+
note: string;
|
|
45
|
+
urgency: RuntimeSignalUrgency;
|
|
46
|
+
}
|
|
47
|
+
export declare class NudgeEngine {
|
|
48
|
+
private readonly rules;
|
|
49
|
+
private readonly state;
|
|
50
|
+
/** Cumulative tool_requested calls, for `tool_calls_at_least` edge detection. */
|
|
51
|
+
private toolCallsSeen;
|
|
52
|
+
/** Highest turn observed, for cooldown + `turns_at_least` edge detection. */
|
|
53
|
+
private currentTurn;
|
|
54
|
+
/** call_id → tool name, so a tool_completed error can name its tool (results carry no name). */
|
|
55
|
+
private readonly callNames;
|
|
56
|
+
constructor(rules: NudgeRule[]);
|
|
57
|
+
/** Fold one session event into rule state; return the notes that fired this event, in rule order. */
|
|
58
|
+
observe(event: SessionEvent): NudgeOutput[];
|
|
59
|
+
/** Returns the template context when the trigger matches this event, else null. */
|
|
60
|
+
private match;
|
|
61
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const MAX_RULES = 16;
|
|
2
|
+
const MAX_NOTE_CHARS = 2000;
|
|
3
|
+
const DEFAULT_COOLDOWN_TURNS = 3;
|
|
4
|
+
const DEFAULT_MAX_FIRES = 2;
|
|
5
|
+
const ALLOWED_TEMPLATE_VARS = ["tool_name", "error_kind", "turn"];
|
|
6
|
+
const URGENCIES = ["low", "normal", "high", "critical"];
|
|
7
|
+
// ── Load validation ──────────────────────────────────────────────────────────
|
|
8
|
+
function validateTrigger(on, id) {
|
|
9
|
+
if (typeof on !== "object" || on === null)
|
|
10
|
+
throw new TypeError(`nudge rule ${id}: trigger must be an object`);
|
|
11
|
+
switch (on.kind) {
|
|
12
|
+
case "tool_error":
|
|
13
|
+
if (on.errorKind !== undefined && typeof on.errorKind !== "string") {
|
|
14
|
+
throw new TypeError(`nudge rule ${id}: tool_error.errorKind must be a string`);
|
|
15
|
+
}
|
|
16
|
+
if (on.toolName !== undefined && typeof on.toolName !== "string") {
|
|
17
|
+
throw new TypeError(`nudge rule ${id}: tool_error.toolName must be a string`);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
case "tool_denied":
|
|
21
|
+
if (on.reasonIncludes !== undefined && typeof on.reasonIncludes !== "string") {
|
|
22
|
+
throw new TypeError(`nudge rule ${id}: tool_denied.reasonIncludes must be a string`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
case "tool_calls_at_least":
|
|
26
|
+
case "turns_at_least":
|
|
27
|
+
if (!Number.isInteger(on.count) || on.count < 1) {
|
|
28
|
+
throw new RangeError(`nudge rule ${id}: ${on.kind}.count must be a positive integer`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
case "entropy_alert":
|
|
32
|
+
return;
|
|
33
|
+
default:
|
|
34
|
+
throw new RangeError(`nudge rule ${id}: unknown trigger kind: ${on.kind}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function validateTemplateVars(note, id) {
|
|
38
|
+
const re = /\{\{([^{}]*)\}\}/g;
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = re.exec(note)) !== null) {
|
|
41
|
+
if (!ALLOWED_TEMPLATE_VARS.includes(match[1])) {
|
|
42
|
+
throw new RangeError(`nudge rule ${id}: unsupported template variable {{${match[1]}}}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Structural load check — throws on a malformed / oversized / duplicate-id rule set. */
|
|
47
|
+
export function validateNudgeRules(rules) {
|
|
48
|
+
if (!Array.isArray(rules))
|
|
49
|
+
throw new TypeError("nudges must be an array");
|
|
50
|
+
if (rules.length > MAX_RULES)
|
|
51
|
+
throw new RangeError(`at most ${MAX_RULES} nudge rules (got ${rules.length})`);
|
|
52
|
+
const ids = new Set();
|
|
53
|
+
for (const rule of rules) {
|
|
54
|
+
if (typeof rule !== "object" || rule === null)
|
|
55
|
+
throw new TypeError("each nudge rule must be an object");
|
|
56
|
+
if (typeof rule.id !== "string" || rule.id.length === 0) {
|
|
57
|
+
throw new TypeError("nudge rule id must be a non-empty string");
|
|
58
|
+
}
|
|
59
|
+
if (ids.has(rule.id))
|
|
60
|
+
throw new RangeError(`duplicate nudge rule id: ${rule.id}`);
|
|
61
|
+
ids.add(rule.id);
|
|
62
|
+
validateTrigger(rule.on, rule.id);
|
|
63
|
+
if (typeof rule.note !== "string" || rule.note.length === 0) {
|
|
64
|
+
throw new TypeError(`nudge rule ${rule.id}: note must be a non-empty string`);
|
|
65
|
+
}
|
|
66
|
+
if (rule.note.length > MAX_NOTE_CHARS) {
|
|
67
|
+
throw new RangeError(`nudge rule ${rule.id}: note exceeds ${MAX_NOTE_CHARS} chars`);
|
|
68
|
+
}
|
|
69
|
+
validateTemplateVars(rule.note, rule.id);
|
|
70
|
+
if (rule.cooldownTurns !== undefined && (!Number.isInteger(rule.cooldownTurns) || rule.cooldownTurns < 0)) {
|
|
71
|
+
throw new RangeError(`nudge rule ${rule.id}: cooldownTurns must be a non-negative integer`);
|
|
72
|
+
}
|
|
73
|
+
if (rule.maxFires !== undefined && (!Number.isInteger(rule.maxFires) || rule.maxFires < 1)) {
|
|
74
|
+
throw new RangeError(`nudge rule ${rule.id}: maxFires must be a positive integer`);
|
|
75
|
+
}
|
|
76
|
+
if (rule.urgency !== undefined && !URGENCIES.includes(rule.urgency)) {
|
|
77
|
+
throw new RangeError(`nudge rule ${rule.id}: invalid urgency ${rule.urgency}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function renderNote(template, ctx) {
|
|
82
|
+
return template
|
|
83
|
+
.replace(/\{\{tool_name\}\}/g, ctx.tool_name ?? "")
|
|
84
|
+
.replace(/\{\{error_kind\}\}/g, ctx.error_kind ?? "")
|
|
85
|
+
.replace(/\{\{turn\}\}/g, String(ctx.turn));
|
|
86
|
+
}
|
|
87
|
+
export class NudgeEngine {
|
|
88
|
+
rules;
|
|
89
|
+
state;
|
|
90
|
+
/** Cumulative tool_requested calls, for `tool_calls_at_least` edge detection. */
|
|
91
|
+
toolCallsSeen = 0;
|
|
92
|
+
/** Highest turn observed, for cooldown + `turns_at_least` edge detection. */
|
|
93
|
+
currentTurn = 0;
|
|
94
|
+
/** call_id → tool name, so a tool_completed error can name its tool (results carry no name). */
|
|
95
|
+
callNames = new Map();
|
|
96
|
+
constructor(rules) {
|
|
97
|
+
validateNudgeRules(rules);
|
|
98
|
+
// Defensive shallow copy: the caller can mutate its array afterwards without touching the engine.
|
|
99
|
+
this.rules = rules.map(rule => ({ ...rule }));
|
|
100
|
+
this.state = this.rules.map(() => ({ fires: 0, lastFireTurn: 0 }));
|
|
101
|
+
}
|
|
102
|
+
/** Fold one session event into rule state; return the notes that fired this event, in rule order. */
|
|
103
|
+
observe(event) {
|
|
104
|
+
const prevToolCalls = this.toolCallsSeen;
|
|
105
|
+
const prevTurn = this.currentTurn;
|
|
106
|
+
if (event.kind === "tool_requested") {
|
|
107
|
+
for (const call of event.calls)
|
|
108
|
+
this.callNames.set(call.id, call.name);
|
|
109
|
+
this.toolCallsSeen += event.calls.length;
|
|
110
|
+
}
|
|
111
|
+
if ("turn" in event && typeof event.turn === "number") {
|
|
112
|
+
this.currentTurn = Math.max(this.currentTurn, event.turn);
|
|
113
|
+
}
|
|
114
|
+
const out = [];
|
|
115
|
+
for (let i = 0; i < this.rules.length; i++) {
|
|
116
|
+
const rule = this.rules[i];
|
|
117
|
+
const matched = this.match(rule.on, event, prevToolCalls, prevTurn);
|
|
118
|
+
if (!matched)
|
|
119
|
+
continue;
|
|
120
|
+
const st = this.state[i];
|
|
121
|
+
if (st.fires >= (rule.maxFires ?? DEFAULT_MAX_FIRES))
|
|
122
|
+
continue;
|
|
123
|
+
if (st.fires > 0 && this.currentTurn - st.lastFireTurn < (rule.cooldownTurns ?? DEFAULT_COOLDOWN_TURNS))
|
|
124
|
+
continue;
|
|
125
|
+
st.fires += 1;
|
|
126
|
+
st.lastFireTurn = this.currentTurn;
|
|
127
|
+
out.push({ note: renderNote(rule.note, matched), urgency: rule.urgency ?? "normal" });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Returns the template context when the trigger matches this event, else null. */
|
|
132
|
+
match(on, event, prevToolCalls, prevTurn) {
|
|
133
|
+
switch (on.kind) {
|
|
134
|
+
case "tool_error": {
|
|
135
|
+
if (event.kind !== "tool_completed")
|
|
136
|
+
return null;
|
|
137
|
+
for (const result of event.results) {
|
|
138
|
+
if (!result.is_error)
|
|
139
|
+
continue;
|
|
140
|
+
if (on.errorKind !== undefined && result.error_kind !== on.errorKind)
|
|
141
|
+
continue;
|
|
142
|
+
const name = this.callNames.get(result.call_id);
|
|
143
|
+
if (on.toolName !== undefined && name !== on.toolName)
|
|
144
|
+
continue;
|
|
145
|
+
return { tool_name: name ?? "", error_kind: result.error_kind ?? "", turn: event.turn };
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
case "tool_denied": {
|
|
150
|
+
if (event.kind !== "tool_denied")
|
|
151
|
+
return null;
|
|
152
|
+
if (on.reasonIncludes !== undefined && !event.reason.includes(on.reasonIncludes))
|
|
153
|
+
return null;
|
|
154
|
+
return { tool_name: event.tool_name, error_kind: "", turn: event.turn };
|
|
155
|
+
}
|
|
156
|
+
case "tool_calls_at_least": {
|
|
157
|
+
// Edge: the cumulative count crosses the threshold on THIS event (monotonic ⇒ fires once).
|
|
158
|
+
if (prevToolCalls < on.count && this.toolCallsSeen >= on.count) {
|
|
159
|
+
return { turn: this.currentTurn };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
case "turns_at_least": {
|
|
164
|
+
if (prevTurn < on.count && this.currentTurn >= on.count) {
|
|
165
|
+
return { turn: this.currentTurn };
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
case "entropy_alert": {
|
|
170
|
+
if (event.kind !== "entropy_alert")
|
|
171
|
+
return null;
|
|
172
|
+
return { turn: event.turn };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/harness/public.d.ts
CHANGED
|
@@ -4,3 +4,7 @@ export { VerdictFnJudge, LlmEvalJudge, HybridJudge } from "./judge.js";
|
|
|
4
4
|
export type { AttemptJudge, JudgeContext, JudgeResult, SkillCandidate } from "./judge.js";
|
|
5
5
|
export { judge } from "../runtime/eval.js";
|
|
6
6
|
export type { VerdictDetail, JudgeArgs } from "../runtime/eval.js";
|
|
7
|
+
export { composeSystemPrompt, manifestDigest, applyManifest, applyPatch, validateManifest, } from "./manifest.js";
|
|
8
|
+
export type { InstructionProfile, HarnessManifest, HarnessRuntimePatch, HarnessPatch, } from "./manifest.js";
|
|
9
|
+
export { NudgeEngine, validateNudgeRules } from "./nudge.js";
|
|
10
|
+
export type { NudgeTrigger, NudgeRule, NudgeOutput } from "./nudge.js";
|
package/dist/harness/public.js
CHANGED
|
@@ -2,3 +2,7 @@
|
|
|
2
2
|
export { AttemptLoop, RuntimeAttemptBody, continueSession, freshWithFeedback, freshWithDigest, } from "./harness.js";
|
|
3
3
|
export { VerdictFnJudge, LlmEvalJudge, HybridJudge } from "./judge.js";
|
|
4
4
|
export { judge } from "../runtime/eval.js";
|
|
5
|
+
// Self-Harness H1: the harness face as data (manifest lineage + declarative event→note rules). The
|
|
6
|
+
// lab layer loads these through the compiled dist, so they live on this public barrel.
|
|
7
|
+
export { composeSystemPrompt, manifestDigest, applyManifest, applyPatch, validateManifest, } from "./manifest.js";
|
|
8
|
+
export { NudgeEngine, validateNudgeRules } from "./nudge.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type { LoopSpec, LoopOutcome } from "./runtime/loop-driver.js";
|
|
|
4
4
|
export type { RunAgentOptions, RunFanoutOptions } from "./runtime/facade.js";
|
|
5
5
|
export { RuntimeRunner, collectText } from "./runtime/runner.js";
|
|
6
6
|
export type { RuntimeOptions, KernelReliabilityOptions, OperationCancellationReason, PromptBudget, SchedulerPolicy } from "./runtime/runner.js";
|
|
7
|
+
export type { InstructionProfile, NudgeRule, NudgeTrigger } from "./harness/public.js";
|
|
7
8
|
export type { SignalPolicy } from "./runtime/os-profile.js";
|
|
8
9
|
export { readKernelDiagnostics, restoreKernelRuntime, snapshotKernelRuntime } from "./runtime/kernel-step.js";
|
|
9
10
|
export type { KernelDiagnostics, KernelSnapshot } from "./runtime/kernel-step.js";
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { type NativeOsProfile, type OsProfileId, type SignalPolicy } from "./os-
|
|
|
16
16
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
17
17
|
import type { BackgroundTaskErrorHandler } from "./reliability.js";
|
|
18
18
|
import { type ContextPolicyOverridesV1 } from "./context-policy.js";
|
|
19
|
+
import { type InstructionProfile } from "../harness/manifest.js";
|
|
20
|
+
import { type NudgeRule } from "../harness/nudge.js";
|
|
19
21
|
export interface SchedulerPolicy {
|
|
20
22
|
version: 1;
|
|
21
23
|
criticalPathWeight: number;
|
|
@@ -142,6 +144,16 @@ export interface RuntimeOptions {
|
|
|
142
144
|
phase?: "initial" | "renewal";
|
|
143
145
|
}) => Promise<MemoryQuery[] | undefined> | MemoryQuery[] | undefined;
|
|
144
146
|
systemPrompt?: string;
|
|
147
|
+
/** Self-Harness H1.1: the four instruction slots (bootstrap/execution/verification/failureRecovery)
|
|
148
|
+
* composed onto `systemPrompt` in fixed order at option normalization. The kernel still sees ONE
|
|
149
|
+
* system prompt; this is the editable instruction surface the self-harness loop rewrites. Absent ⇒
|
|
150
|
+
* `systemPrompt` is used verbatim (zero behavior difference). */
|
|
151
|
+
instructions?: InstructionProfile;
|
|
152
|
+
/** Self-Harness H1.2: declarative event→note rules. On each matching session event a rendered note
|
|
153
|
+
* is pushed through the `injectNote` signal channel (same path as `onToolResult`'s `{note}`).
|
|
154
|
+
* ≤16 rules, validated at construction. Absent/empty ⇒ no engine, no append wrapping, zero
|
|
155
|
+
* behavior difference. */
|
|
156
|
+
nudges?: NudgeRule[];
|
|
145
157
|
initialMemory?: string[];
|
|
146
158
|
skillDir?: string;
|
|
147
159
|
dreamStore?: DreamStore;
|
|
@@ -255,6 +267,13 @@ export interface RuntimeOptions {
|
|
|
255
267
|
* concurrency stays vehicle-scoped (spec §2.5).
|
|
256
268
|
*/
|
|
257
269
|
runGroup?: RunGroup;
|
|
270
|
+
/**
|
|
271
|
+
* Set by the SubAgentOrchestrator for host-derived child runs: the child still joins the
|
|
272
|
+
* `runGroup` (lineage) and settles its actual terminal usage into the group ledger, but reserves
|
|
273
|
+
* no budget axes — group admission governs peer vehicles only. The child's caps stay local
|
|
274
|
+
* (kernel `maxTotalTokens` policy + `resourceQuota`). Never set this for a top-level run.
|
|
275
|
+
*/
|
|
276
|
+
nestedGroupVehicle?: boolean;
|
|
258
277
|
/**
|
|
259
278
|
* Optional long-term memory policy (`set_memory_policy`). Tunes the kernel's memory subsystem
|
|
260
279
|
* (retrieval top-k, stale-warning age, write validation, memory path). Unset leaves the kernel
|
|
@@ -365,6 +384,11 @@ export declare class RuntimeRunner {
|
|
|
365
384
|
private dashboard;
|
|
366
385
|
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
367
386
|
private lastEntropySample;
|
|
387
|
+
/** H1.1: `systemPrompt` with the instruction slots composed in, computed ONCE so `run_started` and
|
|
388
|
+
* the kernel AddSystemMessage take the identical string. */
|
|
389
|
+
private readonly composedSystemPrompt;
|
|
390
|
+
/** H1.2: present only when `opts.nudges` is non-empty; else null and the append funnel is untouched. */
|
|
391
|
+
private readonly nudgeEngine;
|
|
368
392
|
constructor(opts: RuntimeOptions);
|
|
369
393
|
/** Host configuration (for coordinator / sub-agent spawn). */
|
|
370
394
|
get hostOptions(): RuntimeOptions;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -19,6 +19,8 @@ import { LargeResultSpool } from "./large-result-spool.js";
|
|
|
19
19
|
import { formatToolError } from "../tools/errors.js";
|
|
20
20
|
import { ManagedTaskScope } from "./reliability.js";
|
|
21
21
|
import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
|
|
22
|
+
import { composeSystemPrompt } from "../harness/manifest.js";
|
|
23
|
+
import { NudgeEngine } from "../harness/nudge.js";
|
|
22
24
|
export function schedulerPolicyToKernel(policy) {
|
|
23
25
|
const allowed = new Set([
|
|
24
26
|
"version", "criticalPathWeight", "fanoutWeight", "ageWeight", "tokenCostWeight",
|
|
@@ -97,12 +99,18 @@ export class RuntimeRunner {
|
|
|
97
99
|
dashboard = null;
|
|
98
100
|
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
99
101
|
lastEntropySample = null;
|
|
102
|
+
/** H1.1: `systemPrompt` with the instruction slots composed in, computed ONCE so `run_started` and
|
|
103
|
+
* the kernel AddSystemMessage take the identical string. */
|
|
104
|
+
composedSystemPrompt;
|
|
105
|
+
/** H1.2: present only when `opts.nudges` is non-empty; else null and the append funnel is untouched. */
|
|
106
|
+
nudgeEngine;
|
|
100
107
|
constructor(opts) {
|
|
101
108
|
this.opts = opts;
|
|
102
109
|
const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
|
|
103
110
|
if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
|
|
104
111
|
throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
|
|
105
112
|
}
|
|
113
|
+
this.composedSystemPrompt = composeSystemPrompt(opts.systemPrompt, opts.instructions);
|
|
106
114
|
if (opts.enableDiagnosticsDashboard) {
|
|
107
115
|
const originalAppend = opts.sessionLog.append.bind(opts.sessionLog);
|
|
108
116
|
opts.sessionLog.append = async (sessionId, event) => {
|
|
@@ -114,6 +122,21 @@ export class RuntimeRunner {
|
|
|
114
122
|
return seq;
|
|
115
123
|
};
|
|
116
124
|
}
|
|
125
|
+
// H1.2: feed the nudge engine at the append funnel, stacking OVER any prior wrapping so the event
|
|
126
|
+
// is durably logged before a nudge observes it. Notes drain through the same `injectNote` channel
|
|
127
|
+
// as host `onToolResult` notes, surfacing in the NEXT provider request. Skipped entirely when no
|
|
128
|
+
// rules are configured, keeping the default event stream byte-identical.
|
|
129
|
+
this.nudgeEngine = opts.nudges?.length ? new NudgeEngine(opts.nudges) : null;
|
|
130
|
+
if (this.nudgeEngine) {
|
|
131
|
+
const priorAppend = opts.sessionLog.append.bind(opts.sessionLog);
|
|
132
|
+
opts.sessionLog.append = async (sessionId, event) => {
|
|
133
|
+
const seq = await priorAppend(sessionId, event);
|
|
134
|
+
for (const { note, urgency } of this.nudgeEngine.observe(event)) {
|
|
135
|
+
this.injectNote(note, urgency);
|
|
136
|
+
}
|
|
137
|
+
return seq;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
117
140
|
}
|
|
118
141
|
/** Host configuration (for coordinator / sub-agent spawn). */
|
|
119
142
|
get hostOptions() {
|
|
@@ -559,6 +582,7 @@ export class RuntimeRunner {
|
|
|
559
582
|
spec,
|
|
560
583
|
manifest,
|
|
561
584
|
sessionLog: this.opts.sessionLog,
|
|
585
|
+
toolAccess: spec.toolAccess,
|
|
562
586
|
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
563
587
|
});
|
|
564
588
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
@@ -1221,7 +1245,7 @@ export class RuntimeRunner {
|
|
|
1221
1245
|
goal: req.goal,
|
|
1222
1246
|
criteria: req.criteria ?? [],
|
|
1223
1247
|
agent_id: this.opts.agentId,
|
|
1224
|
-
system_prompt: this.
|
|
1248
|
+
system_prompt: this.composedSystemPrompt,
|
|
1225
1249
|
...(attachments ? { attachments } : {}),
|
|
1226
1250
|
});
|
|
1227
1251
|
}
|
|
@@ -1423,11 +1447,11 @@ export class RuntimeRunner {
|
|
|
1423
1447
|
kind: "set_tools",
|
|
1424
1448
|
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1425
1449
|
});
|
|
1426
|
-
if (this.
|
|
1450
|
+
if (this.composedSystemPrompt) {
|
|
1427
1451
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1428
1452
|
kind: "add_system_message",
|
|
1429
|
-
content: this.
|
|
1430
|
-
tokens: Math.max(1, Math.ceil(this.
|
|
1453
|
+
content: this.composedSystemPrompt,
|
|
1454
|
+
tokens: Math.max(1, Math.ceil(this.composedSystemPrompt.length / 4)),
|
|
1431
1455
|
});
|
|
1432
1456
|
}
|
|
1433
1457
|
if (this.opts.initialMemory) {
|
|
@@ -1567,13 +1591,24 @@ export class RuntimeRunner {
|
|
|
1567
1591
|
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
1568
1592
|
}
|
|
1569
1593
|
// Reserve capacity before start_run. The kernel enforces only this vehicle's grant and reports
|
|
1570
|
-
// exact terminal usage against the same opaque reservation identity.
|
|
1594
|
+
// exact terminal usage against the same opaque reservation identity. A nested vehicle joins for
|
|
1595
|
+
// lineage/settlement only: it reserves no budget axes (group admission governs peer vehicles),
|
|
1596
|
+
// so the parent's held reservation cannot squeeze the child's grant to zero.
|
|
1571
1597
|
if (this.opts.runGroup) {
|
|
1572
1598
|
const g = this.opts.runGroup;
|
|
1573
|
-
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.groupBudgetRequest());
|
|
1599
|
+
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.opts.nestedGroupVehicle ? { limits: {}, requested: {} } : this.groupBudgetRequest());
|
|
1574
1600
|
this.activeGroupBudgetScope = groupBudgetScope;
|
|
1575
1601
|
}
|
|
1576
|
-
|
|
1602
|
+
try {
|
|
1603
|
+
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
1604
|
+
}
|
|
1605
|
+
catch (err) {
|
|
1606
|
+
// Admission failure (e.g. the kernel rejecting a zero-capacity grant): release the
|
|
1607
|
+
// reservation so it cannot linger in the group ledger, then surface the error.
|
|
1608
|
+
await groupBudgetScope?.release();
|
|
1609
|
+
this.activeGroupBudgetScope = undefined;
|
|
1610
|
+
throw err;
|
|
1611
|
+
}
|
|
1577
1612
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1578
1613
|
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
1579
1614
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
@@ -89,6 +89,15 @@ export class SubAgentOrchestrator {
|
|
|
89
89
|
const inherit = ctx.toolAccess === "inherit";
|
|
90
90
|
const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
|
|
91
91
|
const metaTools = inherit ? availableMetaTools(ctx.parentOpts) : deriveMetaTools(permitted, ctx.parentOpts);
|
|
92
|
+
// A "filtered" spawn with no capability grants and no meta-tools resolves to a deny-all plane —
|
|
93
|
+
// the child model sees zero tools and reports "no tools available". Warn the host (visible, not
|
|
94
|
+
// fatal) with the fix, mirroring `maybeWarnFailureShapedChunk`'s tone. Exempt workflow nodes:
|
|
95
|
+
// `!inherit && workflow-node ⇒ quarantined ⇒ intentional deny-all, not a misconfiguration.
|
|
96
|
+
if (!inherit && !ctx.isWorkflowNode && permitted.size === 0 && metaTools.size === 0) {
|
|
97
|
+
console.warn(`[deepstrike] spawned sub-agent "${ctx.spec.identity.agentId}" resolved to zero tools ` +
|
|
98
|
+
`(deny-all filter). Mount tools as capabilities and grant via spec.capabilityFilter, or pass ` +
|
|
99
|
+
`spec.toolAccess:'inherit' to run on the parent's plane. If a tool-less child is intentional, ignore this.`);
|
|
100
|
+
}
|
|
92
101
|
const basePlane = inherit
|
|
93
102
|
? ctx.parentOpts.executionPlane
|
|
94
103
|
: new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
|
|
@@ -126,6 +135,10 @@ export class SubAgentOrchestrator {
|
|
|
126
135
|
enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
|
|
127
136
|
// M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
|
|
128
137
|
isWorkflowNode: ctx.isWorkflowNode,
|
|
138
|
+
// Nested vehicle: the child joins the inherited runGroup for lineage/settlement only — it
|
|
139
|
+
// must NOT re-reserve budget axes the parent already holds (that double-reserve squeezed the
|
|
140
|
+
// child's grant to 0 and the kernel stripped its first-turn tools).
|
|
141
|
+
nestedGroupVehicle: true,
|
|
129
142
|
// The child runs under ITS OWN spec, never the parent's: the spread above would otherwise
|
|
130
143
|
// leak the parent's `runSpec` (identity, capability filter — and a LoopDriver's armed
|
|
131
144
|
// `loopRound`, giving every child a phantom pace tool). A loop-node iteration carries its
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export interface AgentRunSpec {
|
|
|
52
52
|
/** O3: per-child wall-clock cap in milliseconds (sets the child runner's `timeoutMs`; falls back to
|
|
53
53
|
* the parent's). A hung child terminates `timeout` instead of stalling the parent indefinitely. */
|
|
54
54
|
maxWallMs?: number;
|
|
55
|
+
/** Tool surface for a spawned sub-agent. Host-side only (like `modelHint`) — NOT sent to the kernel
|
|
56
|
+
* (`agentRunSpecToKernel` maps fields explicitly and omits it). Default `"filtered"` keeps the spawn
|
|
57
|
+
* path's deny-all-safe default: the child is filtered to its manifest grants, and a grant-less spawn
|
|
58
|
+
* resolves to zero tools. `"inherit"` runs the child on the parent's execution plane with the
|
|
59
|
+
* parent's meta-tool availability (same mechanism trusted workflow nodes use) — the child's surface
|
|
60
|
+
* is a subset of the parent's, never a privilege escalation. */
|
|
61
|
+
toolAccess?: "inherit" | "filtered";
|
|
55
62
|
}
|
|
56
63
|
/** Kernel process-table observation (Phase 3 canonical spawn signal). */
|
|
57
64
|
export interface AgentProcessChangedObservation {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
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.47",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|