pi-better-subagents 0.5.0 → 0.6.0
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 +13 -1
- package/agent-commands.ts +26 -7
- package/agent-inspection.ts +4 -1
- package/batch.mjs +7 -0
- package/callback-fields.ts +4 -1
- package/catalog-ui.ts +167 -0
- package/child-incidents.ts +22 -6
- package/child-steer.ts +102 -0
- package/cleanup.ts +18 -2
- package/config.json +1 -0
- package/config.ts +5 -1
- package/delegation.ts +22 -0
- package/docs/agent-catalog-operations.md +2 -0
- package/docs/agent-catalog.md +9 -0
- package/docs/failure-observations.md +17 -4
- package/failures.ts +38 -5
- package/incident-model.ts +64 -53
- package/index.ts +314 -16
- package/list.mjs +4 -1
- package/output-payload.ts +151 -56
- package/package.json +4 -4
- package/parse.ts +6 -0
- package/permission-policy.ts +6 -4
- package/registry.ts +62 -1
- package/roles/role.architect.md +3 -3
- package/roles/role.developer.md +3 -3
- package/roles/role.explorer.md +3 -4
- package/roles/role.product-manager.md +3 -4
- package/roles/role.researcher.md +3 -4
- package/roles/role.reviewer.md +3 -3
- package/shared-callback-batcher.ts +34 -3
- package/shared-failure-observations.ts +378 -47
- package/shared-log-utils.ts +114 -0
- package/shared-navigator.ts +12 -10
- package/shared-sandbox-core.ts +660 -17
- package/shared-task-sandbox.ts +10 -5
- package/spawn.ts +3 -1
- package/task-policy.ts +17 -5
- package/timing.ts +425 -0
- package/tools.ts +52 -14
|
@@ -14,7 +14,7 @@ Each run retains an append-only `failures.jsonl` journal. Nothing is deleted fro
|
|
|
14
14
|
| `Observation incomplete` | Missing, corrupt, truncated, or unreadable evidence. |
|
|
15
15
|
| `Recovered` / `Superseded` | Closed. Counted in history, never shown as active. |
|
|
16
16
|
|
|
17
|
-
A completed run reports lifecycle and incidents as separate facts,
|
|
17
|
+
A completed run reports lifecycle and incidents as separate facts. When a page is small, incident rows are dropped before notes, and the correctness note is kept whole whenever it fits. For example:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
status=completed
|
|
@@ -22,11 +22,22 @@ status=completed
|
|
|
22
22
|
Work correctness was not inferred from lifecycle alone.
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## What the parent sees by default
|
|
26
|
+
|
|
27
|
+
Only incidents that need action are listed: `Action required` and `Observation incomplete`. Unclassified and expected failures are counted as history, never listed or paged by default. A completed run with nothing to act on says so in one line, with no incident cursor:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
No failures need action · 8 unclassified tool errors · 2 expected (history)
|
|
31
|
+
Work correctness was not inferred from lifecycle alone.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass `history: true` to `subagent_result` or `subagent_output` to page every incident, including recovered and superseded ones. Rows are compact: a tool's result JSON is shown as its text, the excerpt is capped at 120 bytes, and evidence reads `output.log#byte=N`. `mode: "raw"` keeps the whole excerpt and the full log path.
|
|
35
|
+
|
|
25
36
|
## When the parent is notified
|
|
26
37
|
|
|
27
38
|
- **While the child runs**, a single tool error does not wake the parent; the child owns it. The parent is woken, after a 60-second grace period, only for non-tool failures, for an operation that failed three times with no recovery, and for incidents the child marked `open`. Each incident is delivered once. Observation gaps (for example an oversized log record) stay visible on every surface and are delivered with the completion or health callback, since the parent cannot act on them mid-run.
|
|
28
39
|
- **At completion**, every still-unresolved incident is reported once in the ordinary completion callback: actionable incidents in full, unclassified tool failures as a count. Incidents a running notification already delivered are counted, not repeated.
|
|
29
|
-
- **Orphaned and lost** health callbacks carry the same facts once.
|
|
40
|
+
- **Orphaned and lost** health callbacks carry the same facts once. After an orphaned or lost run's health callback, observation gaps found later are delivered promptly, once, since no further callback may follow.
|
|
30
41
|
|
|
31
42
|
Every notification renders only its pending incidents. Delivery receipts are independent of recovery and are written after handoff; `callback:false` suppresses notifications without hiding inspection evidence.
|
|
32
43
|
|
|
@@ -34,7 +45,7 @@ Every notification renders only its pending incidents. Delivery receipts are ind
|
|
|
34
45
|
|
|
35
46
|
Automatic recovery still requires an exact retry: the same tool name, arguments, and working directory, started after the failure. Parallel or unrelated successes cannot clear an incident, and neither can exit 0 of another command or anything an assistant writes.
|
|
36
47
|
|
|
37
|
-
Sandboxed children (the task runtime) get two structured additions. The parent honours them only for runs
|
|
48
|
+
Sandboxed children (the task runtime) get two structured additions. The parent honours them only for runs it launched on the task runtime: `meta.taskRuntime` and a parent-authored provenance record written before the child starts, outside the run directory, where the confined child cannot write. For any other run, intent-looking arguments are ordinary arguments and `failure_disposition` calls are ignored. If the metadata cannot be read for a moment, the parent waits and reads it again rather than falling back to the exact rule for the rest of the run. The wait is bounded: when the run ends, or after 30 seconds, the log is scanned under the exact-retry rule so real failures stay visible, with an `Observation incomplete` note that the metadata could not be read.
|
|
38
49
|
|
|
39
50
|
- `bash` accepts optional `operationId`, `attemptId`, and `expectedExitCodes`, validated before the command runs.
|
|
40
51
|
- `operationId` names one logical operation across modified retries. A later success with the same `operationId` (a changed scope, timeout, or flag) recovers the earlier failure automatically. Without it, the exact rule applies. `attemptId` names one execution for use as evidence and never changes operation identity.
|
|
@@ -47,9 +58,11 @@ Sandboxed children (the task runtime) get two structured additions. The parent h
|
|
|
47
58
|
|
|
48
59
|
A `bash` call whose intent is rejected (malformed fields, or a reused `attemptId`) never runs. It is recorded as its own unclassified observation, not as a failure of the command it named, so it cannot make that operation look stuck.
|
|
49
60
|
|
|
61
|
+
Reuse is checked against the child's whole session record, every branch, which is also what the parent scans (the whole log). A reuse the parent sees is always one the child refused, so a real failed run is never filed as a rejected intent.
|
|
62
|
+
|
|
50
63
|
Targets may be incident ids, the `attemptId` or `operationId` the child declared, or tool call ids. Unknown, already-disposed, evidence-free, or partially invalid requests are rejected whole, with the open incidents listed, and nothing is written. The parent replays the same validation from the child's log before it appends the disposition to the journal, so dispositions survive reload and replay exactly once.
|
|
51
64
|
|
|
52
|
-
Unconfined children keep the exact-retry rule and
|
|
65
|
+
Unconfined children keep the exact-retry rule, and this is intentional: an unconfined child can rewrite its own log and metadata, so the parent cannot trust intent or dispositions it reports. Background tasks, which the parent agent launches itself, accept `operation_id` and `expected_exit_codes` directly (see the background-tasks README).
|
|
53
66
|
|
|
54
67
|
## Evidence health
|
|
55
68
|
|
package/failures.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { logPathFor,
|
|
3
|
+
import { logPathFor, runDir, taskRuntimeTrust } from "./registry.ts";
|
|
4
4
|
import { activeFailures, disposeIncidents, failureIdentity, formatFailureSummary, formatTerminalFailureFacts, markFailureAttentionDelivered,
|
|
5
5
|
observeFailures, pendingFailureAttention, readFailureState, type FailureState } from "./shared-failure-observations.ts";
|
|
6
6
|
import { evidenceText, foldToolEnd, foldToolStart, newIncidentModel, toolOperation, type IncidentModel, type IncidentSink } from "./incident-model.ts";
|
|
@@ -10,10 +10,14 @@ export const failurePath = (id: string) => join(runDir(id), "failures.jsonl");
|
|
|
10
10
|
export const readRunFailures = (id: string): FailureState => readFailureState(failurePath(id));
|
|
11
11
|
interface Scan { offset: number; head: string; identity: string; model: IncidentModel; retry?: number }
|
|
12
12
|
const scans = new Map<string, Scan>();
|
|
13
|
+
/** When each run's trust first could not be read, for the bounded wait (#325). */
|
|
14
|
+
const trustUnknownSince = new Map<string, number>();
|
|
15
|
+
/** How long a running scan waits for unreadable trust before scanning under the exact-retry rule. */
|
|
16
|
+
export const TRUST_WAIT_MS = 30_000;
|
|
13
17
|
/** Drop an in-memory scan cursor (e.g. on reload); the journal remains authoritative. */
|
|
14
18
|
export function resetFailureScanCursor(id?: string): void {
|
|
15
|
-
if (id === undefined) scans.clear();
|
|
16
|
-
else scans.delete(id);
|
|
19
|
+
if (id === undefined) { scans.clear(); trustUnknownSince.clear(); }
|
|
20
|
+
else { scans.delete(id); trustUnknownSince.delete(id); }
|
|
17
21
|
}
|
|
18
22
|
const CHUNK = 64 * 1024;
|
|
19
23
|
const MAX_LINE = 2 * 1024 * 1024;
|
|
@@ -97,8 +101,37 @@ export function collectRunFailures(id: string, cwd: string, terminal = false): F
|
|
|
97
101
|
scan = undefined;
|
|
98
102
|
}
|
|
99
103
|
// Structured intent and dispositions are honoured only for runs launched on the trusted task
|
|
100
|
-
// runtime, whose bash and failure_disposition tools are the guarded ones
|
|
101
|
-
|
|
104
|
+
// runtime, whose bash and failure_disposition tools are the guarded ones. Trust needs the
|
|
105
|
+
// parent-authored provenance record as well as `meta.taskRuntime` (#325). It is decided once
|
|
106
|
+
// per scan model, and only from a definite answer: while metadata or provenance cannot be read
|
|
107
|
+
// the scan waits (nothing is folded, so a brief read failure pins nothing to the exact rule).
|
|
108
|
+
// The wait is bounded: at a terminal read, or once trust has been unreadable for
|
|
109
|
+
// TRUST_WAIT_MS, the log is scanned under the exact-retry rule (untrusted) so real failures
|
|
110
|
+
// stay visible, and an observation gap says the metadata could not be read.
|
|
111
|
+
if (!scan) {
|
|
112
|
+
const trust = taskRuntimeTrust(id);
|
|
113
|
+
if (trust === "unknown") {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const since = trustUnknownSince.get(id) ?? now;
|
|
116
|
+
trustUnknownSince.set(id, since);
|
|
117
|
+
while (trustUnknownSince.size > 256) trustUnknownSince.delete(trustUnknownSince.keys().next().value!);
|
|
118
|
+
const prior = readRunFailures(id);
|
|
119
|
+
const gap = prior.observations[failureIdentity("run-metadata")];
|
|
120
|
+
const waited = terminal || now - since >= TRUST_WAIT_MS;
|
|
121
|
+
if (waited && !(gap && gap.status === "unresolved")) {
|
|
122
|
+
observeFailures(path, [{ id: failureIdentity("run-metadata-unreadable", gap?.id ?? "initial"), operation: "run-metadata", kind: "incomplete",
|
|
123
|
+
summary: "Run metadata could not be read; tool failures are scanned under the exact-retry rule" }]);
|
|
124
|
+
}
|
|
125
|
+
if (!waited) return prior;
|
|
126
|
+
scan = { offset: 0, head, identity, model: newIncidentModel(false) };
|
|
127
|
+
} else {
|
|
128
|
+
trustUnknownSince.delete(id);
|
|
129
|
+
scan = { offset: 0, head, identity, model: newIncidentModel(trust === "trusted") };
|
|
130
|
+
const deferred = readRunFailures(id).observations[failureIdentity("run-metadata")];
|
|
131
|
+
if (deferred && deferred.status === "unresolved") observeFailures(path, [{ id: `run-metadata-readable:${deferred.id}`,
|
|
132
|
+
operation: "run-metadata", kind: "recovered", incidents: [deferred.id] }]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
102
135
|
scan.head = head;
|
|
103
136
|
let position = scan.offset;
|
|
104
137
|
let start = position;
|
package/incident-model.ts
CHANGED
|
@@ -8,46 +8,12 @@
|
|
|
8
8
|
* `operationId` or the exact tool/arguments/cwd hash; expected exits require a code
|
|
9
9
|
* declared on the attempt before it ran and a structured exit code from the tool.
|
|
10
10
|
*/
|
|
11
|
-
import { failureIdentity, findIncident, validateDisposition, INCIDENT_DISPOSITIONS,
|
|
12
|
-
type FailureEvent, type FailureState, type IncidentDisposition } from "./shared-failure-observations.ts";
|
|
11
|
+
import { failureIdentity, findIncident, validateDisposition, INCIDENT_DISPOSITIONS, INTENT_ID_PATTERN, MAX_EXPECTED_EXIT_CODES,
|
|
12
|
+
REJECTED_INTENT_CATEGORY, readCommandIntent, withoutAbsentIntent, type CommandIntent, type FailureEvent, type FailureState, type IncidentDisposition } from "./shared-failure-observations.ts";
|
|
13
13
|
|
|
14
14
|
export const DISPOSITION_TOOL = "failure_disposition";
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
const INTENT_ID = new RegExp(INTENT_ID_PATTERN);
|
|
18
|
-
export const MAX_EXPECTED_EXIT_CODES = 16;
|
|
19
|
-
|
|
20
|
-
export interface CommandIntent {
|
|
21
|
-
/** Stable across caller-declared modified retries of one logical operation. */
|
|
22
|
-
operationId?: string;
|
|
23
|
-
/** Names one concrete execution; evidence identity, never an operation match. */
|
|
24
|
-
attemptId?: string;
|
|
25
|
-
/** Non-zero exit codes declared intentional before execution. */
|
|
26
|
-
expectedExitCodes?: number[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Validate structured intent fields. Absent fields are fine; malformed ones are an error. */
|
|
30
|
-
export function readCommandIntent(args: unknown): { intent: CommandIntent; error?: string } {
|
|
31
|
-
const input = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
|
|
32
|
-
const intent: CommandIntent = {};
|
|
33
|
-
for (const key of ["operationId", "attemptId"] as const) {
|
|
34
|
-
const value = input[key];
|
|
35
|
-
if (value === undefined) continue;
|
|
36
|
-
if (typeof value !== "string" || !INTENT_ID.test(value)) {
|
|
37
|
-
return { intent: {}, error: `${key} must match ${INTENT_ID_PATTERN}` };
|
|
38
|
-
}
|
|
39
|
-
intent[key] = value;
|
|
40
|
-
}
|
|
41
|
-
const codes = input.expectedExitCodes;
|
|
42
|
-
if (codes !== undefined) {
|
|
43
|
-
if (!Array.isArray(codes) || codes.length === 0 || codes.length > MAX_EXPECTED_EXIT_CODES ||
|
|
44
|
-
!codes.every((code) => Number.isInteger(code) && code >= 1 && code <= 255) || new Set(codes).size !== codes.length) {
|
|
45
|
-
return { intent: {}, error: `expectedExitCodes must be 1-${MAX_EXPECTED_EXIT_CODES} distinct integers from 1 to 255` };
|
|
46
|
-
}
|
|
47
|
-
intent.expectedExitCodes = [...codes] as number[];
|
|
48
|
-
}
|
|
49
|
-
return { intent };
|
|
50
|
-
}
|
|
15
|
+
// The intent validator is shared with background tasks through the vendored failure-observations module (#325).
|
|
16
|
+
export { INTENT_ID_PATTERN, MAX_EXPECTED_EXIT_CODES, readCommandIntent, withoutAbsentIntent, type CommandIntent };
|
|
51
17
|
|
|
52
18
|
function stable(value: unknown): unknown {
|
|
53
19
|
if (Array.isArray(value)) return value.map(stable);
|
|
@@ -61,6 +27,8 @@ export function toolOperation(name: string, args: unknown, cwd: string): string
|
|
|
61
27
|
/** Declared identity when an operationId is present, otherwise the exact rule. attemptId never affects identity. */
|
|
62
28
|
export function attemptOperation(name: string, args: unknown, cwd: string, intent: CommandIntent): string {
|
|
63
29
|
if (intent.operationId) return failureIdentity("operation", name, intent.operationId, cwd);
|
|
30
|
+
// An explicit-null intent field is an undeclared one: `{expectedExitCodes: null}` and `{}` are the same command.
|
|
31
|
+
args = withoutAbsentIntent(args);
|
|
64
32
|
if (args && typeof args === "object" && !Array.isArray(args) && "attemptId" in args) {
|
|
65
33
|
const { attemptId: _attemptId, ...rest } = args as Record<string, unknown>;
|
|
66
34
|
return toolOperation(name, rest, cwd);
|
|
@@ -73,10 +41,12 @@ export function evidenceText(value: unknown): string | undefined {
|
|
|
73
41
|
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
74
42
|
return raw?.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 300);
|
|
75
43
|
}
|
|
44
|
+
/** The text a tool result carries (its first text part, stderr, or error), not its JSON wrapper. */
|
|
45
|
+
function resultText(result: any): string | undefined {
|
|
46
|
+
return evidenceText(result?.content?.find?.((x: any) => x?.type === "text")?.text ?? result?.stderr ?? result?.error ?? result);
|
|
47
|
+
}
|
|
76
48
|
function resultError(result: any): string | undefined {
|
|
77
|
-
if (result?.isError === true || (typeof result?.exitCode === "number" && result.exitCode !== 0))
|
|
78
|
-
return evidenceText(result?.content?.find?.((x: any) => x?.type === "text")?.text ?? result?.stderr ?? result?.error ?? result);
|
|
79
|
-
}
|
|
49
|
+
if (result?.isError === true || (typeof result?.exitCode === "number" && result.exitCode !== 0)) return resultText(result);
|
|
80
50
|
return undefined;
|
|
81
51
|
}
|
|
82
52
|
function time(row: any): number | undefined {
|
|
@@ -90,8 +60,13 @@ interface Attempt {
|
|
|
90
60
|
operation: string;
|
|
91
61
|
intent: CommandIntent;
|
|
92
62
|
startSequence: number;
|
|
93
|
-
/**
|
|
94
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Why the confined intent bash is expected to refuse this attempt. A prediction only: the
|
|
65
|
+
* attempt is filed as a rejected intent when its end row carries the child's own refusal.
|
|
66
|
+
*/
|
|
67
|
+
predictedRejection?: string;
|
|
68
|
+
/** Intent names this start registered, undone if the child refused it after all. */
|
|
69
|
+
registered?: { attemptId?: string; operationId?: string };
|
|
95
70
|
}
|
|
96
71
|
interface Finished extends Attempt {
|
|
97
72
|
ok: boolean;
|
|
@@ -236,13 +211,23 @@ export function foldToolStart(model: IncidentModel, row: any, cwd: string, sink:
|
|
|
236
211
|
const rawAttemptId = toolName === "bash" && typeof row.args?.attemptId === "string" ? row.args.attemptId as string : undefined;
|
|
237
212
|
const reused = rawAttemptId !== undefined && model.namedAttempts.has(rawAttemptId);
|
|
238
213
|
if (rawAttemptId !== undefined) model.namedAttempts.add(rawAttemptId);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
214
|
+
const predictedRejection = parsed.error ?? (reused ? `attemptId ${intent.attemptId} was already used` : undefined);
|
|
215
|
+
// A predicted refusal falls back to exact identity: if the child ran the command after all, it is
|
|
216
|
+
// an ordinary failure of exactly that command, never a failure of the operation it named.
|
|
217
|
+
const effective = predictedRejection ? {} : intent;
|
|
218
|
+
const operation = attemptOperation(toolName, row.args, cwd, effective);
|
|
219
|
+
const attempt: Attempt = { toolCallId, toolName, operation, intent: effective, startSequence: seq,
|
|
220
|
+
...(predictedRejection ? { predictedRejection } : {}) };
|
|
221
|
+
model.open.set(toolCallId, attempt);
|
|
222
|
+
if (predictedRejection) return;
|
|
223
|
+
if (intent.attemptId) {
|
|
224
|
+
model.attemptIds.set(intent.attemptId, toolCallId);
|
|
225
|
+
attempt.registered = { attemptId: intent.attemptId };
|
|
226
|
+
}
|
|
227
|
+
if (intent.operationId && !model.operationIds.has(intent.operationId)) {
|
|
228
|
+
model.operationIds.set(intent.operationId, operation);
|
|
229
|
+
attempt.registered = { ...attempt.registered, operationId: intent.operationId };
|
|
230
|
+
}
|
|
246
231
|
if (toolName === DISPOSITION_TOOL) {
|
|
247
232
|
const parsed = readDispositionRequest(row.args);
|
|
248
233
|
model.pendingDispositions.set(toolCallId, parsed.request
|
|
@@ -251,6 +236,28 @@ export function foldToolStart(model: IncidentModel, row: any, cwd: string, sink:
|
|
|
251
236
|
}
|
|
252
237
|
}
|
|
253
238
|
|
|
239
|
+
/** The confined intent bash's own pre-run refusal (child-incidents.ts), matched as the whole result text. */
|
|
240
|
+
const INTENT_REFUSAL = /^Invalid command intent: ([\s\S]+)\. The command was not run\.$/;
|
|
241
|
+
/** Pi's pre-execution schema rejection; it names the tool, and nothing ran. */
|
|
242
|
+
const SCHEMA_REFUSAL = /^Validation failed for tool "/;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Why the child refused this bash call before running it, read from the end row the child
|
|
246
|
+
* produced, never re-derived from the arguments. A command that ran and failed returns undefined.
|
|
247
|
+
*/
|
|
248
|
+
function preRunRefusal(model: IncidentModel, attempt: Attempt | undefined, toolName: string, row: any): string | undefined {
|
|
249
|
+
if (!model.structuredIntent || toolName !== "bash" || row.isError !== true) return undefined;
|
|
250
|
+
const content = row.result?.content;
|
|
251
|
+
const text = Array.isArray(content) && content.length === 1 && typeof content[0]?.text === "string" ? content[0].text.trim() : undefined;
|
|
252
|
+
if (text === undefined) return undefined;
|
|
253
|
+
const refused = INTENT_REFUSAL.exec(text);
|
|
254
|
+
if (refused) return refused[1];
|
|
255
|
+
// Pi validates arguments against the intent schema before execute; that refusal is ours only
|
|
256
|
+
// when the intent fields themselves are what the shared validator rejects.
|
|
257
|
+
if (attempt?.predictedRejection && !attempt.predictedRejection.includes("already used") && SCHEMA_REFUSAL.test(text)) return attempt.predictedRejection;
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
254
261
|
/** Fold one tool execution end: failures, declared expected exits, exact retry recovery, dispositions. */
|
|
255
262
|
export function foldToolEnd(model: IncidentModel, row: any, cwd: string, evidence: string, sink: IncidentSink): void {
|
|
256
263
|
const toolCallId = String(row.toolCallId);
|
|
@@ -263,15 +270,19 @@ export function foldToolEnd(model: IncidentModel, row: any, cwd: string, evidenc
|
|
|
263
270
|
const details = row.result?.details;
|
|
264
271
|
const declaredExit = row.isError === false && details?.expectedExit === true && typeof details.exitCode === "number" &&
|
|
265
272
|
(intent.expectedExitCodes ?? []).includes(details.exitCode) ? details.exitCode as number : undefined;
|
|
266
|
-
const error = row.isError === true ?
|
|
273
|
+
const error = row.isError === true ? resultText(row.result) ?? "Tool returned an error" : resultError(row.result);
|
|
267
274
|
const finished: Finished = { ...(attempt ?? { toolCallId, toolName, operation, intent, startSequence: seq }),
|
|
268
275
|
ok: !error && declaredExit === undefined, endSequence: seq, evidence };
|
|
269
276
|
model.finished.set(toolCallId, finished);
|
|
270
|
-
|
|
277
|
+
const refusal = preRunRefusal(model, attempt, toolName, row);
|
|
278
|
+
if (refusal !== undefined) {
|
|
271
279
|
// Nothing ran: visible, but never grouped with (or escalating) the operation it named.
|
|
280
|
+
if (attempt?.registered?.attemptId && model.attemptIds.get(attempt.registered.attemptId) === toolCallId) model.attemptIds.delete(attempt.registered.attemptId);
|
|
281
|
+
if (attempt?.registered?.operationId) model.operationIds.delete(attempt.registered.operationId);
|
|
282
|
+
finished.intent = {};
|
|
272
283
|
const failureId = `tool:${toolCallId}`;
|
|
273
284
|
sink.observe([{ id: failureId, operation: failureIdentity("rejected-intent", toolCallId), kind: "failure", at: time(row),
|
|
274
|
-
category: REJECTED_INTENT_CATEGORY, evidence, summary: `${toolName} not run: invalid command intent (${
|
|
285
|
+
category: REJECTED_INTENT_CATEGORY, evidence, summary: `${toolName} not run: invalid command intent (${refusal})` }]);
|
|
275
286
|
finished.incident = failureId;
|
|
276
287
|
return;
|
|
277
288
|
}
|