@mrclrchtr/supi-consulting 9.1.2 → 11.0.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 CHANGED
@@ -82,6 +82,27 @@ Results report web use, workspace use, observed and claimed references, warnings
82
82
 
83
83
  Use `/supi-settings` → **Consulting** to disable the tool. Disabling it skips availability checks and removes `consulting_run` from the active tool list.
84
84
 
85
+ ## Failure diagnostics
86
+
87
+ Execution failures report a fixed reason when the adapter can identify it:
88
+
89
+ - Missing final result, missing structured answer, or answer schema mismatch.
90
+ - Unsuccessful final result or data received after the final result.
91
+ - Malformed JSON, a non-object event, or an output limit exceeded.
92
+ - Missing conversation identifier, process failure, timeout, or cancellation.
93
+
94
+ An observed tool permission denial is reported separately. It does not prove the cause of a later failure. A denial does not fail a Consultation that returns a valid answer. Workspace Access does not grant access to files outside the selected workspace.
95
+
96
+ When SuPi Debug is enabled, failed executions emit one `consulting/consultation.failed` event at `error` level. Cancellations emit `consulting/consultation.cancelled` at `info` level. Use the `debug` tool:
97
+
98
+ ```json
99
+ { "source": "consulting", "level": "error", "limit": 10 }
100
+ ```
101
+
102
+ Omit `level` to include cancellations. Add `sessionFile` to inspect events persisted in a saved PI session. A response aborted before the adapter starts has no Consulting event.
103
+
104
+ Events contain the execution phase, timing, selected model, workspace-access and follow-up flags, failure category and reason, and available event counts, terminal-state flags, permission-denial count, and exit code. They exclude prompts, answers, resource paths, conversation IDs, raw stdout, stderr, tool payloads, and error causes, even when raw Debug access is enabled. The event's `cwd` identifies the selected working directory. Failure messages remain specific when Debug is disabled.
105
+
85
106
  ## Security limits
86
107
 
87
108
  The Antigravity adapter enforces its Inspection Permission Set in the Isolated Antigravity Home. It allows `read_url(*)` and denies known file writes, commands, unsandboxed actions, MCP, and URL execution. These rules are **not an operating-system sandbox**.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "9.1.2",
3
+ "version": "11.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-consulting",
3
- "version": "9.1.2",
3
+ "version": "11.0.0",
4
4
  "description": "Bounded consultations with explicit agent and workspace selection and observed evidence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "docs/adr/*.md"
38
38
  ],
39
39
  "dependencies": {
40
- "@mrclrchtr/supi-core": "9.1.2"
40
+ "@mrclrchtr/supi-core": "11.0.0"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-core"
@@ -1,3 +1,4 @@
1
+ import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
1
2
  import type { ConsultationActivity, ConsultationExecutionFacts } from "../../types.ts";
2
3
  import { ConsultingAgentError, type ConsultingAgentFailureKind } from "../errors.ts";
3
4
  import type {
@@ -13,6 +14,7 @@ import {
13
14
  type IsolatedAntigravityPaths,
14
15
  prepareIsolatedAntigravityHome,
15
16
  } from "./isolated-home.ts";
17
+ import { isCuratedModel } from "./models.ts";
16
18
  import { probeProjectHooks } from "./process/hooks.ts";
17
19
  import { AntigravityProcessError, runAntigravityConversation } from "./process/runner.ts";
18
20
  import { ANTIGRAVITY_ANSWER_SCHEMA } from "./structured-output.ts";
@@ -22,6 +24,8 @@ const MAX_WARNING_COUNT = 24;
22
24
  const MAX_WARNING_CHARS = 500;
23
25
  const CONTINUATION_PREFIX = "agy1.";
24
26
 
27
+ type ExecutionPhase = "preparation" | "hook-probe" | "conversation" | "validation";
28
+
25
29
  /** The one agent supported by Consulting in this package. */
