@deepstrike/sdk 0.2.39 → 0.2.40
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 +61 -31
- package/dist/collaboration/contract.d.ts +2 -2
- package/dist/collaboration/contract.js +2 -2
- package/dist/collaboration/handoff.d.ts +2 -14
- package/dist/collaboration/handoff.js +1 -17
- package/dist/collaboration/harness.d.ts +16 -47
- package/dist/collaboration/harness.js +57 -158
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +1 -2
- package/dist/collaboration/modes/creator-verifier.js +42 -6
- package/dist/collaboration/pool.d.ts +8 -0
- package/dist/collaboration/pool.js +56 -3
- package/dist/harness/harness.d.ts +97 -97
- package/dist/harness/harness.js +190 -144
- package/dist/harness/judge.d.ts +3 -2
- package/dist/harness/judge.js +7 -3
- package/dist/harness/public.d.ts +5 -3
- package/dist/harness/public.js +3 -2
- package/dist/index.d.ts +22 -7
- package/dist/index.js +8 -1
- package/dist/kernel.d.ts +12 -39
- package/dist/memory/agent.d.ts +8 -41
- package/dist/memory/agent.js +34 -117
- package/dist/memory/extraction.d.ts +4 -0
- package/dist/memory/extraction.js +79 -0
- package/dist/memory/in-memory-store.d.ts +28 -15
- package/dist/memory/in-memory-store.js +74 -25
- package/dist/memory/protocols.d.ts +56 -56
- package/dist/memory/protocols.js +1 -0
- package/dist/memory/public.d.ts +3 -1
- package/dist/memory/public.js +1 -0
- package/dist/memory/ranking.d.ts +33 -0
- package/dist/memory/ranking.js +77 -0
- package/dist/memory/retention.d.ts +17 -0
- package/dist/memory/retention.js +54 -0
- package/dist/os/public.d.ts +3 -3
- package/dist/os/public.js +1 -1
- package/dist/providers/base.d.ts +8 -0
- package/dist/providers/base.js +0 -0
- package/dist/providers/gemini.js +12 -0
- package/dist/providers/ollama.js +3 -1
- package/dist/providers/openai-responses.js +9 -2
- package/dist/providers/profiles.d.ts +5 -3
- package/dist/providers/profiles.js +3 -3
- package/dist/runtime/context-policy.d.ts +35 -0
- package/dist/runtime/context-policy.js +66 -0
- package/dist/runtime/eval.d.ts +6 -2
- package/dist/runtime/eval.js +2 -2
- package/dist/runtime/event-stream.d.ts +9 -0
- package/dist/runtime/event-stream.js +25 -11
- package/dist/runtime/execution-plane.d.ts +5 -1
- package/dist/runtime/execution-plane.js +9 -3
- package/dist/runtime/facade.js +2 -1
- package/dist/runtime/kernel-event-log.d.ts +0 -2
- package/dist/runtime/kernel-event-log.js +61 -13
- package/dist/runtime/kernel-primitives-dashboard.js +1 -1
- package/dist/runtime/kernel-rebuild.d.ts +13 -0
- package/dist/runtime/kernel-rebuild.js +75 -0
- package/dist/runtime/kernel-step.d.ts +157 -8
- package/dist/runtime/kernel-step.js +220 -7
- package/dist/runtime/kernel-transaction-log.d.ts +61 -0
- package/dist/runtime/kernel-transaction-log.js +149 -0
- package/dist/runtime/large-result-spool.d.ts +3 -1
- package/dist/runtime/large-result-spool.js +24 -5
- package/dist/runtime/loop-driver.d.ts +1 -1
- package/dist/runtime/loop-driver.js +2 -7
- package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
- package/dist/runtime/mcp-proxy-plane.js +23 -6
- package/dist/runtime/os-profile.d.ts +9 -10
- package/dist/runtime/os-profile.js +14 -10
- package/dist/runtime/os-snapshot.d.ts +19 -0
- package/dist/runtime/os-snapshot.js +33 -3
- package/dist/runtime/process-sandbox-plane.js +16 -11
- package/dist/runtime/reaction-checkpoint.d.ts +51 -0
- package/dist/runtime/reaction-checkpoint.js +83 -0
- package/dist/runtime/reactive-session.d.ts +9 -3
- package/dist/runtime/reactive-session.js +44 -14
- package/dist/runtime/reliability.d.ts +48 -0
- package/dist/runtime/reliability.js +86 -0
- package/dist/runtime/remote-vpc-plane.js +4 -3
- package/dist/runtime/run-group.d.ts +38 -35
- package/dist/runtime/run-group.js +97 -54
- package/dist/runtime/runner.d.ts +117 -78
- package/dist/runtime/runner.js +1516 -1197
- package/dist/runtime/session-log.d.ts +71 -12
- package/dist/runtime/session-log.js +192 -32
- package/dist/runtime/session-repair.d.ts +11 -7
- package/dist/runtime/session-repair.js +11 -8
- package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
- package/dist/runtime/sub-agent-orchestrator.js +45 -18
- package/dist/signals/gateway.d.ts +23 -10
- package/dist/signals/gateway.js +81 -17
- package/dist/signals/scheduled.js +0 -1
- package/dist/signals/types.d.ts +21 -13
- package/dist/skills/loader.js +12 -2
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/agent.d.ts +40 -1
- package/dist/types/agent.js +61 -1
- package/dist/types.d.ts +6 -1
- package/dist/workflow/public.d.ts +2 -3
- package/dist/workflow/public.js +0 -1
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const KERNEL_LOG_RECORD_VERSION = 1;
|
|
3
|
+
export class KernelLogConflictError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "KernelLogConflictError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class KernelLogIntegrityError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "KernelLogIntegrityError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function canonicalKernelJson(value) {
|
|
16
|
+
if (value === null)
|
|
17
|
+
return "null";
|
|
18
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
if (typeof value === "number") {
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
throw new KernelLogIntegrityError("canonical records require finite numbers");
|
|
23
|
+
if (Number.isSafeInteger(value))
|
|
24
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
25
|
+
if (Number.isInteger(value)) {
|
|
26
|
+
throw new KernelLogIntegrityError("canonical record integer exceeds the cross-SDK safe range");
|
|
27
|
+
}
|
|
28
|
+
const bytes = new Uint8Array(8);
|
|
29
|
+
new DataView(bytes.buffer).setFloat64(0, value, false);
|
|
30
|
+
return `f64:${Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return `[${value.map(canonicalKernelJson).join(",")}]`;
|
|
34
|
+
if (typeof value === "object") {
|
|
35
|
+
const object = value;
|
|
36
|
+
const keys = Object.keys(object).sort();
|
|
37
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalKernelJson(object[key])}`).join(",")}}`;
|
|
38
|
+
}
|
|
39
|
+
throw new KernelLogIntegrityError(`unsupported canonical record value: ${typeof value}`);
|
|
40
|
+
}
|
|
41
|
+
export function kernelRecordDigest(value) {
|
|
42
|
+
return createHash("sha256").update(canonicalKernelJson(value), "utf8").digest("hex");
|
|
43
|
+
}
|
|
44
|
+
export async function createKernelOperationGenesis(input) {
|
|
45
|
+
const body = {
|
|
46
|
+
record_version: KERNEL_LOG_RECORD_VERSION,
|
|
47
|
+
...input,
|
|
48
|
+
};
|
|
49
|
+
validateGenesisBody(body);
|
|
50
|
+
return { ...body, genesis_digest: kernelRecordDigest(body) };
|
|
51
|
+
}
|
|
52
|
+
export async function createKernelTransaction(input) {
|
|
53
|
+
const body = {
|
|
54
|
+
record_version: KERNEL_LOG_RECORD_VERSION,
|
|
55
|
+
operation_id: input.operation_id,
|
|
56
|
+
step_seq: input.step_seq,
|
|
57
|
+
base_generation: input.base_generation,
|
|
58
|
+
input: input.input,
|
|
59
|
+
input_digest: kernelRecordDigest(input.input),
|
|
60
|
+
previous_transaction_digest: input.previous_transaction_digest,
|
|
61
|
+
step_digest: kernelRecordDigest(input.step),
|
|
62
|
+
};
|
|
63
|
+
validateTransactionBody(body);
|
|
64
|
+
return { ...body, transaction_digest: kernelRecordDigest(body) };
|
|
65
|
+
}
|
|
66
|
+
export function verifyKernelOperationGenesis(genesis) {
|
|
67
|
+
const { genesis_digest, ...body } = genesis;
|
|
68
|
+
validateGenesisBody(body);
|
|
69
|
+
if (kernelRecordDigest(body) !== genesis_digest) {
|
|
70
|
+
throw new KernelLogIntegrityError("kernel genesis digest does not match its canonical body");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function verifyKernelTransaction(transaction) {
|
|
74
|
+
const { transaction_digest, ...body } = transaction;
|
|
75
|
+
validateTransactionBody(body);
|
|
76
|
+
if (kernelRecordDigest(body.input) !== body.input_digest) {
|
|
77
|
+
throw new KernelLogIntegrityError("kernel transaction input digest does not match its input");
|
|
78
|
+
}
|
|
79
|
+
if (kernelRecordDigest(body) !== transaction_digest) {
|
|
80
|
+
throw new KernelLogIntegrityError("kernel transaction digest does not match its canonical body");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function verifyKernelTransactionSuccessor(previous, transaction) {
|
|
84
|
+
const expectedStepSeq = previous ? previous.step_seq + 1 : 1;
|
|
85
|
+
const expectedGeneration = previous ? previous.base_generation + 1 : 0;
|
|
86
|
+
if (transaction.step_seq !== expectedStepSeq) {
|
|
87
|
+
throw new KernelLogIntegrityError(`kernel transaction step_seq ${transaction.step_seq} does not follow ${expectedStepSeq - 1}`);
|
|
88
|
+
}
|
|
89
|
+
if (transaction.base_generation !== expectedGeneration) {
|
|
90
|
+
throw new KernelLogIntegrityError(`kernel transaction base_generation ${transaction.base_generation} does not match ${expectedGeneration}`);
|
|
91
|
+
}
|
|
92
|
+
if (transaction.input.operation_id !== transaction.operation_id) {
|
|
93
|
+
throw new KernelLogIntegrityError("kernel transaction input operation_id does not match its envelope");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Validate one complete authoritative operation stream and derive its next wire cursor. */
|
|
97
|
+
export function verifyKernelTransactionStream(genesis, transactions) {
|
|
98
|
+
verifyKernelOperationGenesis(genesis);
|
|
99
|
+
let previous;
|
|
100
|
+
let head = genesis.genesis_digest;
|
|
101
|
+
for (const transaction of transactions) {
|
|
102
|
+
verifyKernelTransaction(transaction);
|
|
103
|
+
if (transaction.operation_id !== genesis.operation_id) {
|
|
104
|
+
throw new KernelLogIntegrityError("kernel transaction operation_id does not match genesis");
|
|
105
|
+
}
|
|
106
|
+
if (transaction.previous_transaction_digest !== head) {
|
|
107
|
+
throw new KernelLogIntegrityError("kernel transaction digest chain is not continuous");
|
|
108
|
+
}
|
|
109
|
+
verifyKernelTransactionSuccessor(previous, transaction);
|
|
110
|
+
previous = transaction;
|
|
111
|
+
head = transaction.transaction_digest;
|
|
112
|
+
}
|
|
113
|
+
const nextSequence = (previous?.step_seq ?? 0) + 1;
|
|
114
|
+
return {
|
|
115
|
+
operation_id: genesis.operation_id,
|
|
116
|
+
next_event_sequence: nextSequence,
|
|
117
|
+
next_step_seq: nextSequence,
|
|
118
|
+
transaction_head_digest: head,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function validateGenesisBody(genesis) {
|
|
122
|
+
if (genesis.record_version !== KERNEL_LOG_RECORD_VERSION) {
|
|
123
|
+
throw new KernelLogIntegrityError("unsupported kernel genesis record version");
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isSafeInteger(genesis.abi_version) || genesis.abi_version <= 0) {
|
|
126
|
+
throw new KernelLogIntegrityError("kernel genesis abi_version must be a positive safe integer");
|
|
127
|
+
}
|
|
128
|
+
if (!genesis.operation_id)
|
|
129
|
+
throw new KernelLogIntegrityError("kernel genesis operation_id is required");
|
|
130
|
+
if (!Number.isSafeInteger(genesis.default_policy_version) || genesis.default_policy_version <= 0) {
|
|
131
|
+
throw new KernelLogIntegrityError("kernel genesis default_policy_version must be a positive safe integer");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function validateTransactionBody(transaction) {
|
|
135
|
+
if (transaction.record_version !== KERNEL_LOG_RECORD_VERSION) {
|
|
136
|
+
throw new KernelLogIntegrityError("unsupported kernel transaction record version");
|
|
137
|
+
}
|
|
138
|
+
if (!transaction.operation_id)
|
|
139
|
+
throw new KernelLogIntegrityError("kernel transaction operation_id is required");
|
|
140
|
+
if (!Number.isSafeInteger(transaction.step_seq) || transaction.step_seq <= 0) {
|
|
141
|
+
throw new KernelLogIntegrityError("kernel transaction step_seq must be a positive safe integer");
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isSafeInteger(transaction.base_generation) || transaction.base_generation < 0) {
|
|
144
|
+
throw new KernelLogIntegrityError("kernel transaction base_generation must be a non-negative safe integer");
|
|
145
|
+
}
|
|
146
|
+
if (!transaction.previous_transaction_digest) {
|
|
147
|
+
throw new KernelLogIntegrityError("kernel transaction previous_transaction_digest is required");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -57,6 +57,8 @@ export declare class LargeResultSpool {
|
|
|
57
57
|
* Get spool file path for a hash.
|
|
58
58
|
*/
|
|
59
59
|
private getSpoolPath;
|
|
60
|
+
private callKey;
|
|
61
|
+
private atomicWrite;
|
|
60
62
|
/**
|
|
61
63
|
* Write large result to disk.
|
|
62
64
|
*/
|
|
@@ -80,7 +82,7 @@ export declare class LargeResultSpool {
|
|
|
80
82
|
/**
|
|
81
83
|
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
82
84
|
* `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
|
|
83
|
-
* for the
|
|
85
|
+
* for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
|
|
84
86
|
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
85
87
|
*/
|
|
86
88
|
findByCallId(callId: string): Promise<string | undefined>;
|
|
@@ -46,6 +46,25 @@ export class LargeResultSpool {
|
|
|
46
46
|
getSpoolPath(hash) {
|
|
47
47
|
return path.join(this.spoolDir, `${hash}.txt`);
|
|
48
48
|
}
|
|
49
|
+
callKey(callId) {
|
|
50
|
+
return this.hashContent(callId).slice(0, 32);
|
|
51
|
+
}
|
|
52
|
+
async atomicWrite(spoolPath, content) {
|
|
53
|
+
const tempPath = `${spoolPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
54
|
+
let handle;
|
|
55
|
+
try {
|
|
56
|
+
handle = await fs.open(tempPath, 'wx');
|
|
57
|
+
await handle.writeFile(content, 'utf-8');
|
|
58
|
+
await handle.sync();
|
|
59
|
+
await handle.close();
|
|
60
|
+
handle = undefined;
|
|
61
|
+
await fs.rename(tempPath, spoolPath);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await handle?.close().catch(() => undefined);
|
|
65
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
49
68
|
/**
|
|
50
69
|
* Write large result to disk.
|
|
51
70
|
*/
|
|
@@ -56,7 +75,7 @@ export class LargeResultSpool {
|
|
|
56
75
|
promise = (async () => {
|
|
57
76
|
try {
|
|
58
77
|
await fs.mkdir(this.spoolDir, { recursive: true });
|
|
59
|
-
await
|
|
78
|
+
await this.atomicWrite(spoolPath, content);
|
|
60
79
|
return spoolPath;
|
|
61
80
|
}
|
|
62
81
|
finally {
|
|
@@ -111,13 +130,13 @@ omitted: ${omitted} chars
|
|
|
111
130
|
*/
|
|
112
131
|
async persistOutput(callId, content) {
|
|
113
132
|
const hash = this.hashContent(content);
|
|
114
|
-
const spoolPath = this.getSpoolPath(`${callId}-${hash.slice(0, 16)}`);
|
|
133
|
+
const spoolPath = this.getSpoolPath(`${this.callKey(callId)}-${hash.slice(0, 16)}`);
|
|
115
134
|
let promise = this.activeWrites.get(spoolPath);
|
|
116
135
|
if (!promise) {
|
|
117
136
|
promise = (async () => {
|
|
118
137
|
try {
|
|
119
138
|
await fs.mkdir(this.spoolDir, { recursive: true });
|
|
120
|
-
await
|
|
139
|
+
await this.atomicWrite(spoolPath, content);
|
|
121
140
|
return spoolPath;
|
|
122
141
|
}
|
|
123
142
|
finally {
|
|
@@ -143,7 +162,7 @@ omitted: ${omitted} chars
|
|
|
143
162
|
/**
|
|
144
163
|
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
145
164
|
* `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
|
|
146
|
-
* for the
|
|
165
|
+
* for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
|
|
147
166
|
* for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
|
|
148
167
|
*/
|
|
149
168
|
async findByCallId(callId) {
|
|
@@ -154,7 +173,7 @@ omitted: ${omitted} chars
|
|
|
154
173
|
catch {
|
|
155
174
|
return undefined;
|
|
156
175
|
}
|
|
157
|
-
const prefix = `${callId}-`;
|
|
176
|
+
const prefix = `${this.callKey(callId)}-`;
|
|
158
177
|
const match = files.find(f => f.startsWith(prefix) && f.endsWith('.txt'));
|
|
159
178
|
if (!match)
|
|
160
179
|
return undefined;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Durable pacing: every round appends `round_started` / `round_paced` to the loop's
|
|
14
14
|
* session log, so `LoopDriver.resume()`-style recovery is a fold over the log
|
|
15
|
-
*
|
|
15
|
+
* — zero new storage. A stateless host
|
|
16
16
|
* reads `wake_at_ms` from the fold and re-arms via its own cron/queue; an
|
|
17
17
|
* in-process host lets `run()` sleep inline.
|
|
18
18
|
*/
|
|
@@ -108,9 +108,8 @@ export class LoopDriver {
|
|
|
108
108
|
defaultAction: this.spec.defaultAction,
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
|
-
// With a RunGroup configured, run()
|
|
112
|
-
//
|
|
113
|
-
// then happens IN-KERNEL; the check above is the ungrouped backstop.
|
|
111
|
+
// With a RunGroup configured, run() reserves one round and the kernel reports its
|
|
112
|
+
// correlated local usage; the check above remains the ungrouped backstop.
|
|
114
113
|
try {
|
|
115
114
|
for await (const evt of this.runner.run({
|
|
116
115
|
sessionId: loopId,
|
|
@@ -164,10 +163,6 @@ export class LoopDriver {
|
|
|
164
163
|
reason: finalDecision.reason,
|
|
165
164
|
...(finalDecision.coercedFrom ? { coerced_from: finalDecision.coercedFrom } : {}),
|
|
166
165
|
});
|
|
167
|
-
// Lifetime governance: one round = one group charge on the rounds axis.
|
|
168
|
-
const group = this.runner.hostOptions.runGroup;
|
|
169
|
-
if (group)
|
|
170
|
-
await group.budgetStore.charge(group.id, { rounds: 1 });
|
|
171
166
|
if (finalDecision.action === "stop") {
|
|
172
167
|
return {
|
|
173
168
|
loopId, roundsCompleted: round, stopped: true, state: "stopped",
|
|
@@ -39,6 +39,7 @@ export declare class McpProxyPlane implements ExecutionPlane {
|
|
|
39
39
|
constructor(opts: {
|
|
40
40
|
servers: Record<string, McpServerConfig>;
|
|
41
41
|
vault: CredentialVault;
|
|
42
|
+
timeoutMs?: number;
|
|
42
43
|
});
|
|
43
44
|
/** Start all configured MCP server processes and discover their tool schemas. */
|
|
44
45
|
connect(): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { LocalExecutionPlane } from "./execution-plane.js";
|
|
4
4
|
import { formatToolError } from "../tools/errors.js";
|
|
5
|
+
import { operationAbortSignal } from "./reliability.js";
|
|
5
6
|
class McpConnection {
|
|
6
7
|
serverName;
|
|
7
8
|
config;
|
|
@@ -63,14 +64,28 @@ class McpConnection {
|
|
|
63
64
|
this.schemaNames.add(t.name);
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
request(method, params) {
|
|
67
|
+
request(method, params, signal) {
|
|
67
68
|
return new Promise((resolve, reject) => {
|
|
68
69
|
if (!this.child?.stdin) {
|
|
69
70
|
reject(new Error(`MCP server "${this.serverName}" not running`));
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
const id = this.nextId++;
|
|
73
|
-
|
|
74
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
75
|
+
const abort = () => {
|
|
76
|
+
this.pending.delete(id);
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("operation cancelled"));
|
|
79
|
+
};
|
|
80
|
+
this.pending.set(id, {
|
|
81
|
+
resolve: result => { cleanup(); resolve(result); },
|
|
82
|
+
reject: error => { cleanup(); reject(error); },
|
|
83
|
+
});
|
|
84
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
85
|
+
if (signal?.aborted) {
|
|
86
|
+
abort();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
74
89
|
const msg = { jsonrpc: "2.0", method, id };
|
|
75
90
|
if (params !== undefined)
|
|
76
91
|
msg.params = params;
|
|
@@ -87,10 +102,10 @@ class McpConnection {
|
|
|
87
102
|
}
|
|
88
103
|
schemas() { return this._schemas; }
|
|
89
104
|
hasSchema(name) { return this.schemaNames.has(name); }
|
|
90
|
-
async execute(call) {
|
|
105
|
+
async execute(call, signal) {
|
|
91
106
|
try {
|
|
92
107
|
const args = JSON.parse(call.arguments || "{}");
|
|
93
|
-
const result = await this.request("tools/call", { name: call.name, arguments: args });
|
|
108
|
+
const result = await this.request("tools/call", { name: call.name, arguments: args }, signal);
|
|
94
109
|
const text = (result.content ?? [])
|
|
95
110
|
.filter(c => c.type === "text")
|
|
96
111
|
.map(c => c.text ?? "")
|
|
@@ -192,8 +207,10 @@ export class McpProxyPlane {
|
|
|
192
207
|
}
|
|
193
208
|
const tasks = Array.from(groups.entries()).map(async ([conn, serverCalls]) => {
|
|
194
209
|
const results = [];
|
|
195
|
-
for (const call of serverCalls)
|
|
196
|
-
|
|
210
|
+
for (const call of serverCalls) {
|
|
211
|
+
const signal = operationAbortSignal(ctx.operation, this.opts.timeoutMs ?? 30_000);
|
|
212
|
+
results.push({ call, result: await conn.execute(call, signal) });
|
|
213
|
+
}
|
|
197
214
|
return results;
|
|
198
215
|
});
|
|
199
216
|
for (const settled of await Promise.all(tasks)) {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { GovernancePolicy } from "../governance.js";
|
|
2
2
|
export type OsProfileId = "native";
|
|
3
|
+
export interface SignalPolicy {
|
|
4
|
+
queueMax: number;
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
deadlineEscalation?: boolean;
|
|
7
|
+
}
|
|
3
8
|
export interface NativeOsProfile {
|
|
4
9
|
id: OsProfileId;
|
|
5
|
-
|
|
6
|
-
maxQueueSize?: number;
|
|
7
|
-
};
|
|
10
|
+
signalPolicy: SignalPolicy;
|
|
8
11
|
governancePolicy: GovernancePolicy;
|
|
9
12
|
}
|
|
10
|
-
/** Default
|
|
11
|
-
export declare const
|
|
12
|
-
maxQueueSize: number;
|
|
13
|
-
};
|
|
13
|
+
/** Default signal policy for native profile smoke tests. */
|
|
14
|
+
export declare const DEFAULT_NATIVE_SIGNAL_POLICY: SignalPolicy;
|
|
14
15
|
/** Permissive governance policy for native runs that do not need AskUser. */
|
|
15
16
|
export declare const DEFAULT_NATIVE_GOVERNANCE_POLICY: GovernancePolicy;
|
|
16
17
|
/** Default restrictive sandbox policy template requiring confirmation for modification/execution. */
|
|
@@ -22,9 +23,7 @@ export declare function assertNativeProfile(profile?: OsProfileId | NativeOsProf
|
|
|
22
23
|
/**
|
|
23
24
|
* Validates the declarative policies statically to prevent runtime crashes when loaded into the microkernel.
|
|
24
25
|
*/
|
|
25
|
-
export declare function validateDeclarativePolicy(govPolicy?: GovernancePolicy,
|
|
26
|
-
maxQueueSize?: number;
|
|
27
|
-
}): {
|
|
26
|
+
export declare function validateDeclarativePolicy(govPolicy?: GovernancePolicy, signalPolicy?: SignalPolicy): {
|
|
28
27
|
valid: boolean;
|
|
29
28
|
errors: string[];
|
|
30
29
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Default
|
|
2
|
-
export const
|
|
1
|
+
/** Default signal policy for native profile smoke tests. */
|
|
2
|
+
export const DEFAULT_NATIVE_SIGNAL_POLICY = { queueMax: 64 };
|
|
3
3
|
/** Permissive governance policy for native runs that do not need AskUser. */
|
|
4
4
|
export const DEFAULT_NATIVE_GOVERNANCE_POLICY = {
|
|
5
5
|
rules: [{ pattern: "*", action: "allow" }],
|
|
@@ -21,7 +21,7 @@ export function osProfile(profile = "native") {
|
|
|
21
21
|
throw new Error(`Unsupported OS profile: ${profile}`);
|
|
22
22
|
return {
|
|
23
23
|
id: "native",
|
|
24
|
-
|
|
24
|
+
signalPolicy: DEFAULT_NATIVE_SIGNAL_POLICY,
|
|
25
25
|
governancePolicy: DEFAULT_NATIVE_GOVERNANCE_POLICY,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
@@ -31,7 +31,7 @@ export function assertNativeProfile(profile = "native") {
|
|
|
31
31
|
if (resolved.id !== "native") {
|
|
32
32
|
throw new Error(`Unsupported OS profile: ${resolved.id}`);
|
|
33
33
|
}
|
|
34
|
-
const validation = validateDeclarativePolicy(resolved.governancePolicy, resolved.
|
|
34
|
+
const validation = validateDeclarativePolicy(resolved.governancePolicy, resolved.signalPolicy);
|
|
35
35
|
if (!validation.valid) {
|
|
36
36
|
throw new Error(`Invalid native OS profile: ${validation.errors.join("; ")}`);
|
|
37
37
|
}
|
|
@@ -40,7 +40,7 @@ export function assertNativeProfile(profile = "native") {
|
|
|
40
40
|
/**
|
|
41
41
|
* Validates the declarative policies statically to prevent runtime crashes when loaded into the microkernel.
|
|
42
42
|
*/
|
|
43
|
-
export function validateDeclarativePolicy(govPolicy,
|
|
43
|
+
export function validateDeclarativePolicy(govPolicy, signalPolicy) {
|
|
44
44
|
const errors = [];
|
|
45
45
|
if (govPolicy) {
|
|
46
46
|
if (!Array.isArray(govPolicy.rules)) {
|
|
@@ -57,11 +57,15 @@ export function validateDeclarativePolicy(govPolicy, attentionPolicy) {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
if (
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
if (signalPolicy) {
|
|
61
|
+
if (!Number.isInteger(signalPolicy.queueMax) || signalPolicy.queueMax <= 0) {
|
|
62
|
+
errors.push("SignalPolicy queueMax must be a positive integer");
|
|
63
|
+
}
|
|
64
|
+
if (signalPolicy.ttlMs !== undefined && (!Number.isInteger(signalPolicy.ttlMs) || signalPolicy.ttlMs <= 0)) {
|
|
65
|
+
errors.push("SignalPolicy ttlMs must be a positive integer");
|
|
66
|
+
}
|
|
67
|
+
if (signalPolicy.deadlineEscalation !== undefined && typeof signalPolicy.deadlineEscalation !== "boolean") {
|
|
68
|
+
errors.push("SignalPolicy deadlineEscalation must be a boolean");
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
return {
|
|
@@ -14,10 +14,29 @@ export interface OsSnapshot {
|
|
|
14
14
|
}>;
|
|
15
15
|
budgetExceeded: Array<{
|
|
16
16
|
turn: number;
|
|
17
|
+
operation_id: string;
|
|
18
|
+
reservation_id?: string;
|
|
17
19
|
budget: string;
|
|
18
20
|
}>;
|
|
21
|
+
budgetUsageReported: Array<{
|
|
22
|
+
turn: number;
|
|
23
|
+
operation_id: string;
|
|
24
|
+
reservation_id: string;
|
|
25
|
+
tokens: number;
|
|
26
|
+
subagents: number;
|
|
27
|
+
rounds: number;
|
|
28
|
+
}>;
|
|
29
|
+
cancellations: Array<{
|
|
30
|
+
turn: number;
|
|
31
|
+
operation_id: string;
|
|
32
|
+
reason: "user" | "deadline" | "lease_lost" | "host_shutdown";
|
|
33
|
+
pending_call_ids: string[];
|
|
34
|
+
}>;
|
|
19
35
|
signals: Array<{
|
|
20
36
|
turn: number;
|
|
37
|
+
operation_id: string;
|
|
38
|
+
delivery_id: string;
|
|
39
|
+
attempt: number;
|
|
21
40
|
signal_id: string;
|
|
22
41
|
disposition: string;
|
|
23
42
|
queue_depth: number;
|
|
@@ -8,8 +8,10 @@ const KERNEL_KINDS = new Set([
|
|
|
8
8
|
"suspended",
|
|
9
9
|
"resumed",
|
|
10
10
|
"tool_gated",
|
|
11
|
-
"
|
|
11
|
+
"signal_delivery_disposed",
|
|
12
12
|
"budget_exceeded",
|
|
13
|
+
"budget_usage_reported",
|
|
14
|
+
"operation_cancelled",
|
|
13
15
|
"checkpoint_taken",
|
|
14
16
|
"rollbacked",
|
|
15
17
|
"agent_process_changed",
|
|
@@ -23,6 +25,8 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
|
|
|
23
25
|
const snap = {
|
|
24
26
|
processByAgent: [],
|
|
25
27
|
budgetExceeded: [],
|
|
28
|
+
budgetUsageReported: [],
|
|
29
|
+
cancellations: [],
|
|
26
30
|
signals: [],
|
|
27
31
|
pageOutCount: 0,
|
|
28
32
|
pageInCount: 0,
|
|
@@ -73,11 +77,37 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
|
|
|
73
77
|
break;
|
|
74
78
|
}
|
|
75
79
|
case "budget_exceeded":
|
|
76
|
-
snap.budgetExceeded.push({
|
|
80
|
+
snap.budgetExceeded.push({
|
|
81
|
+
turn: event.turn,
|
|
82
|
+
operation_id: event.operation_id,
|
|
83
|
+
...(event.reservation_id ? { reservation_id: event.reservation_id } : {}),
|
|
84
|
+
budget: event.budget,
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
case "budget_usage_reported":
|
|
88
|
+
snap.budgetUsageReported.push({
|
|
89
|
+
turn: event.turn,
|
|
90
|
+
operation_id: event.operation_id,
|
|
91
|
+
reservation_id: event.reservation_id,
|
|
92
|
+
tokens: event.tokens,
|
|
93
|
+
subagents: event.subagents,
|
|
94
|
+
rounds: event.rounds,
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
case "operation_cancelled":
|
|
98
|
+
snap.cancellations.push({
|
|
99
|
+
turn: event.turn,
|
|
100
|
+
operation_id: event.operation_id,
|
|
101
|
+
reason: event.reason,
|
|
102
|
+
pending_call_ids: event.pending_call_ids,
|
|
103
|
+
});
|
|
77
104
|
break;
|
|
78
|
-
case "
|
|
105
|
+
case "signal_delivery_disposed":
|
|
79
106
|
snap.signals.push({
|
|
80
107
|
turn: event.turn,
|
|
108
|
+
operation_id: event.operation_id,
|
|
109
|
+
delivery_id: event.delivery_id,
|
|
110
|
+
attempt: event.attempt,
|
|
81
111
|
signal_id: event.signal_id,
|
|
82
112
|
disposition: event.disposition,
|
|
83
113
|
queue_depth: event.queue_depth,
|
|
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { tool } from "../tools/index.js";
|
|
4
4
|
import { LocalExecutionPlane } from "./execution-plane.js";
|
|
5
5
|
import { formatToolError } from "../tools/errors.js";
|
|
6
|
+
import { operationAbortSignal } from "./reliability.js";
|
|
6
7
|
/**
|
|
7
8
|
* ExecutionPlane that runs subprocesses with a sandbox directory as cwd.
|
|
8
9
|
* Extends LocalExecutionPlane with two built-in tools:
|
|
@@ -39,7 +40,7 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
|
|
|
39
40
|
}
|
|
40
41
|
return env;
|
|
41
42
|
}
|
|
42
|
-
runSubprocess(cmd, argv, cwd) {
|
|
43
|
+
runSubprocess(cmd, argv, cwd, operation) {
|
|
43
44
|
return new Promise(resolve => {
|
|
44
45
|
const chunks = [];
|
|
45
46
|
let totalBytes = 0;
|
|
@@ -48,7 +49,7 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
|
|
|
48
49
|
if (settled)
|
|
49
50
|
return;
|
|
50
51
|
settled = true;
|
|
51
|
-
|
|
52
|
+
signal.removeEventListener("abort", abort);
|
|
52
53
|
resolve({ output, isError });
|
|
53
54
|
};
|
|
54
55
|
const child = spawn(cmd, argv, {
|
|
@@ -56,10 +57,14 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
|
|
|
56
57
|
env: this.buildEnv(),
|
|
57
58
|
stdio: ["ignore", "pipe", "pipe"],
|
|
58
59
|
});
|
|
59
|
-
const
|
|
60
|
+
const signal = operationAbortSignal(operation, this.timeoutMs);
|
|
61
|
+
const abort = () => {
|
|
60
62
|
child.kill("SIGKILL");
|
|
61
|
-
settle(
|
|
62
|
-
}
|
|
63
|
+
settle(signal.reason instanceof Error ? signal.reason.message : "operation cancelled", true);
|
|
64
|
+
};
|
|
65
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
66
|
+
if (signal.aborted)
|
|
67
|
+
abort();
|
|
63
68
|
const capture = (chunk) => {
|
|
64
69
|
if (settled)
|
|
65
70
|
return;
|
|
@@ -89,9 +94,9 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
|
|
|
89
94
|
const cwd = ctx?.cwd ?? this.sandboxDir;
|
|
90
95
|
if (!ctx?.cwd)
|
|
91
96
|
await mkdir(this.sandboxDir, { recursive: true });
|
|
92
|
-
const { output, isError } = await this.runSubprocess("bash", ["-c", String(args.command)], cwd);
|
|
93
|
-
if (isError
|
|
94
|
-
|
|
97
|
+
const { output, isError } = await this.runSubprocess("bash", ["-c", String(args.command)], cwd, ctx?.operation);
|
|
98
|
+
if (isError)
|
|
99
|
+
throw new Error(output.trim() || "Process exited with non-zero status and produced no output.");
|
|
95
100
|
return output || "(no output)";
|
|
96
101
|
});
|
|
97
102
|
}
|
|
@@ -107,9 +112,9 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
|
|
|
107
112
|
const cwd = ctx?.cwd ?? this.sandboxDir;
|
|
108
113
|
if (!ctx?.cwd)
|
|
109
114
|
await mkdir(this.sandboxDir, { recursive: true });
|
|
110
|
-
const { output, isError } = await this.runSubprocess("node", ["-e", String(args.code)], cwd);
|
|
111
|
-
if (isError
|
|
112
|
-
|
|
115
|
+
const { output, isError } = await this.runSubprocess("node", ["-e", String(args.code)], cwd, ctx?.operation);
|
|
116
|
+
if (isError)
|
|
117
|
+
throw new Error(output.trim() || "Script exited with non-zero status and produced no output.");
|
|
113
118
|
return output || "(no output)";
|
|
114
119
|
});
|
|
115
120
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ReactionRecord {
|
|
2
|
+
personaId: string;
|
|
3
|
+
output: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ReactionCheckpointReceipt {
|
|
6
|
+
checkpointKey: string;
|
|
7
|
+
leaseToken: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ReactionCheckpointClaim extends ReactionCheckpointReceipt {
|
|
10
|
+
leaseExpiresAtMs: number;
|
|
11
|
+
plan?: string[];
|
|
12
|
+
outputs: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export type ReactionCheckpointClaimResult = {
|
|
15
|
+
status: "claimed";
|
|
16
|
+
claim: ReactionCheckpointClaim;
|
|
17
|
+
} | {
|
|
18
|
+
status: "completed";
|
|
19
|
+
reactions: ReactionRecord[];
|
|
20
|
+
} | {
|
|
21
|
+
status: "busy";
|
|
22
|
+
};
|
|
23
|
+
export interface ReactionCheckpointStore {
|
|
24
|
+
claim(checkpointKey: string, leaseMs?: number): Promise<ReactionCheckpointClaimResult>;
|
|
25
|
+
savePlan(receipt: ReactionCheckpointReceipt, personaIds: string[]): Promise<string[] | null>;
|
|
26
|
+
record(receipt: ReactionCheckpointReceipt, reaction: ReactionRecord): Promise<boolean>;
|
|
27
|
+
complete(receipt: ReactionCheckpointReceipt): Promise<boolean>;
|
|
28
|
+
release(receipt: ReactionCheckpointReceipt): Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
export interface InMemoryReactionCheckpointStoreOptions {
|
|
31
|
+
now?: () => number;
|
|
32
|
+
defaultLeaseMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Process-local reference implementation; durable stores implement the same atomic contract. */
|
|
35
|
+
export declare class InMemoryReactionCheckpointStore implements ReactionCheckpointStore {
|
|
36
|
+
private readonly opts;
|
|
37
|
+
private readonly states;
|
|
38
|
+
private leaseSeq;
|
|
39
|
+
constructor(opts?: InMemoryReactionCheckpointStoreOptions);
|
|
40
|
+
claim(checkpointKey: string, leaseMs?: number): Promise<ReactionCheckpointClaimResult>;
|
|
41
|
+
savePlan(receipt: ReactionCheckpointReceipt, personaIds: string[]): Promise<string[] | null>;
|
|
42
|
+
record(receipt: ReactionCheckpointReceipt, reaction: ReactionRecord): Promise<boolean>;
|
|
43
|
+
complete(receipt: ReactionCheckpointReceipt): Promise<boolean>;
|
|
44
|
+
release(receipt: ReactionCheckpointReceipt): Promise<boolean>;
|
|
45
|
+
private current;
|
|
46
|
+
private reactions;
|
|
47
|
+
}
|
|
48
|
+
export declare class ReactionInProgressError extends Error {
|
|
49
|
+
readonly checkpointKey: string;
|
|
50
|
+
constructor(checkpointKey: string);
|
|
51
|
+
}
|