@mrclrchtr/supi-consulting 0.0.0-stage → 8.0.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.
Files changed (78) hide show
  1. package/CLAUDE.md +23 -0
  2. package/CONTEXT.md +53 -0
  3. package/README.md +104 -2
  4. package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
  5. package/node_modules/@mrclrchtr/supi-core/README.md +137 -0
  6. package/node_modules/@mrclrchtr/supi-core/package.json +77 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/api.ts +42 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +342 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/config.ts +17 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +313 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/index.ts +38 -0
  21. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
  22. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  23. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  26. package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
  28. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +242 -0
  31. package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
  32. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
  33. package/node_modules/@mrclrchtr/supi-core/src/session.ts +47 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
  35. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +454 -0
  36. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
  37. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  38. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  39. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  40. package/node_modules/@mrclrchtr/supi-core/src/tui.ts +141 -0
  41. package/package.json +78 -4
  42. package/scripts/live-probe.ts +139 -0
  43. package/src/agents/antigravity/activity.ts +22 -0
  44. package/src/agents/antigravity/adapter.ts +196 -0
  45. package/src/agents/antigravity/availability.ts +231 -0
  46. package/src/agents/antigravity/isolated-home.ts +346 -0
  47. package/src/agents/antigravity/models.ts +15 -0
  48. package/src/agents/antigravity/process/environment.ts +33 -0
  49. package/src/agents/antigravity/process/event-values.ts +273 -0
  50. package/src/agents/antigravity/process/events.ts +366 -0
  51. package/src/agents/antigravity/process/hooks.ts +219 -0
  52. package/src/agents/antigravity/process/ndjson.ts +120 -0
  53. package/src/agents/antigravity/process/protocol.ts +69 -0
  54. package/src/agents/antigravity/process/runner.ts +147 -0
  55. package/src/agents/antigravity/process/subprocess.ts +278 -0
  56. package/src/agents/antigravity/process/usage.ts +50 -0
  57. package/src/agents/antigravity/structured-output.ts +58 -0
  58. package/src/agents/antigravity/types.ts +17 -0
  59. package/src/agents/errors.ts +27 -0
  60. package/src/agents/types.ts +43 -0
  61. package/src/catalogue.ts +17 -0
  62. package/src/config.ts +26 -0
  63. package/src/conversation/handles.ts +187 -0
  64. package/src/evidence-hash.ts +6 -0
  65. package/src/extension.ts +26 -0
  66. package/src/footer-constants.ts +5 -0
  67. package/src/footer.ts +25 -0
  68. package/src/runtime.ts +244 -0
  69. package/src/settings.ts +38 -0
  70. package/src/tool/consulting_run/evidence.ts +169 -0
  71. package/src/tool/consulting_run/execute.ts +201 -0
  72. package/src/tool/consulting_run/guidance.ts +3 -0
  73. package/src/tool/consulting_run/input.ts +117 -0
  74. package/src/tool/consulting_run/register.ts +36 -0
  75. package/src/tool/consulting_run/render.ts +261 -0
  76. package/src/tool/consulting_run/result.ts +185 -0
  77. package/src/tool/consulting_run/spec.ts +20 -0
  78. package/src/types.ts +104 -0