26
30
  export const ANTIGRAVITY_AGENT_ID = "antigravity" as const;
27
31
 
@@ -65,21 +69,53 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
65
69
  }
66
70
 
67
71
  async execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts> {
72
+ const timer = startDebugTimer();
73
+ let phase: ExecutionPhase = "preparation";
68
74
  try {
69
- return await this.#execute(options);
75
+ return await this.#execute(options, (nextPhase) => {
76
+ timer.mark(phase);
77
+ phase = nextPhase;
78
+ });
70
79
  } catch (error) {
71
- throw safeExecutionFailure(error);
80
+ const failure = safeExecutionFailure(error);
81
+ const canceled = failure.kind === "cancelled";
82
+ timer.finish(
83
+ () => ({
84
+ source: "consulting",
85
+ level: canceled ? "info" : "error",
86
+ category: canceled ? "consultation.cancelled" : "consultation.failed",
87
+ message: failure.message,
88
+ cwd: options.canonicalWorkingDirectory,
89
+ data: {
90
+ agent: this.identity,
91
+ model: isCuratedModel(options.model) ? options.model : "unknown",
92
+ workspaceAccess: options.workspaceAccess,
93
+ followUp: options.continuation !== undefined,
94
+ phase,
95
+ kind: failure.kind,
96
+ reason: failure.reason ?? failure.kind,
97
+ ...failure.diagnostics,
98
+ },
99
+ }),
100
+ phase,
101
+ );
102
+ throw failure;
72
103
  }
73
104
  }
74
105
 
