@kasenri/dsh-orbit 0.5.1

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.
@@ -0,0 +1,167 @@
1
+ import { ORBIT_CAPABILITIES } from "./kernel.js";
2
+ import {} from "./types.js";
3
+ // ── DSH-native structured decision schemas ───────────────────────────────────
4
+ //
5
+ // The model submits its decision through the DSH structured-output protocol and
6
+ // the provider validates it against one of these schemas. They are written in
7
+ // the enforced JSON Schema subset (dsh-tools `assertObjectJsonSchema`), keep only
8
+ // format-level rules (types, required fields, legal enums), and leave Orbit's
9
+ // domain rules (plan size, correction depth, budget) to the checks below.
10
+ const CAPABILITY_ENUM = [...ORBIT_CAPABILITIES];
11
+ /** Plan steps are object-rooted; capabilities stay optional per step. */
12
+ export const COMMANDER_PLAN_SCHEMA = {
13
+ type: 'object',
14
+ additionalProperties: false,
15
+ required: ['summary', 'steps'],
16
+ properties: {
17
+ summary: { type: 'string', description: 'One-sentence plan summary.' },
18
+ steps: {
19
+ type: 'array',
20
+ description: '2-5 logical engineering steps.',
21
+ items: {
22
+ type: 'object',
23
+ additionalProperties: false,
24
+ required: ['goal'],
25
+ properties: {
26
+ id: { type: 'string', description: 'Stable id such as P0.' },
27
+ goal: { type: 'string' },
28
+ capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ };
34
+ export const COMMANDER_STEP_EVALUATE_SCHEMA = {
35
+ type: 'object',
36
+ additionalProperties: false,
37
+ required: ['decision'],
38
+ properties: {
39
+ decision: { type: 'string', enum: ['PASS_CURRENT_STEP', 'CORRECT_CURRENT_STEP', 'NEEDS_USER'] },
40
+ reason: { type: 'string' },
41
+ next_step_goal: { type: 'string' },
42
+ next_step_capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
43
+ },
44
+ };
45
+ export const COMMANDER_FINAL_EVALUATE_SCHEMA = {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ required: ['decision'],
49
+ properties: {
50
+ decision: { type: 'string', enum: ['SUCCESS', 'APPEND', 'NEEDS_USER'] },
51
+ summary: { type: 'string' },
52
+ next_step_goal: { type: 'string' },
53
+ next_step_capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
54
+ next_steps: {
55
+ type: 'array',
56
+ items: {
57
+ oneOf: [
58
+ { type: 'string' },
59
+ {
60
+ type: 'object',
61
+ additionalProperties: false,
62
+ required: ['goal'],
63
+ properties: {
64
+ goal: { type: 'string' },
65
+ capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ },
72
+ };
73
+ export const COMMANDER_STRATEGY_SCHEMA = {
74
+ type: 'object',
75
+ additionalProperties: false,
76
+ required: ['decision'],
77
+ properties: {
78
+ decision: { type: 'string', enum: ['KEEP_APPROACH', 'REPLACE_CURRENT_STEP', 'NEEDS_USER'] },
79
+ reason: { type: 'string' },
80
+ replacement_goal: { type: 'string' },
81
+ },
82
+ };
83
+ export const WATCHDOG_RUNTIME_SCHEMA = {
84
+ type: 'object',
85
+ additionalProperties: false,
86
+ required: ['decision'],
87
+ properties: {
88
+ decision: { type: 'string', enum: ['RESUME_CHILD', 'RESTART_STEP', 'NEEDS_USER', 'RUNTIME_BUG'] },
89
+ reason: { type: 'string' },
90
+ },
91
+ };
92
+ export const WATCHDOG_STRATEGY_SCHEMA = {
93
+ type: 'object',
94
+ additionalProperties: false,
95
+ required: ['question'],
96
+ properties: {
97
+ question: { type: 'string', description: 'The single strategy question to put to the Commander.' },
98
+ },
99
+ };
100
+ export const WATCHDOG_GUARD_SCHEMA = {
101
+ type: 'object',
102
+ additionalProperties: false,
103
+ required: ['decision'],
104
+ properties: {
105
+ decision: { type: 'string', enum: ['RETRY_DIFFERENTLY', 'NEEDS_USER'] },
106
+ instruction: { type: 'string' },
107
+ },
108
+ };
109
+ export const WATCHDOG_TIMEOUT_SCHEMA = {
110
+ type: 'object',
111
+ additionalProperties: false,
112
+ required: ['decision'],
113
+ properties: {
114
+ decision: { type: 'string', enum: ['EXTEND', 'INTERRUPT', 'NEEDS_USER'] },
115
+ reason: { type: 'string' },
116
+ },
117
+ };
118
+ export const ORBIT_DECISION_SCHEMAS = {
119
+ COMMANDER_PLAN_SCHEMA,
120
+ COMMANDER_STEP_EVALUATE_SCHEMA,
121
+ COMMANDER_FINAL_EVALUATE_SCHEMA,
122
+ COMMANDER_STRATEGY_SCHEMA,
123
+ WATCHDOG_RUNTIME_SCHEMA,
124
+ WATCHDOG_STRATEGY_SCHEMA,
125
+ WATCHDOG_GUARD_SCHEMA,
126
+ WATCHDOG_TIMEOUT_SCHEMA,
127
+ };
128
+ const STEP_DECISIONS = ['PASS_CURRENT_STEP', 'CORRECT_CURRENT_STEP', 'NEEDS_USER'];
129
+ const FINAL_DECISIONS = ['SUCCESS', 'APPEND', 'NEEDS_USER'];
130
+ export function assertCommanderDecision(decision, mode) {
131
+ const allowed = mode === 'FINAL_EVALUATE' ? FINAL_DECISIONS : STEP_DECISIONS;
132
+ if (!allowed.includes(decision.decision)) {
133
+ throw new Error(`COMMANDER_EVALUATION_DECISION_INVALID_FOR_MODE: ${mode} cannot return ${decision.decision}`);
134
+ }
135
+ return decision;
136
+ }
137
+ export function assertStrategyDecision(decision) {
138
+ const allowed = ['KEEP_APPROACH', 'REPLACE_CURRENT_STEP', 'NEEDS_USER'];
139
+ if (!allowed.includes(decision.decision)) {
140
+ throw new Error(`COMMANDER_STRATEGY_DECISION_INVALID: ${decision.decision}`);
141
+ }
142
+ if (decision.decision === 'REPLACE_CURRENT_STEP' && !decision.replacement_goal?.trim()) {
143
+ throw new Error('COMMANDER_STRATEGY_OUTPUT_INVALID: replacement_goal is required');
144
+ }
145
+ return decision;
146
+ }
147
+ export function assertTimeoutDecision(decision) {
148
+ const allowed = ['EXTEND', 'INTERRUPT', 'NEEDS_USER'];
149
+ if (!allowed.includes(decision.decision)) {
150
+ throw new Error(`COMMANDER_TIMEOUT_WATCHDOG_DECISION_INVALID: ${decision.decision}`);
151
+ }
152
+ return decision;
153
+ }
154
+ export function assertWatchdogDecision(decision) {
155
+ const allowed = ['RESUME_CHILD', 'RESTART_STEP', 'NEEDS_USER', 'RUNTIME_BUG'];
156
+ if (!allowed.includes(decision.decision)) {
157
+ throw new Error(`SMART_WATCHDOG_RUNTIME_DECISION_INVALID: ${decision.decision}`);
158
+ }
159
+ return decision;
160
+ }
161
+ export function assertGuardWatchdogDecision(decision) {
162
+ const allowed = ['RETRY_DIFFERENTLY', 'NEEDS_USER'];
163
+ if (!allowed.includes(decision.decision)) {
164
+ throw new Error(`SMART_WATCHDOG_GUARD_DECISION_INVALID: ${decision.decision}`);
165
+ }
166
+ return decision;
167
+ }
@@ -0,0 +1,364 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { collectTurnToolFacts } from "./evidence.js";
3
+ import { redactText, truncateSafe } from "./sanitize.js";
4
+ import { classifyTurnSettlement } from "./settlement.js";
5
+ const EXECUTOR_TURN_START_TIMEOUT_MS = 5_000;
6
+ function contentToText(blocks) {
7
+ if (!blocks)
8
+ return '';
9
+ return blocks
10
+ .map((block) => (block.type === 'text' ? block.text : `[${block.type}]`))
11
+ .join('\n');
12
+ }
13
+ /**
14
+ * Wire the Orbit supervisor to DeepSeek Harness native Agent/Subagent services.
15
+ *
16
+ * Roles keep the Pi-validated split:
17
+ * - Commander/Watchdog: one-shot children, cancelled through the launch signal
18
+ * plus `run.dispose()` (the real one-shot cancellation seam).
19
+ * - Executor: continuable child so a runtime restart can interrupt it and a
20
+ * resume can reuse the same child.
21
+ */
22
+ export class DshOrbitHost {
23
+ ctx;
24
+ ownedChildren = new Set();
25
+ interruptedChildren = new Set();
26
+ childParents = new Map();
27
+ nowFn;
28
+ sleepFn;
29
+ constructor(ctx, options = {}) {
30
+ this.ctx = ctx;
31
+ this.nowFn = options.now ?? (() => Date.now());
32
+ this.sleepFn = options.sleep ?? defaultSleep;
33
+ }
34
+ now() {
35
+ return this.nowFn();
36
+ }
37
+ sleep(ms, signal) {
38
+ return this.sleepFn(ms, signal);
39
+ }
40
+ async startRole(request) {
41
+ const parent = this.parent();
42
+ const prompt = [{ type: 'text', text: request.prompt }];
43
+ const agentOptions = {
44
+ provider: request.route.provider,
45
+ model: request.route.model,
46
+ ...(request.route.reasoningEffort ? { reasoningEffort: request.route.reasoningEffort } : {}),
47
+ ...(request.route.maxTokens ? { maxTokens: request.route.maxTokens } : {}),
48
+ };
49
+ if (request.role === 'executor') {
50
+ return this.startExecutor(parent, request, prompt, agentOptions);
51
+ }
52
+ return this.startOneShot(parent, request, prompt, agentOptions);
53
+ }
54
+ parent() {
55
+ const initiator = this.ctx.agents.currentInitiator();
56
+ if (initiator)
57
+ return initiator;
58
+ return this.ctx.agents.requireInitiator();
59
+ }
60
+ async startOneShot(parent, request, prompt, agentOptions) {
61
+ const controller = new AbortController();
62
+ const onAbort = () => controller.abort();
63
+ request.signal?.addEventListener('abort', onAbort, { once: true });
64
+ const run = (await this.ctx.subagents.start('spawn', {
65
+ label: request.label,
66
+ prompt,
67
+ parent,
68
+ signal: controller.signal,
69
+ agentOptions,
70
+ ...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
71
+ ...(request.outputSchema ? { outputSchema: request.outputSchema } : {}),
72
+ }));
73
+ this.registerChild(run.id, parent);
74
+ const result = run.result
75
+ .then((value) => ({
76
+ childId: run.id,
77
+ output: contentToText(value.output),
78
+ interrupted: value.stopReason !== 'completed',
79
+ ...(value.stopReason !== 'completed' ? { reason: value.stopReason } : {}),
80
+ ...(value.diagnostic ? { testSummary: [value.diagnostic] } : {}),
81
+ ...(value.structured !== undefined ? { structured: value.structured } : {}),
82
+ }))
83
+ .catch((error) => ({
84
+ childId: run.id,
85
+ output: '',
86
+ interrupted: true,
87
+ reason: error instanceof Error ? error.message : String(error),
88
+ }));
89
+ return {
90
+ childId: run.id,
91
+ result,
92
+ cancel: async (reason) => {
93
+ this.interruptedChildren.add(run.id);
94
+ controller.abort(new Error(reason));
95
+ await run.dispose();
96
+ this.forgetChild(run.id);
97
+ },
98
+ dispose: async () => {
99
+ await run.dispose();
100
+ this.forgetChild(run.id);
101
+ },
102
+ runtimeSnapshot: () => this.snapshotAgent(run.localAgent),
103
+ };
104
+ }
105
+ async startExecutor(parent, request, prompt, agentOptions) {
106
+ if (request.resumeOf) {
107
+ const existingId = request.resumeOf;
108
+ const existing = this.ctx.agents.get(existingId);
109
+ if (existing) {
110
+ const agent = existing;
111
+ this.interruptedChildren.delete(existingId);
112
+ const done = this.waitForExecutorSettlement(agent, existingId);
113
+ await this.ctx.subagents.sendMessage(parent, existingId, prompt, {
114
+ signal: new AbortController().signal,
115
+ });
116
+ return {
117
+ childId: existingId,
118
+ result: done,
119
+ cancel: async (reason) => this.interruptExecutor(existingId, reason),
120
+ dispose: async () => this.drainExecutor(parent, existingId),
121
+ runtimeSnapshot: () => this.snapshotAgent(agent),
122
+ };
123
+ }
124
+ }
125
+ const controller = new AbortController();
126
+ request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
127
+ const started = await this.ctx.subagents.startContinuable({
128
+ provider: 'spawn',
129
+ label: request.label,
130
+ request: {
131
+ prompt,
132
+ parent,
133
+ agentOptions,
134
+ ...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
135
+ },
136
+ signal: controller.signal,
137
+ });
138
+ const childId = String(started.childId);
139
+ this.registerChild(childId, parent);
140
+ const agent = this.ctx.agents.get(started.childId);
141
+ const result = agent
142
+ ? this.waitForExecutorSettlement(agent, childId)
143
+ : Promise.resolve({ childId, output: '', interrupted: true, reason: 'EXECUTOR_CHILD_MISSING' });
144
+ return {
145
+ childId,
146
+ result,
147
+ cancel: async (reason) => this.interruptExecutor(childId, reason),
148
+ dispose: async () => this.drainExecutor(parent, childId),
149
+ runtimeSnapshot: agent ? () => this.snapshotAgent(agent) : undefined,
150
+ };
151
+ }
152
+ registerChild(childId, parent) {
153
+ this.ownedChildren.add(childId);
154
+ this.childParents.set(childId, parent);
155
+ }
156
+ forgetChild(childId) {
157
+ this.ownedChildren.delete(childId);
158
+ this.interruptedChildren.delete(childId);
159
+ this.childParents.delete(childId);
160
+ }
161
+ async interruptExecutor(childId, reason) {
162
+ this.interruptedChildren.add(childId);
163
+ const parent = this.childParents.get(childId);
164
+ this.ctx.subagents.interrupt(childId, parent ? { kind: 'ancestor', agent: parent } : { kind: 'user', parentSessionId: childId });
165
+ void reason;
166
+ }
167
+ async drainExecutor(parent, childId) {
168
+ try {
169
+ await this.ctx.subagents.drainContinuableChildren(parent, [childId]);
170
+ }
171
+ catch {
172
+ // already released
173
+ }
174
+ this.forgetChild(childId);
175
+ }
176
+ async waitForExecutorSettlement(agent, childId) {
177
+ await this.waitForTurnOrIdle(agent);
178
+ const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
179
+ const classified = classifyTurnSettlement(events);
180
+ const output = this.readFinalOutput(agent);
181
+ const telemetry = await this.snapshotAgent(agent);
182
+ // Evidence is read from the settled turn's own events; a resumed executor
183
+ // therefore reports only the turn that just finished, never an earlier one.
184
+ const toolEvidence = collectTurnToolFacts(events);
185
+ const evidence = {
186
+ settlement: classified.settlement,
187
+ ...(toolEvidence.length > 0 ? { toolEvidence } : {}),
188
+ };
189
+ if (this.interruptedChildren.has(childId)) {
190
+ return { childId, output, interrupted: true, reason: 'EXECUTOR_INTERRUPTED', telemetry, ...evidence };
191
+ }
192
+ switch (classified.settlement) {
193
+ case 'completed':
194
+ return { childId, output, interrupted: false, telemetry, ...evidence };
195
+ case 'aborted':
196
+ return {
197
+ childId,
198
+ output,
199
+ interrupted: true,
200
+ reason: `EXECUTOR_ABORTED${classified.cancelCause ? `:${classified.cancelCause}` : ''}`,
201
+ telemetry,
202
+ ...evidence,
203
+ };
204
+ case 'error':
205
+ return {
206
+ childId,
207
+ output,
208
+ interrupted: true,
209
+ reason: `EXECUTOR_ERROR: ${redactText(classified.errorMessage ?? 'unknown failure')}`,
210
+ telemetry,
211
+ ...evidence,
212
+ };
213
+ case 'blocked':
214
+ return { childId, output, interrupted: true, reason: 'EXECUTOR_BLOCKED', telemetry, ...evidence };
215
+ case 'max-tokens':
216
+ return { childId, output, interrupted: true, reason: 'EXECUTOR_MAX_TOKENS', telemetry, ...evidence };
217
+ default:
218
+ return { childId, output, interrupted: true, reason: 'EXECUTOR_NO_TURN', telemetry, ...evidence };
219
+ }
220
+ }
221
+ async waitForTurnOrIdle(agent) {
222
+ const deadline = this.nowFn() + EXECUTOR_TURN_START_TIMEOUT_MS;
223
+ while (this.nowFn() < deadline && !this.hasTurnStarted(agent)) {
224
+ await this.sleepFn(20);
225
+ }
226
+ await agent.whenIdle?.();
227
+ }
228
+ hasTurnStarted(agent) {
229
+ const events = agent.session?.snapshotEvents?.() ?? [];
230
+ return events.some((event) => event.type === 'turn/start');
231
+ }
232
+ readFinalOutput(agent) {
233
+ const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
234
+ for (let index = events.length - 1; index >= 0; index -= 1) {
235
+ const event = events[index];
236
+ if (!event || event.type !== 'assistant/message')
237
+ continue;
238
+ const data = event.data;
239
+ const text = contentToText(data?.message?.content ?? data?.content);
240
+ if (text.trim())
241
+ return text;
242
+ }
243
+ return '';
244
+ }
245
+ /** Bounded telemetry for the exact agent behind a handle. */
246
+ async snapshotAgent(agent) {
247
+ if (!agent)
248
+ return { status: 'unknown' };
249
+ const events = agent.session?.snapshotEvents?.() ?? [];
250
+ const openCalls = new Map();
251
+ let turnCount = 0;
252
+ let toolCount = 0;
253
+ let lastAssistant = '';
254
+ for (const event of events) {
255
+ if (event.type === 'turn/end')
256
+ turnCount += 1;
257
+ if (event.type === 'tool/call') {
258
+ toolCount += 1;
259
+ const data = event.data;
260
+ if (data?.callId)
261
+ openCalls.set(data.callId, data.name ?? 'unknown');
262
+ }
263
+ if (event.type === 'tool/result') {
264
+ const data = event.data;
265
+ if (data?.callId)
266
+ openCalls.delete(data.callId);
267
+ }
268
+ if (event.type === 'assistant/message') {
269
+ const data = event.data;
270
+ const text = contentToText(data?.message?.content ?? data?.content);
271
+ if (text.trim())
272
+ lastAssistant = text;
273
+ }
274
+ }
275
+ const currentTool = [...openCalls.values()].pop();
276
+ return {
277
+ status: agent.status ?? 'unknown',
278
+ activity_state: agent.status ?? 'unknown',
279
+ turn_count: turnCount,
280
+ tool_count: toolCount,
281
+ ...(currentTool ? { current_tool: currentTool } : {}),
282
+ ...(lastAssistant ? { recent_output: truncateSafe(lastAssistant, 1500) } : {}),
283
+ };
284
+ }
285
+ async interruptRole(handle, reason) {
286
+ if (handle.cancel) {
287
+ await handle.cancel(reason);
288
+ return;
289
+ }
290
+ if (handle.childId)
291
+ await this.interruptExecutor(handle.childId, reason);
292
+ }
293
+ async releaseRole(handle) {
294
+ if (handle.dispose) {
295
+ await handle.dispose();
296
+ return;
297
+ }
298
+ if (handle.childId) {
299
+ const parent = this.childParents.get(handle.childId);
300
+ if (parent)
301
+ await this.drainExecutor(parent, handle.childId);
302
+ }
303
+ }
304
+ hasTool(name) {
305
+ // Standard profiles mount their tool composition on the agent plane
306
+ // (agent presets), so the visible set must resolve against the initiating
307
+ // agent's scope. Without an initiator this falls back to the global view.
308
+ const agent = this.ctx.agents.currentInitiator();
309
+ return this.ctx.tools.get(name, agent) !== undefined;
310
+ }
311
+ /**
312
+ * Mutation ownership is about top-level autonomous drivers, not about every
313
+ * running agent. The calling parent, Orbit's own children, and ordinary
314
+ * conversational/read-only agents are never competitors. Goal is the one
315
+ * DSH-native driver with a readable active state; ralph/workflow are blocked
316
+ * at tool start by the Orbit mutation guard.
317
+ */
318
+ async otherMutationDrivers(cwd) {
319
+ void cwd;
320
+ const drivers = [];
321
+ const initiator = this.ctx.agents.currentInitiator();
322
+ // `ctx.reflect.get` is the official service lookup that does not require an
323
+ // inject declaration, so Orbit stays loadable in profiles without dsh-goal.
324
+ const reflect = this.ctx.reflect;
325
+ const goals = reflect?.get('goals');
326
+ if (goals && initiator) {
327
+ try {
328
+ const goal = goals.get(initiator);
329
+ if (goal?.phase === 'active')
330
+ drivers.push('goal');
331
+ }
332
+ catch {
333
+ // goal service is present but not readable for this initiator
334
+ }
335
+ }
336
+ return drivers;
337
+ }
338
+ changedFiles(cwd) {
339
+ try {
340
+ const output = execFileSync('git', ['-C', cwd, 'status', '--porcelain'], {
341
+ encoding: 'utf8',
342
+ timeout: 5000,
343
+ stdio: ['ignore', 'pipe', 'ignore'],
344
+ });
345
+ return output
346
+ .split('\n')
347
+ .map((line) => line.slice(3).trim())
348
+ .filter((line) => line.length > 0 && !line.startsWith('.cx/'));
349
+ }
350
+ catch {
351
+ return [];
352
+ }
353
+ }
354
+ }
355
+ function defaultSleep(ms, signal) {
356
+ return new Promise((resolve, reject) => {
357
+ const timer = setTimeout(resolve, ms);
358
+ const onAbort = () => {
359
+ clearTimeout(timer);
360
+ reject(new Error('ORBIT_ABORTED'));
361
+ };
362
+ signal?.addEventListener('abort', onAbort, { once: true });
363
+ });
364
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Bounded execution evidence assembled from real DSH session events.
3
+ *
4
+ * Everything in a bundle comes from data Orbit already has when a child
5
+ * settles: current-turn `tool/call` + `tool/result` events, the durable turn
6
+ * settlement, the executor's final text, real workspace changes, and agent
7
+ * telemetry. Nothing is inferred, nothing is re-run, and no extra model call
8
+ * is made. Bundles are sanitized and bounded before they reach a prompt, and
9
+ * they are never persisted into `.cx/state.json` as a whole.
10
+ */
11
+ import { redactText, redactValue } from "./sanitize.js";
12
+ export const EVIDENCE_LIMITS = {
13
+ changedFiles: 50,
14
+ toolEvidence: 20,
15
+ testEvidence: 10,
16
+ entry: 400,
17
+ executorSummary: 2000,
18
+ total: 8000,
19
+ };
20
+ const TEST_COMMAND_PATTERN = /(?:\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b|\bnode\s+--test\b|\bvitest\b|\bjest\b|\bpytest\b|\bcargo\s+test\b|\bgo\s+test\b|\bmake\s+test\b)/u;
21
+ /** True when a real command string is recognizably a test invocation. */
22
+ export function isTestCommand(command) {
23
+ return TEST_COMMAND_PATTERN.test(command);
24
+ }
25
+ /** Strictly bound and redact one string without exceeding `max` characters. */
26
+ function bounded(value, max) {
27
+ const redacted = redactText(value);
28
+ if (redacted.length <= max)
29
+ return redacted;
30
+ const marker = '...[truncated]';
31
+ return `${redacted.slice(0, Math.max(0, max - marker.length))}${marker}`;
32
+ }
33
+ function eventTurn(event) {
34
+ const turn = event.data?.turn;
35
+ return typeof turn === 'number' ? turn : undefined;
36
+ }
37
+ /** The most recent turn boundary in the log (closed or open), if any. */
38
+ function currentTurnOf(events) {
39
+ for (let index = events.length - 1; index >= 0; index -= 1) {
40
+ const event = events[index];
41
+ if (event?.type !== 'turn/end' && event?.type !== 'turn/start')
42
+ continue;
43
+ const turn = eventTurn(event);
44
+ if (turn !== undefined)
45
+ return turn;
46
+ }
47
+ return undefined;
48
+ }
49
+ function commandOf(argumentsJson) {
50
+ if (!argumentsJson)
51
+ return undefined;
52
+ try {
53
+ const parsed = JSON.parse(argumentsJson);
54
+ return typeof parsed.command === 'string' && parsed.command.length > 0 ? parsed.command : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Extract tool facts for the most recent turn only, so a resumed executor
62
+ * never mixes a previous turn's activity into the current settlement.
63
+ */
64
+ export function collectTurnToolFacts(events) {
65
+ const turn = currentTurnOf(events);
66
+ if (turn === undefined)
67
+ return [];
68
+ const results = new Map();
69
+ for (const event of events) {
70
+ if (event.type !== 'tool/result' || eventTurn(event) !== turn)
71
+ continue;
72
+ const data = event.data;
73
+ const block = data?.message?.content?.[0];
74
+ if (!data || !block?.toolCallId)
75
+ continue;
76
+ const isError = data.error !== undefined || block.isError === true;
77
+ const identity = data.error ? [data.error.name, data.error.code].filter(Boolean).join(':') : '';
78
+ results.set(block.toolCallId, {
79
+ status: isError ? 'error' : 'ok',
80
+ ...(identity ? { detail: identity } : {}),
81
+ });
82
+ }
83
+ const facts = [];
84
+ for (const event of events) {
85
+ if (event.type !== 'tool/call' || eventTurn(event) !== turn)
86
+ continue;
87
+ const data = event.data;
88
+ if (!data?.name)
89
+ continue;
90
+ const result = data.callId ? results.get(data.callId) : undefined;
91
+ const command = commandOf(data.arguments);
92
+ const detail = result?.detail ?? command;
93
+ facts.push({
94
+ name: data.name,
95
+ status: result?.status ?? 'unknown',
96
+ ...(command ? { command } : {}),
97
+ ...(detail ? { detail } : {}),
98
+ });
99
+ }
100
+ return facts;
101
+ }
102
+ function boundTelemetry(telemetry) {
103
+ const redacted = redactValue(telemetry);
104
+ const out = {};
105
+ for (const [key, value] of Object.entries(redacted)) {
106
+ out[key] = typeof value === 'string' ? bounded(value, EVIDENCE_LIMITS.entry) : value;
107
+ }
108
+ return out;
109
+ }
110
+ /** Build one bounded, sanitized bundle from already-available execution facts. */
111
+ export function buildEvidenceBundle(input) {
112
+ const changedFiles = [];
113
+ for (const path of input.changedFiles ?? []) {
114
+ const entry = bounded(path.trim(), EVIDENCE_LIMITS.entry);
115
+ if (entry && !changedFiles.includes(entry))
116
+ changedFiles.push(entry);
117
+ if (changedFiles.length >= EVIDENCE_LIMITS.changedFiles)
118
+ break;
119
+ }
120
+ const tools = [];
121
+ for (const tool of (input.tools ?? []).slice(0, EVIDENCE_LIMITS.toolEvidence)) {
122
+ tools.push({
123
+ name: bounded(tool.name, EVIDENCE_LIMITS.entry),
124
+ status: tool.status,
125
+ ...(tool.detail ? { detail: bounded(tool.detail, EVIDENCE_LIMITS.entry) } : {}),
126
+ });
127
+ }
128
+ const tests = [];
129
+ for (const tool of input.tools ?? []) {
130
+ if (tool.command === undefined || !isTestCommand(tool.command))
131
+ continue;
132
+ tests.push({ command: bounded(tool.command, EVIDENCE_LIMITS.entry), status: tool.status });
133
+ if (tests.length >= EVIDENCE_LIMITS.testEvidence)
134
+ break;
135
+ }
136
+ const summary = input.executorOutput ? bounded(input.executorOutput, EVIDENCE_LIMITS.executorSummary) : '';
137
+ return {
138
+ ...(input.settlement ? { settlement: input.settlement } : {}),
139
+ ...(summary ? { executor_summary: summary } : {}),
140
+ changed_files: changedFiles,
141
+ tools,
142
+ tests,
143
+ ...(input.telemetry ? { telemetry: boundTelemetry(input.telemetry) } : {}),
144
+ };
145
+ }
146
+ /** Render a bundle for a prompt, hard-capped at the total evidence budget. */
147
+ export function formatEvidenceBundle(bundle) {
148
+ return bounded(JSON.stringify(bundle), EVIDENCE_LIMITS.total);
149
+ }