@@ -0,0 +1,278 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { buildAntigravityEnvironment } from "./environment.ts";
3
+ import {
4
+ BoundedLineParser,
5
+ BoundedStderrCapture,
6
+ MAX_STDOUT_BYTES,
7
+ MAX_STDOUT_LINE_BYTES,
8
+ } from "./ndjson.ts";
9
+
10
+ const COMMAND = "agy";
11
+ const TERMINATION_GRACE_MS = 500;
12
+
13
+ /** Safe outcomes from a bounded non-stream Antigravity probe. */
14
+ export interface AntigravityProbeResult {
15
+ exitCode: number | null;
16
+ signal: NodeJS.Signals | null;
17
+ stdout: string;
18
+ stderr: string;
19
+ }
20
+
21
+ /** Error raised for a failed Antigravity process or protocol. */
22
+ export class AntigravityProcessError extends Error {
23
+ readonly kind: "missing" | "timeout" | "cancelled" | "stream" | "process" | "protocol";
24
+ readonly exitCode: number | null | undefined;
25
+ /** Bounded stderr retained for an in-memory caller, never put in result details. */
26
+ readonly stderr: string | undefined;
27
+
28
+ constructor(
29
+ message: string,
30
+ kind: AntigravityProcessError["kind"],
31
+ options: { cause?: unknown; exitCode?: number | null; stderr?: string } = {},
32
+ ) {
33
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
34
+ this.name = "AntigravityProcessError";
35
+ this.kind = kind;
36
+ this.exitCode = options.exitCode;
37
+ this.stderr = options.stderr;
38
+ }
39
+ }
40
+
41
+ /** Options for one directly spawned, bounded agy process. */
42
+ export interface BoundedChildProcessOptions {
43
+ args: string[];
44
+ cwd: string;
45
+ homeDir: string;
46
+ prompt?: string;
47
+ signal?: AbortSignal;
48
+ timeoutMs: number;
49
+ onLine: (line: string) => void;
50
+ maxStdoutBytes?: number;
51
+ allowNonZero?: boolean;
52
+ onProcessStart?: () => void;
53
+ }
54
+
55
+ /** Spawn agy without a shell and apply stream, timeout, and process-group limits. */
56
+ export function runBoundedChildProcess(
57
+ options: BoundedChildProcessOptions,
58
+ ): Promise<AntigravityProbeResult> {
59
+ if (process.platform !== "darwin" && process.platform !== "linux") {
60
+ return Promise.reject(
61
+ new AntigravityProcessError("Antigravity is supported on macOS and Linux only.", "process"),
62
+ );
63
+ }
64
+ if (options.signal?.aborted) {
65
+ return Promise.reject(
66
+ new AntigravityProcessError("Antigravity run was canceled.", "cancelled"),
67
+ );
68
+ }
69
+
70
+ return new Promise((resolve, reject) => {
71
+ const parser = new BoundedLineParser({
72
+ maxLineBytes: MAX_STDOUT_LINE_BYTES,
73
+ maxTotalBytes: options.maxStdoutBytes ?? MAX_STDOUT_BYTES,
74
+ });
75
+ const stderr = new BoundedStderrCapture();
76
+ let child: ChildProcess | undefined;
77
+ let closed = false;
78
+ let settled = false;
79
+ let failure: Error | undefined;
80
+ let closeResolve: (() => void) | undefined;
81
+ const closePromise = new Promise<void>((resolveClose) => {
82
+ closeResolve = resolveClose;
83
+ });
84
+
85
+ const finishReject = (error: Error): void => {
86
+ if (settled) return;
87
+ settled = true;
88
+ reject(error);
89
+ };
90
+
91
+ const terminateAndReject = (error: Error): void => {
92
+ if (settled || failure) return;
93
+ failure = error;
94
+ void terminateProcess(child, closePromise, () => closed).then(() => {
95
+ finishReject(error);
96
+ });
97
+ };
98
+
99
+ const timer = setTimeout(() => {
100
+ terminateAndReject(new AntigravityProcessError("Antigravity process timed out.", "timeout"));
101
+ }, options.timeoutMs);
102
+ const onAbort = (): void => {
103
+ terminateAndReject(new AntigravityProcessError("Antigravity run was canceled.", "cancelled"));
104
+ };
105
+ options.signal?.addEventListener("abort", onAbort, { once: true });
106
+
107
+ try {
108
+ child = spawn(COMMAND, options.args, {
109
+ cwd: options.cwd,
110
+ env: buildAntigravityEnvironment(options.homeDir),
111
+ detached: true,
112
+ shell: false,
113
+ stdio: ["pipe", "pipe", "pipe"],
114
+ });
115
+ } catch (error) {
116
+ clearTimeout(timer);
117
+ options.signal?.removeEventListener("abort", onAbort);
118
+ terminateAndReject(classifySpawnError(error));
119
+ return;
120
+ }
121
+
122
+ child.stdout?.on("data", (chunk: Buffer | string) => {
123
+ try {
124
+ parser.feed(toBuffer(chunk), options.onLine);
125
+ } catch (error) {
126
+ terminateAndReject(
127
+ error instanceof Error
128
+ ? new AntigravityProcessError(error.message, "stream", { cause: error })
129
+ : new AntigravityProcessError("Antigravity stdout exceeded its limits.", "stream"),
130
+ );
131
+ }
132
+ });
133
+ child.stderr?.on("data", (chunk: Buffer | string) => stderr.feed(toBuffer(chunk)));
134
+ child.stdin?.on("error", (error) => terminateAndReject(classifySpawnError(error)));
135
+ child.on("error", (error) => terminateAndReject(classifySpawnError(error)));
136
+ child.on("close", (exitCode, signal) => {
137
+ handleChildClose(exitCode, signal, {
138
+ parser,
139
+ options,
140
+ stderr,
141
+ setClosed: () => {
142
+ closed = true;
143
+ closeResolve?.();
144
+ },
145
+ clearTimer: () => clearTimeout(timer),
146
+ removeAbortListener: () => options.signal?.removeEventListener("abort", onAbort),
147
+ getFailure: () => failure,
148
+ isSettled: () => settled,
149
+ resolve: (result) => {
150
+ settled = true;
151
+ resolve(result);
152
+ },
153
+ fail: terminateAndReject,
154
+ });
155
+ });
156
+
157
+ try {
158
+ options.onProcessStart?.();
159
+ if (options.prompt !== undefined) {
160
+ const userEvent = JSON.stringify({
161
+ event: "user",
162
+ message: { role: "user", content: options.prompt },
163
+ });
164
+ child.stdin?.end(`${userEvent}\n`);
165
+ } else {
166
+ child.stdin?.end();
167
+ }
168
+ } catch (error) {
169
+ terminateAndReject(classifySpawnError(error));
170
+ }
171
+ });
172
+ }
173
+
174
+ interface ChildCloseState {
175
+ parser: BoundedLineParser;
176
+ options: BoundedChildProcessOptions;
177
+ stderr: BoundedStderrCapture;
178
+ setClosed: () => void;
179
+ clearTimer: () => void;
180
+ removeAbortListener: () => void;
181
+ getFailure: () => Error | undefined;
182
+ isSettled: () => boolean;
183
+ resolve: (result: AntigravityProbeResult) => void;
184
+ fail: (error: Error) => void;
185
+ }
186
+
187
+ function handleChildClose(
188
+ exitCode: number | null,
189
+ signal: NodeJS.Signals | null,
190
+ state: ChildCloseState,
191
+ ): void {
192
+ state.setClosed();
193
+ state.clearTimer();
194
+ state.removeAbortListener();
195
+ if (state.getFailure()) return;
196
+ try {
197
+ state.parser.finish(state.options.onLine);
198
+ ensureSuccessfulExit(exitCode, signal, state.options.allowNonZero, state.stderr.text());
199
+ if (!state.isSettled()) {
200
+ state.resolve({ exitCode, signal, stdout: "", stderr: state.stderr.text() });
201
+ }
202
+ } catch (error) {
203
+ state.fail(toProtocolOrProcessError(error));
204
+ }
205
+ }
206
+
207
+ function ensureSuccessfulExit(
208
+ exitCode: number | null,
209
+ signal: NodeJS.Signals | null,
210
+ allowNonZero: boolean | undefined,
211
+ stderr: string,
212
+ ): void {
213
+ if (allowNonZero || (exitCode === 0 && !signal)) return;
214
+ throw new AntigravityProcessError(processFailureMessage(exitCode, signal), "process", {
215
+ exitCode,
216
+ stderr,
217
+ });
218
+ }
219
+
220
+ function toProtocolOrProcessError(error: unknown): AntigravityProcessError {
221
+ if (error instanceof AntigravityProcessError) return error;
222
+ return new AntigravityProcessError("Antigravity returned an invalid stream.", "protocol", {
223
+ cause: error,
224
+ });
225
+ }
226
+
227
+ async function terminateProcess(
228
+ child: ChildProcess | undefined,
229
+ closePromise: Promise<void>,
230
+ isClosed: () => boolean,
231
+ ): Promise<void> {
232
+ if (!child || isClosed()) return;
233
+ sendProcessGroupSignal(child, "SIGTERM");
234
+ await waitForClose(closePromise, TERMINATION_GRACE_MS);
235
+ if (!isClosed()) {
236
+ sendProcessGroupSignal(child, "SIGKILL");
237
+ await waitForClose(closePromise, TERMINATION_GRACE_MS);
238
+ }
239
+ }
240
+
241
+ function sendProcessGroupSignal(child: ChildProcess, signal: NodeJS.Signals): void {
242
+ if (typeof child.pid !== "number") return;
243
+ try {
244
+ process.kill(-child.pid, signal);
245
+ return;
246
+ } catch {
247
+ try {
248
+ child.kill(signal);
249
+ } catch {
250
+ // The process may have exited between the two kill attempts.
251
+ }
252
+ }
253
+ }
254
+
255
+ async function waitForClose(closePromise: Promise<void>, timeoutMs: number): Promise<void> {
256
+ await Promise.race([
257
+ closePromise,
258
+ new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
259
+ ]);
260
+ }
261
+
262
+ function toBuffer(chunk: Buffer | string): Buffer {
263
+ return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
264
+ }
265
+
266
+ function classifySpawnError(error: unknown): AntigravityProcessError {
267
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
268
+ return new AntigravityProcessError(
269
+ code === "ENOENT" ? "Antigravity CLI was not found." : "Could not start Antigravity CLI.",
270
+ code === "ENOENT" ? "missing" : "process",
271
+ { cause: error },
272
+ );
273
+ }
274
+
275
+ function processFailureMessage(exitCode: number | null, signal: NodeJS.Signals | null): string {
276
+ if (signal) return `Antigravity process stopped with ${signal}.`;
277
+ return `Antigravity process exited with code ${String(exitCode)}.`;
278
+ }
@@ -0,0 +1,50 @@
1
+ import type { ConsultationUsage } from "../../../types.ts";
2
+ import { isRecord } from "./event-values.ts";
3
+
4
+ /** Read bounded token usage from one normalized Antigravity event. */
5
+ export function readUsage(event: Record<string, unknown>): ConsultationUsage | undefined {
6
+ const value = isRecord(event.usage)
7
+ ? event.usage
8
+ : isRecord(event.token_usage)
9
+ ? event.token_usage
10
+ : isRecord(event.tokenUsage)
11
+ ? event.tokenUsage
12
+ : undefined;
13
+ if (!value) return undefined;
14
+ const inputTokens = finiteToken(value.input_tokens ?? value.inputTokens ?? value.prompt_tokens);
15
+ const outputTokens = finiteToken(
16
+ value.output_tokens ?? value.outputTokens ?? value.completion_tokens,
17
+ );
18
+ const totalTokens = finiteToken(value.total_tokens ?? value.totalTokens ?? value.total);
19
+ if (inputTokens === undefined && outputTokens === undefined && totalTokens === undefined)
20
+ return undefined;
21
+ return {
22
+ ...(inputTokens === undefined ? {} : { inputTokens }),
23
+ ...(outputTokens === undefined ? {} : { outputTokens }),
24
+ ...(totalTokens === undefined ? {} : { totalTokens }),
25
+ };
26
+ }
27
+
28
+ /** Merge usage fields without retaining any provider event payload. */
29
+ export function mergeUsage(
30
+ left: ConsultationUsage | undefined,
31
+ right: ConsultationUsage | undefined,
32
+ ): ConsultationUsage | undefined {
33
+ if (!left) return right;
34
+ if (!right) return left;
35
+ return {
36
+ ...(left.inputTokens === undefined && right.inputTokens === undefined
37
+ ? {}
38
+ : { inputTokens: right.inputTokens ?? left.inputTokens }),
39
+ ...(left.outputTokens === undefined && right.outputTokens === undefined
40
+ ? {}
41
+ : { outputTokens: right.outputTokens ?? left.outputTokens }),
42
+ ...(left.totalTokens === undefined && right.totalTokens === undefined
43
+ ? {}
44
+ : { totalTokens: right.totalTokens ?? left.totalTokens }),
45
+ };
46
+ }
47
+
48
+ function finiteToken(value: unknown): number | undefined {
49
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
50
+ }
@@ -0,0 +1,58 @@
1
+ import { type TSchema, Type } from "typebox";
2
+ import { Value } from "typebox/value";
3
+ import type { ConsultationAnswer } from "../../types.ts";
4
+
5
+ /** Maximum length of Antigravity's final answer text. */
6
+ export const MAX_ANSWER_CHARS = 16_000;
7
+ /** Maximum source references accepted from one answer. */
8
+ export const MAX_SOURCES = 8;
9
+ /** Maximum workspace references accepted from one answer. */
10
+ export const MAX_WORKSPACE_EVIDENCE = 8;
11
+ /** Maximum source title length. */
12
+ export const MAX_SOURCE_TITLE_CHARS = 240;
13
+ /** Maximum source URL length. */
14
+ export const MAX_SOURCE_URL_CHARS = 1_024;
15
+ /** Maximum workspace evidence path length. */
16
+ export const MAX_WORKSPACE_PATH_CHARS = 512;
17
+ /** Maximum workspace evidence summary length. */
18
+ export const MAX_EVIDENCE_SUMMARY_CHARS = 1_000;
19
+
20
+ /** JSON Schema passed to agy for every paid Antigravity Run. */
21
+ export const ANTIGRAVITY_ANSWER_SCHEMA: TSchema = Type.Object(
22
+ {
23
+ answer: Type.String({
24
+ minLength: 1,
25
+ maxLength: MAX_ANSWER_CHARS,
26
+ description: "The answer to the user's request.",
27
+ }),
28
+ sources: Type.Array(
29
+ Type.Object(
30
+ {
31
+ title: Type.String({ minLength: 1, maxLength: MAX_SOURCE_TITLE_CHARS }),
32
+ url: Type.String({ minLength: 1, maxLength: MAX_SOURCE_URL_CHARS }),
33
+ },
34
+ { additionalProperties: false },
35
+ ),
36
+ { maxItems: MAX_SOURCES },
37
+ ),
38
+ workspaceEvidence: Type.Array(
39
+ Type.Object(
40
+ {
41
+ path: Type.String({ minLength: 1, maxLength: MAX_WORKSPACE_PATH_CHARS }),
42
+ summary: Type.String({ minLength: 1, maxLength: MAX_EVIDENCE_SUMMARY_CHARS }),
43
+ },
44
+ { additionalProperties: false },
45
+ ),
46
+ { maxItems: MAX_WORKSPACE_EVIDENCE },
47
+ ),
48
+ },
49
+ { additionalProperties: false },
50
+ );
51
+
52
+ /** Validate and return the exact structured answer shape. */
53
+ export function validateAntigravityAnswer(value: unknown): ConsultationAnswer {
54
+ if (!Value.Check(ANTIGRAVITY_ANSWER_SCHEMA, value)) {
55
+ throw new Error("Antigravity returned invalid structured output.");
56
+ }
57
+ return value as ConsultationAnswer;
58
+ }
@@ -0,0 +1,17 @@
1
+ import type { ConsultationAnswer, ConsultationUsage } from "../../types.ts";
2
+
3
+ /** Process facts retained only inside the Antigravity adapter. */
4
+ export interface AntigravityExecutionFacts {
5
+ answer: ConsultationAnswer;
6
+ conversationId: string;
7
+ usage?: ConsultationUsage;
8
+ observedToolNames: string[];
9
+ observedToolCounts: Record<string, number>;
10
+ successfulToolNames: string[];
11
+ permissionDenials: number;
12
+ observedSourceHashes: string[];
13
+ observedWorkspacePathHashes: string[];
14
+ }
15
+
16
+ /** Safe, bounded progress from one Antigravity event. */
17
+ export type AntigravityProgressCallback = (activity: string) => void;
@@ -0,0 +1,27 @@
1
+ const FAILURE_MESSAGES = {
2
+ unavailable: "The Consulting Agent is unavailable. Reload PI to check availability.",
3
+ timeout: "The Consultation timed out.",
4
+ cancelled: "The Consultation was canceled.",
5
+ "invalid-response": "The Consulting Agent returned an invalid response.",
6
+ "invalid-continuation": "Invalid Conversation Handle continuation state.",
7
+ "conversation-mismatch":
8
+ "The Consulting Agent returned a different conversation for this handle.",
9
+ execution: "Consultation execution failed.",
10
+ } as const;
11
+
12
+ /** Safe failure categories that can leave a private Consulting Agent adapter. */
13
+ export type ConsultingAgentFailureKind = keyof typeof FAILURE_MESSAGES;
14
+
15
+ /**
16
+ * A bounded failure with no agent diagnostics or error cause.
17
+ * Construct messages from fixed categories, never from process output.
18
+ */
19
+ export class ConsultingAgentError extends Error {
20
+ readonly kind: ConsultingAgentFailureKind;
21
+
22
+ constructor(kind: ConsultingAgentFailureKind) {
23
+ super(FAILURE_MESSAGES[kind]);
24
+ this.name = "ConsultingAgentError";
25
+ this.kind = kind;
26
+ }
27
+ }
@@ -0,0 +1,43 @@
1
+ import type { ConsultationExecutionFacts, ConsultingAgentId, ConsultingModel } from "../types.ts";
2
+
3
+ /** Immutable availability returned by one Consulting Agent adapter. */
4
+ export interface ConsultingAgentAvailability {
5
+ status: "available";
6
+ agent: ConsultingAgentId;
7
+ agentVersion: string;
8
+ catalogue: readonly ConsultingModel[];
9
+ }
10
+
11
+ /** A bounded reason why a Consulting Agent is not available. */
12
+ export interface ConsultingAgentUnavailable {
13
+ status: "unavailable";
14
+ reason: string;
15
+ warning: string;
16
+ agentVersion?: string;
17
+ }
18
+
19
+ /** Result of one session-scoped agent discovery. */
20
+ export type ConsultingAvailability = ConsultingAgentAvailability | ConsultingAgentUnavailable;
21
+
22
+ /** Inputs to one bounded Consultation executed by an agent adapter. */
23
+ export interface ConsultingAgentExecutionOptions {
24
+ prompt: string;
25
+ model: ConsultingModel;
26
+ canonicalWorkingDirectory: string;
27
+ workspaceAccess: boolean;
28
+ continuation?: string;
29
+ signal?: AbortSignal;
30
+ onProgress?: (activity: string) => void;
31
+ onWarning?: (warning: string) => void;
32
+ onProcessStart?: () => void;
33
+ }
34
+
35
+ /** Private execution and discovery interface for the supported Consulting Agent. */
36
+ export interface ConsultingAgentAdapter {
37
+ readonly identity: ConsultingAgentId;
38
+ /** The agent's stable, package-managed empty Consultation Workspace. */
39
+ readonly consultationWorkspace: string;
40
+ discover(options?: { signal?: AbortSignal }): Promise<ConsultingAvailability>;
41
+ /** Reject with a ConsultingAgentError; raw diagnostics and causes stay inside the adapter. */
42
+ execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts>;
43
+ }
@@ -0,0 +1,17 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import type { TSchema } from "typebox";
3
+
4
+ /** Build a provider-facing model enum from one immutable agent catalogue. */
5
+ export function buildModelCatalogueEnum(catalogue: readonly string[]): TSchema {
6
+ if (catalogue.length === 0) {
7
+ throw new Error("Cannot build a model enum from an empty Consulting Agent catalogue.");
8
+ }
9
+ return StringEnum([...catalogue] as [string, ...string[]], {
10
+ description: "Model available in the current Consulting Agent catalogue.",
11
+ });
12
+ }
13
+
14
+ /** Check a model ID against the same discovered catalogue used by the schema. */
15
+ export function isAvailableModel(value: unknown, catalogue: readonly string[]): value is string {
16
+ return typeof value === "string" && catalogue.includes(value);
17
+ }
package/src/config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
2
+
3
+ /** The persisted SuPi configuration section owned by this package. */
4
+ export const CONSULTING_CONFIG_SECTION = "consulting";
5
+
6
+ /** Configuration for Consulting availability and the consulting_run tool. */
7
+ export interface ConsultingConfig extends Record<string, unknown> {
8
+ /** Enable agent availability discovery and the consulting_run tool. */
9
+ agentToolEnabled: boolean;
10
+ }
11
+
12
+ /** Default Consulting configuration. */
13
+ export const CONSULTING_DEFAULTS: ConsultingConfig = Object.freeze({
14
+ agentToolEnabled: true,
15
+ });
16
+
17
+ /** Load only the new consulting configuration section for a workspace. */
18
+ export function loadConsultingConfig(cwd: string, homeDir?: string): ConsultingConfig {
19
+ const raw = loadSupiConfig(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, { homeDir });
20
+ return {
21
+ agentToolEnabled:
22
+ typeof raw.agentToolEnabled === "boolean"
23
+ ? raw.agentToolEnabled
24
+ : CONSULTING_DEFAULTS.agentToolEnabled,
25
+ };
26
+ }