75
- async #execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts> {
106
+ async #execute(
107
+ options: ConsultingAgentExecutionOptions,
108
+ onPhase: (phase: ExecutionPhase) => void,
109
+ ): Promise<ConsultationExecutionFacts> {
76
110
  const expectedConversationId = options.continuation
77
111
  ? decodeContinuation(options.continuation)
78
112
  : undefined;
79
113
  await prepareIsolatedAntigravityHome(this.#paths);
114
+ if (options.workspaceAccess) onPhase("hook-probe");
80
115
  const hookWarning = options.workspaceAccess
81
116
  ? await inspectProjectHooks(this.#paths, options, options.onWarning)
82
117
  : undefined;
118
+ onPhase("conversation");
83
119
  const rawFacts = await runAntigravityConversation({
84
120
  paths: this.#paths,
85
121
  cwd: options.canonicalWorkingDirectory,
@@ -92,6 +128,7 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
92
128
  onActivity: options.onProgress,
93
129
  onProcessStart: options.onProcessStart,
94
130
  });
131
+ onPhase("validation");
95
132
  if (expectedConversationId && rawFacts.conversationId !== expectedConversationId) {
96
133
  throw new ConsultingAgentError("conversation-mismatch");
97
134
  }
@@ -179,9 +216,14 @@ function decodeContinuation(value: string): string {
179
216
  return conversationId;
180
217
  }
181
218
 
182
- /** Drop raw diagnostics on every failure path, including preparation and callbacks. */
219
+ /** Keep only fixed reasons and safe facts, including preparation and callback failures. */
183
220
  function safeExecutionFailure(error: unknown): ConsultingAgentError {
184
- if (error instanceof ConsultingAgentError) return new ConsultingAgentError(error.kind);
221
+ if (error instanceof ConsultingAgentError) {
222
+ return new ConsultingAgentError(error.kind, {
223
+ reason: error.reason,
224
+ diagnostics: error.diagnostics,
225
+ });
226
+ }
185
227
  const processKinds: Record<AntigravityProcessError["kind"], ConsultingAgentFailureKind> = {
186
228
  missing: "unavailable",
187
229
  timeout: "timeout",
@@ -190,7 +232,9 @@ function safeExecutionFailure(error: unknown): ConsultingAgentError {
190
232
  protocol: "invalid-response",
191
233
  process: "execution",
192
234
  };
193
- return new ConsultingAgentError(
194
- error instanceof AntigravityProcessError ? processKinds[error.kind] : "execution",
195
- );
235
+ if (!(error instanceof AntigravityProcessError)) return new ConsultingAgentError("execution");
236
+ return new ConsultingAgentError(processKinds[error.kind], {
237
+ reason: error.reason,
238
+ diagnostics: { ...error.diagnostics, exitCode: error.exitCode },
239
+ });
196
240
  }
@@ -23,6 +23,7 @@ import {
23
23
  toolName,
24
24
  } from "./event-values.ts";
25
25
  import { normalizeProtocolEvent } from "./protocol.ts";
26
+ import { AntigravityResponseError } from "./response-error.ts";
26
27
  import { mergeUsage, readUsage } from "./usage.ts";
27
28
 
28
29
  interface PendingTool {
@@ -67,8 +68,7 @@ export class AntigravityEventAccumulator {
67
68
 
68
69
  /** Consume one already-parsed event. */
69
70
  consume(event: Record<string, unknown>): void {
70
- if (this.#terminalSeen)
71
- throw new Error("Antigravity returned events after its terminal event.");
71
+ if (this.#terminalSeen) throw new AntigravityResponseError("events-after-terminal");
72
72
  const normalizedEvent = normalizeProtocolEvent(event);
73
73
  this.#conversationId =
74
74
  conversationId(event) ?? conversationId(normalizedEvent) ?? this.#conversationId;
@@ -103,14 +103,25 @@ export class AntigravityEventAccumulator {
103
103
  }
104
104
  }
105
105
 
106
+ /** Return safe execution facts even when the response is incomplete. */
107
+ get diagnostics(): {
108
+ permissionDenials: number;
109
+ terminalSeen: boolean;
110
+ terminalSucceeded: boolean;
111
+ } {
112
+ return {
113
+ permissionDenials: this.#permissionDenials,
114
+ terminalSeen: this.#terminalSeen,
115
+ terminalSucceeded: this.#terminalSucceeded,
116
+ };
117
+ }
118
+
106
119
  /** Finish the reduction and validate terminal structured output. */
107
120
  finish(): AntigravityExecutionFacts {
108
- if (!this.#terminalSeen || !this.#terminalSucceeded) {
109
- throw new Error("Antigravity did not return a successful terminal event.");
110
- }
111
- if (!this.#candidate)
112
- throw new Error("Antigravity terminal output was missing structured data.");
113
- if (!this.#conversationId) throw new Error("Antigravity did not return a conversation handle.");
121
+ if (!this.#terminalSeen) throw new AntigravityResponseError("missing-terminal");
122
+ if (!this.#terminalSucceeded) throw new AntigravityResponseError("terminal-failed");
123
+ if (!this.#candidate) throw new AntigravityResponseError("missing-answer");
124
+ if (!this.#conversationId) throw new AntigravityResponseError("missing-conversation");
114
125
  return {
115
126
  answer: validateAntigravityAnswer(this.#candidate),
116
127
  conversationId: this.#conversationId,
@@ -254,7 +265,7 @@ export class AntigravityEventAccumulator {
254
265
  this.#terminalSeen = true;
255
266
  const status = eventStatus(event);
256
267
  if (isErrorStatus(status) || !hasExplicitSuccess(event, status)) {
257
- throw new Error("Antigravity returned a non-success terminal status.");
268
+ throw new AntigravityResponseError("terminal-failed");
258
269
  }
259
270
  this.#terminalSucceeded = true;
260
271
  this.#candidate = answerCandidate(event);
@@ -1,3 +1,5 @@
1
+ import { AntigravityResponseError } from "./response-error.ts";
2
+
1
3
  /** Limits for streamed Antigravity output. */
2
4
  export const MAX_STDOUT_LINE_BYTES = 128 * 1024;
3
5
  export const MAX_STDOUT_BYTES = 5 * 1024 * 1024;
@@ -5,9 +7,9 @@ export const MAX_STREAM_EVENTS = 512;
5
7
  export const MAX_RETAINED_STDERR_BYTES = 8 * 1024;
6
8
 
7
9
  /** Error raised when a process stream exceeds a package-owned bound. */
8
- export class StreamLimitError extends Error {
9
- constructor(message: string) {
10
- super(message);
10
+ export class StreamLimitError extends AntigravityResponseError {
11
+ constructor(reason: "stdout-line-limit" | "stdout-byte-limit" | "event-limit") {
12
+ super(reason);
11
13
  this.name = "StreamLimitError";
12
14
  }
13
15
  }
@@ -40,7 +42,7 @@ export class BoundedLineParser {
40
42
  feed(chunk: Uint8Array, onLine: (line: string) => void): void {
41
43
  this.#totalBytes += chunk.byteLength;
42
44
  if (this.#totalBytes > this.#maxTotalBytes) {
43
- throw new StreamLimitError("Antigravity stdout exceeded its byte limit.");
45
+ throw new StreamLimitError("stdout-byte-limit");
44
46
  }
45
47
 
46
48
  let start = 0;
@@ -62,7 +64,7 @@ export class BoundedLineParser {
62
64
  if (part.byteLength === 0) return;
63
65
  this.#lineBytes += part.byteLength;
64
66
  if (this.#lineBytes > this.#maxLineBytes) {
65
- throw new StreamLimitError("An Antigravity stdout line exceeded its byte limit.");
67
+ throw new StreamLimitError("stdout-line-limit");
66
68
  }
67
69
  this.#lineParts.push(Buffer.from(part));
68
70
  }
@@ -111,10 +113,10 @@ export function parseNdjsonLine(line: string): Record<string, unknown> | undefin
111
113
  try {
112
114
  parsed = JSON.parse(line);
113
115
  } catch (error) {
114
- throw new Error("Antigravity returned malformed NDJSON.", { cause: error });
116
+ throw new AntigravityResponseError("malformed-json", { cause: error });
115
117
  }
116
118
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
117
- throw new Error("Antigravity returned a non-object NDJSON event.");
119
+ throw new AntigravityResponseError("invalid-event");
118
120
  }
119
121
  return parsed as Record<string, unknown>;
120
122
  }
@@ -9,9 +9,10 @@ export function normalizeProtocolEvent(event: Record<string, unknown>): Record<s
9
9
  }
10
10
 
11
11
  function normalizeResultEnvelope(event: Record<string, unknown>): Record<string, unknown> {
12
- return isRecord(event.result)
13
- ? { ...event, ...event.result, type: "result" }
14
- : { ...event, type: "result" };
12
+ if (!isRecord(event.result)) return { ...event, type: "result" };
13
+ // Remove the envelope so its status fields cannot be mistaken for answer data.
14
+ const { result, ...envelope } = event;
15
+ return { ...envelope, ...result, type: "result" };
15
16
  }
16
17
 
17
18
  function normalizeStepUpdateEnvelope(event: Record<string, unknown>): Record<string, unknown> {
@@ -0,0 +1,12 @@
1
+ import type { ConsultingFailureReason } from "../../errors.ts";
2
+
3
+ /** Internal response failure with a fixed reason that survives process error wrapping. */
4
+ export class AntigravityResponseError extends Error {
5
+ readonly reason: ConsultingFailureReason;
6
+
7
+ constructor(reason: ConsultingFailureReason, options?: ErrorOptions) {
8
+ super(`Antigravity response failure: ${reason}.`, options);
9
+ this.name = "AntigravityResponseError";
10
+ this.reason = reason;
11
+ }
12
+ }
@@ -6,6 +6,7 @@ import type { AntigravityExecutionFacts, AntigravityProgressCallback } from "../
6
6
  import { eventType, toolName } from "./event-values.ts";
7
7
  import { AntigravityEventAccumulator } from "./events.ts";
8
8
  import { MAX_STREAM_EVENTS, parseNdjsonLine, StreamLimitError } from "./ndjson.ts";
9
+ import { AntigravityResponseError } from "./response-error.ts";
9
10
  import type { AntigravityProbeResult } from "./subprocess.ts";
10
11
  import { AntigravityProcessError, runBoundedChildProcess } from "./subprocess.ts";
11
12
 
@@ -72,6 +73,8 @@ export async function runAntigravityConversation(options: {
72
73
  join(options.schemaDirectoryParent ?? tmpdir(), "supi-consulting-schema-"),
73
74
  );
74
75
  const schemaPath = join(schemaDirectory, "answer.json");
76
+ const accumulator = new AntigravityEventAccumulator({ workspaceDirectory: options.cwd });
77
+ let eventCount = 0;
75
78
  try {
76
79
  await writeFile(schemaPath, `${JSON.stringify(options.schema)}\n`, { mode: 0o600 });
77
80
  const args = buildAntigravityRunArguments(
@@ -80,8 +83,6 @@ export async function runAntigravityConversation(options: {
80
83
  options.conversationId,
81
84
  options.workspaceDirectory,
82
85
  );
83
- const accumulator = new AntigravityEventAccumulator({ workspaceDirectory: options.cwd });
84
- let eventCount = 0;
85
86
  await runBoundedChildProcess({
86
87
  args,
87
88
  cwd: options.cwd,
@@ -94,7 +95,7 @@ export async function runAntigravityConversation(options: {
94
95
  if (!event) return;
95
96
  eventCount += 1;
96
97
  if (eventCount > MAX_STREAM_EVENTS) {
97
- throw new StreamLimitError("Antigravity returned too many stream events.");
98
+ throw new StreamLimitError("event-limit");
98
99
  }
99
100
  options.onActivity?.(safeActivityLabel(event));
100
101
  accumulator.consume(event);
@@ -103,11 +104,19 @@ export async function runAntigravityConversation(options: {
103
104
  });
104
105
  return accumulator.finish();
105
106
  } catch (error) {
106
- if (error instanceof AntigravityProcessError) throw error;
107
+ const processError = error instanceof AntigravityProcessError ? error : undefined;
107
108
  // biome-ignore lint/style/useErrorCause: the custom process error preserves the protocol cause.
108
- throw new AntigravityProcessError("Antigravity returned an invalid stream.", "protocol", {
109
- cause: error,
110
- });
109
+ throw new AntigravityProcessError(
110
+ processError?.message ?? "Antigravity returned an invalid stream.",
111
+ processError?.kind ?? "protocol",
112
+ {
113
+ cause: error,
114
+ reason: error instanceof AntigravityResponseError ? error.reason : processError?.reason,
115
+ exitCode: processError?.exitCode,
116
+ stderr: processError?.stderr,
117
+ diagnostics: { ...accumulator.diagnostics, eventCount },
118
+ },
119
+ );
111
120
  } finally {
112
121
  await rm(schemaDirectory, { recursive: true, force: true });
113
122
  }
@@ -1,4 +1,5 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
+ import type { ConsultingFailureDiagnostics, ConsultingFailureReason } from "../../errors.ts";
2
3
  import { buildAntigravityEnvironment } from "./environment.ts";
3
4
  import {
4
5
  BoundedLineParser,
@@ -6,6 +7,7 @@ import {
6
7
  MAX_STDOUT_BYTES,
7
8
  MAX_STDOUT_LINE_BYTES,
8
9
  } from "./ndjson.ts";
10
+ import { AntigravityResponseError } from "./response-error.ts";
9
11
 
10
12
  const COMMAND = "agy";
11
13
  const TERMINATION_GRACE_MS = 500;
@@ -24,17 +26,27 @@ export class AntigravityProcessError extends Error {
24
26
  readonly exitCode: number | null | undefined;
25
27
  /** Bounded stderr retained for an in-memory caller, never put in result details. */
26
28
  readonly stderr: string | undefined;
29
+ readonly reason: ConsultingFailureReason | undefined;
30
+ readonly diagnostics: ConsultingFailureDiagnostics | undefined;
27
31
 
28
32
  constructor(
29
33
  message: string,
30
34
  kind: AntigravityProcessError["kind"],
31
- options: { cause?: unknown; exitCode?: number | null; stderr?: string } = {},
35
+ options: {
36
+ cause?: unknown;
37
+ exitCode?: number | null;
38
+ stderr?: string;
39
+ reason?: ConsultingFailureReason;
40
+ diagnostics?: ConsultingFailureDiagnostics;
41
+ } = {},
32
42
  ) {
33
43
  super(message, options.cause === undefined ? undefined : { cause: options.cause });
34
44
  this.name = "AntigravityProcessError";
35
45
  this.kind = kind;
36
46
  this.exitCode = options.exitCode;
37
47
  this.stderr = options.stderr;
48
+ this.reason = options.reason;
49
+ this.diagnostics = options.diagnostics;
38
50
  }
39
51
  }
40
52
 
@@ -125,7 +137,10 @@ export function runBoundedChildProcess(
125
137
  } catch (error) {
126
138
  terminateAndReject(
127
139
  error instanceof Error
128
- ? new AntigravityProcessError(error.message, "stream", { cause: error })
140
+ ? new AntigravityProcessError(error.message, "stream", {
141
+ cause: error,
142
+ ...(error instanceof AntigravityResponseError ? { reason: error.reason } : {}),
143
+ })
129
144
  : new AntigravityProcessError("Antigravity stdout exceeded its limits.", "stream"),
130
145
  );
131
146
  }
@@ -221,6 +236,7 @@ function toProtocolOrProcessError(error: unknown): AntigravityProcessError {
221
236
  if (error instanceof AntigravityProcessError) return error;
222
237
  return new AntigravityProcessError("Antigravity returned an invalid stream.", "protocol", {
223
238
  cause: error,
239
+ ...(error instanceof AntigravityResponseError ? { reason: error.reason } : {}),
224
240
  });
225
241
  }
226
242
 
@@ -1,6 +1,7 @@
1
1
  import { type TSchema, Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
3
  import type { ConsultationAnswer } from "../../types.ts";
4
+ import { AntigravityResponseError } from "./process/response-error.ts";
4
5
 
5
6
  /** Maximum length of Antigravity's final answer text. */
6
7
  export const MAX_ANSWER_CHARS = 16_000;
@@ -52,7 +53,7 @@ export const ANTIGRAVITY_ANSWER_SCHEMA: TSchema = Type.Object(
52
53
  /** Validate and return the exact structured answer shape. */
53
54
  export function validateAntigravityAnswer(value: unknown): ConsultationAnswer {
54
55
  if (!Value.Check(ANTIGRAVITY_ANSWER_SCHEMA, value)) {
55
- throw new Error("Antigravity returned invalid structured output.");
56
+ throw new AntigravityResponseError("invalid-answer");
56
57
  }
57
58
  return value as ConsultationAnswer;
58
59
  }
@@ -9,19 +9,68 @@ const FAILURE_MESSAGES = {
9
9
  execution: "Consultation execution failed.",
10
10
  } as const;
11
11
 
12
+ const FAILURE_REASON_MESSAGES = {
13
+ "missing-terminal": "The Consulting Agent did not return a final result.",
14
+ "terminal-failed": "The Consulting Agent reported an unsuccessful final result.",
15
+ "missing-answer": "The Consulting Agent did not return a final structured answer.",
16
+ "invalid-answer":
17
+ "The Consulting Agent returned an answer that does not match the required schema.",
18
+ "missing-conversation": "The Consulting Agent did not return a conversation identifier.",
19
+ "events-after-terminal": "The Consulting Agent sent data after its final result.",
20
+ "malformed-json": "The Consulting Agent returned malformed JSON.",
21
+ "invalid-event": "The Consulting Agent returned a non-object response event.",
22
+ "stdout-line-limit": "The Consulting Agent response exceeded the line-size limit.",
23
+ "stdout-byte-limit": "The Consulting Agent response exceeded the total output-size limit.",
24
+ "event-limit": "The Consulting Agent response exceeded the event limit.",
25
+ } as const;
26
+
27
+ /** Fixed failure reasons that do not contain agent output. */
28
+ export type ConsultingFailureReason = keyof typeof FAILURE_REASON_MESSAGES;
29
+
30
+ /** Execution facts that can leave the adapter on failure. No resource names or raw output. */
31
+ export interface ConsultingFailureDiagnostics {
32
+ permissionDenials?: number;
33
+ eventCount?: number;
34
+ terminalSeen?: boolean;
35
+ terminalSucceeded?: boolean;
36
+ exitCode?: number | null;
37
+ }
38
+
12
39
  /** Safe failure categories that can leave a private Consulting Agent adapter. */
13
40
  export type ConsultingAgentFailureKind = keyof typeof FAILURE_MESSAGES;
14
41
 
15
42
  /**
16
- * A bounded failure with no agent diagnostics or error cause.
17
- * Construct messages from fixed categories, never from process output.
43
+ * A bounded failure with fixed messages and safe execution facts, but no raw cause.
44
+ * Permission denials are observations, not a claim about the cause of failure.
18
45
  */
19
46
  export class ConsultingAgentError extends Error {
20
47
  readonly kind: ConsultingAgentFailureKind;
48
+ readonly reason: ConsultingFailureReason | undefined;
49
+ readonly diagnostics: ConsultingFailureDiagnostics | undefined;
21
50
 
22
- constructor(kind: ConsultingAgentFailureKind) {
23
- super(FAILURE_MESSAGES[kind]);
51
+ constructor(
52
+ kind: ConsultingAgentFailureKind,
53
+ options: { reason?: ConsultingFailureReason; diagnostics?: ConsultingFailureDiagnostics } = {},
54
+ ) {
55
+ const message = options.reason
56
+ ? FAILURE_REASON_MESSAGES[options.reason]
57
+ : FAILURE_MESSAGES[kind];
58
+ const denial = (options.diagnostics?.permissionDenials ?? 0) > 0;
59
+ super(
60
+ `${message}${denial ? " A tool permission denial was observed. Check the requested resources and the Consulting Agent permissions." : ""}`,
61
+ );
24
62
  this.name = "ConsultingAgentError";
25
63
  this.kind = kind;
64
+ this.reason = options.reason;
65
+ const facts = options.diagnostics;
66
+ this.diagnostics = facts
67
+ ? {
68
+ permissionDenials: facts.permissionDenials,
69
+ eventCount: facts.eventCount,
70
+ terminalSeen: facts.terminalSeen,
71
+ terminalSucceeded: facts.terminalSucceeded,
72
+ exitCode: facts.exitCode,
73
+ }
74
+ : undefined;
26
75
  }
27
76
  }
@@ -38,6 +38,6 @@ export interface ConsultingAgentAdapter {
38
38
  /** The agent's stable, package-managed empty Consultation Workspace. */
39
39
  readonly consultationWorkspace: string;
40
40
  discover(options?: { signal?: AbortSignal }): Promise<ConsultingAvailability>;
41
- /** Reject with a ConsultingAgentError; raw diagnostics and causes stay inside the adapter. */
41
+ /** Reject with a ConsultingAgentError and safe facts; raw diagnostics and causes stay inside the adapter. */
42
42
  execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts>;
43
43
  }