@mrclrchtr/supi-consulting 11.2.0 → 11.3.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/CHANGELOG.md +9 -0
- package/CLAUDE.md +1 -1
- package/README.md +19 -5
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
- package/package.json +3 -2
- package/scripts/live-probe.ts +79 -34
- package/src/agents/antigravity/adapter.ts +26 -5
- package/src/agents/antigravity/availability.ts +2 -2
- package/src/agents/antigravity/inspection-agent.ts +53 -0
- package/src/agents/antigravity/process/events.ts +2 -0
- package/src/agents/antigravity/process/permission-diagnostics.ts +15 -0
- package/src/agents/antigravity/process/runner.ts +22 -6
- package/src/agents/errors.ts +2 -1
- package/src/catalogue.ts +3 -3
- package/src/tool/consulting_run/execute.ts +13 -2
- package/src/tool/consulting_run/input.ts +2 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [11.3.0]
|
|
4
|
+
|
|
5
|
+
- Added package-specific release notes for bounded agent consultations.
|
|
6
|
+
- Consultations now use an explicit read-only Antigravity tool selection for web search, direct URL reading, and workspace inspection, with structured completion and observed source evidence.
|
|
7
|
+
- Follow-ups that ask the agent not to use tools no longer fail with an unverifiable answer.
|
|
8
|
+
- Reading cached web pages no longer reports workspace use or a misleading workspace warning.
|
|
9
|
+
- Requires Antigravity CLI `1.4.9` or newer. Start a new Consultation after upgrading: existing default-agent Conversation Handles cannot gain the new tools; they are rejected before process startup, retired, and recorded as `info`-level `consultation.rejected` Debug events instead of errors.
|
package/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- Keep agent-specific models, authentication, process code, protocol parsing, and tool-activity classification in `src/agents/antigravity/`. Shared workflow uses the private interface in `src/agents/types.ts`.
|
|
10
10
|
- Keep each Model Catalogue immutable until the next session start or reload. Register `consulting_run` only after agent discovery succeeds.
|
|
11
11
|
- Require explicit agent, model, and Workspace Access for a new Consultation. Keep `new` and `continue` exact-one; follow-ups inherit all selections and their working directory.
|
|
12
|
-
- Acquire a handle before process work. Report process start, including hook probes, through the adapter interface. Reject overlap before startup; retire
|
|
12
|
+
- Acquire a handle before process work. Report process start, including hook probes, through the adapter interface. Reject overlap before startup; retire after a started follow-up fails or is canceled, or when the adapter rejects incompatible continuation state (which can never succeed).
|
|
13
13
|
- Keep raw conversation IDs opaque to shared policy. Persist only new consulting handle records and results; old Antigravity state has no migration or legacy read path.
|
|
14
14
|
- Use `<PI agent dir>/supi/consulting/antigravity/` for Antigravity state. Leave the old `supi/antigravity/` tree untouched.
|
|
15
15
|
- Use the Isolated Antigravity Home for every `agy` process. On macOS, create and unlock its private keychain, exposed as the login keychain, before startup. Do not copy the normal profile, credentials, or PI provider environment.
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ cd "<consultation-workspace>" &&
|
|
|
44
44
|
HOME="<isolated-home>" AGY_CLI_DISABLE_AUTO_UPDATE=true agy
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
On macOS, the adapter creates and unlocks a private keychain inside the Isolated Antigravity Home. It exposes that keychain as the login keychain without prompting for the normal keychain password. Run the displayed command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.
|
|
47
|
+
On macOS, the adapter creates and unlocks a private keychain inside the Isolated Antigravity Home. It exposes that keychain as the login keychain without prompting for the normal keychain password. Run the displayed command, sign in, exit Antigravity, and reload Pi. The installed `agy` version must be at least `1.4.9`.
|
|
48
48
|
|
|
49
49
|
If macOS displays `antigravity.` in a warning, the final `.` is sentence punctuation, not part of a keychain name.
|
|
50
50
|
|
|
@@ -76,10 +76,24 @@ A follow-up supplies only a returned Conversation Handle:
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Supply exactly one of `new` and `continue`. Follow-ups retain the original agent, model, working directory, and Workspace Access. A handle is opaque and branch-aware. It is retired if a follow-up fails or is canceled after process work starts. Overlapping follow-ups on the same handle are rejected.
|
|
79
|
+
Supply exactly one of `new` and `continue`. Follow-ups retain the original agent, model, working directory, and Workspace Access. A handle is opaque and branch-aware. It is retired if a follow-up fails or is canceled after process work starts, or if its continuation state is incompatible. Overlapping follow-ups on the same handle are rejected.
|
|
80
80
|
|
|
81
81
|
Results report web use, workspace use, observed and claimed references, warnings, token use, and the Conversation Handle. An answer can claim a reference without matching tool activity; SuPi labels that reference as claimed, not observed.
|
|
82
82
|
|
|
83
|
+
### Inspection tools and existing handles
|
|
84
|
+
|
|
85
|
+
New Antigravity conversations use a package-owned temporary agent definition with explicit web search (`search_web`), direct URL reading (`read_url_content`), read-only file inspection (`view_file`, `list_dir`, `find_by_name`, `grep_search`), and structured completion (`finish`). The default Antigravity agent's tool selection is not used. A permission such as `read_url(*)` authorizes access; it does not itself make a URL-reading tool available.
|
|
86
|
+
|
|
87
|
+
The definition lives in the Isolated Antigravity Home under a random name to avoid workspace-agent name collisions. SuPi removes it after the process, including ordinary failures, timeouts, and cancellation. A hard process crash can leave a temporary definition behind. Follow-ups resume the saved conversation without a new agent override; Antigravity retains the original definition even after its file is removed.
|
|
88
|
+
|
|
89
|
+
**Start a new Consultation after upgrading from the default-agent integration.** Old Conversation Handles cannot acquire the inspection tools by resuming. SuPi rejects legacy or incompatible profile continuations before starting any Antigravity process, retires the handle, and asks for a new Consultation. Profile compatibility is tracked privately; the calling tool's input shape is unchanged.
|
|
90
|
+
|
|
91
|
+
Follow-up prompts get one fixed line asking the agent to end with `finish` using the current schema. Without it, a follow-up such as "do not use any tools" can skip `finish`, and Antigravity then replays the previous turn's answer, which SuPi rejects as unbound.
|
|
92
|
+
|
|
93
|
+
Antigravity caches fetched pages inside its own conversation state and may read them back with `view_file`. File activity whose path resolves outside the selected workspace and configured read-only directories is treated as agent-internal: it does not count as workspace use or workspace evidence. File calls without a classifiable path remain counted.
|
|
94
|
+
|
|
95
|
+
Web activity alone does not prove a cited page was read. For research, the agent is instructed to search for sources and directly read relevant pages before citing them. SuPi still classifies references from observed activity, not from the agent's claims. The CLI's startup tool inventory is not treated as proof that a tool is active for the selected agent.
|
|
96
|
+
|
|
83
97
|
Use `/supi-settings` → **Consulting** to disable the tool. Disabling it skips availability checks and removes `consulting_run` from the active tool list.
|
|
84
98
|
|
|
85
99
|
## Additional read-only directories
|
|
@@ -120,13 +134,13 @@ An observed tool permission denial is reported separately. It does not prove the
|
|
|
120
134
|
|
|
121
135
|
Failure messages returned to the calling agent include the observed terminal status and denial counts by fixed operation category (`command`, `file-read`, `file-write`, `web-read`, `browser`, `mcp`, or `other`). They distinguish a successful agent terminal status from a usable final answer and recommend permitted inspection or a limited/blocked report rather than repeating denied actions unchanged. This feedback remains available when Debug is disabled.
|
|
122
136
|
|
|
123
|
-
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:
|
|
137
|
+
When SuPi Debug is enabled, failed executions emit one `consulting/consultation.failed` event at `error` level. Cancellations emit `consulting/consultation.cancelled` at `info` level. Rejected incompatible Conversation Handles emit `consulting/consultation.rejected` at `info` level; no process was started for them. Use the `debug` tool:
|
|
124
138
|
|
|
125
139
|
```json
|
|
126
140
|
{ "source": "consulting", "level": "error", "limit": 10 }
|
|
127
141
|
```
|
|
128
142
|
|
|
129
|
-
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.
|
|
143
|
+
Omit `level` to include cancellations and rejected handles. Add `sessionFile` to inspect events persisted in a saved PI session. A response aborted before the adapter starts has no Consulting event.
|
|
130
144
|
|
|
131
145
|
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. `permissionDenialsByOperation` breaks down deduplicated denials by fixed category; unknown tool names become `other`. `deniedFileAccessOutsideReadRoots` counts denied file operations whose requested paths resolve outside the selected directory and configured additional read-only directories, following symlinks and existing parents of missing targets. Unavailable path information, dangling symlink destinations, and paths containing parent traversal (`..`) remain unclassified rather than risking an incorrect outside-root claim. This location observation does not identify which rule caused a denial.
|
|
132
146
|
|
|
@@ -163,4 +177,4 @@ The live probe requires separate approval and sign-in. It never runs in normal t
|
|
|
163
177
|
SUPI_CONSULTING_LIVE=1 pnpm exec jiti packages/supi-consulting/scripts/live-probe.ts
|
|
164
178
|
```
|
|
165
179
|
|
|
166
|
-
It uses the isolated profile and
|
|
180
|
+
It uses the isolated profile and requires successful web search and direct URL reading, an observed source matching the fetched URL, and a schema-validated follow-up that retains context and the URL reader after the temporary definition has been removed. It also checks observed workspace evidence and byte-identical fixture state. The direct-reader checks use the private process interface; workspace checks use the Consulting Agent adapter. No paid capability checks run during normal discovery.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Session-provided Web tools for SuPi child sessions (Agent Runs and Reviewer Sessions).
|
|
2
|
+
//
|
|
3
|
+
// The loaded supi-web extension answers a synchronous request on the parent event bus.
|
|
4
|
+
// Consumers never depend on supi-web; when it is absent or provides no tools, the
|
|
5
|
+
// request returns undefined and the child simply lacks Web tools.
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
/** Web tools that child sessions may receive when the containing session provides them. */
|
|
10
|
+
export const CHILD_WEB_TOOL_NAMES = [
|
|
11
|
+
"web_fetch_md",
|
|
12
|
+
"web_search",
|
|
13
|
+
"web_docs_search",
|
|
14
|
+
"web_docs_fetch",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/** One session-provided Web tool name. */
|
|
18
|
+
export type ChildWebToolName = (typeof CHILD_WEB_TOOL_NAMES)[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Web tools currently callable in the containing session plus a child-only extension
|
|
22
|
+
* factory that registers them. The factory must not capture the parent session; it may
|
|
23
|
+
* capture plain data such as the containing session's configuration directory.
|
|
24
|
+
*/
|
|
25
|
+
export interface ChildWebToolProvision {
|
|
26
|
+
readonly toolNames: readonly ChildWebToolName[];
|
|
27
|
+
/** Register the requested subset of `toolNames` in a child session. */
|
|
28
|
+
readonly createFactory: (toolNames: readonly ChildWebToolName[]) => ExtensionFactory;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Plain containing-session data supplied with a child Web tool request. */
|
|
32
|
+
export interface ChildWebToolsRequestContext {
|
|
33
|
+
/**
|
|
34
|
+
* Working directory whose SuPi configuration governs Web settings such as Firecrawl.
|
|
35
|
+
* Children with a different cwd (for example a frozen Review Workspace) keep this policy.
|
|
36
|
+
*/
|
|
37
|
+
readonly configCwd: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type EventBusLike = Pick<ExtensionAPI["events"], "on" | "emit">;
|
|
41
|
+
|
|
42
|
+
const REQUEST_CHANNEL = "supi:child-web-tools:request";
|
|
43
|
+
const RESPONSE_CHANNEL = "supi:child-web-tools:response";
|
|
44
|
+
|
|
45
|
+
interface ChildWebToolsRequest extends ChildWebToolsRequestContext {
|
|
46
|
+
readonly requestId: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ChildWebToolsResponse {
|
|
50
|
+
readonly requestId: string;
|
|
51
|
+
readonly provision: ChildWebToolProvision;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let nextRequestId = 0;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Answer child Web tool requests on the parent event bus. The callback must be synchronous
|
|
58
|
+
* because Pi's event bus does not await listeners. Returns an unsubscribe function.
|
|
59
|
+
*/
|
|
60
|
+
export function provideChildWebTools(
|
|
61
|
+
events: EventBusLike,
|
|
62
|
+
resolve: (context: ChildWebToolsRequestContext) => ChildWebToolProvision | undefined,
|
|
63
|
+
): () => void {
|
|
64
|
+
return events.on(REQUEST_CHANNEL, (data) => {
|
|
65
|
+
const request = data as Partial<ChildWebToolsRequest> | undefined;
|
|
66
|
+
if (typeof request?.requestId !== "string" || typeof request.configCwd !== "string") return;
|
|
67
|
+
const { requestId, configCwd } = request;
|
|
68
|
+
const provision = resolve({ configCwd });
|
|
69
|
+
if (!provision || provision.toolNames.length === 0) return;
|
|
70
|
+
events.emit(RESPONSE_CHANNEL, { requestId, provision } satisfies ChildWebToolsResponse);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Capture the Web tools the containing session currently provides, if any. */
|
|
75
|
+
export function requestChildWebTools(
|
|
76
|
+
events: EventBusLike,
|
|
77
|
+
context: ChildWebToolsRequestContext,
|
|
78
|
+
): ChildWebToolProvision | undefined {
|
|
79
|
+
const requestId = `child-web-${process.pid}-${++nextRequestId}`;
|
|
80
|
+
let provision: ChildWebToolProvision | undefined;
|
|
81
|
+
const unsubscribe = events.on(RESPONSE_CHANNEL, (data) => {
|
|
82
|
+
const response = data as Partial<ChildWebToolsResponse> | undefined;
|
|
83
|
+
if (response?.requestId === requestId && response.provision) provision = response.provision;
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
events.emit(REQUEST_CHANNEL, { requestId, ...context } satisfies ChildWebToolsRequest);
|
|
87
|
+
} finally {
|
|
88
|
+
unsubscribe();
|
|
89
|
+
}
|
|
90
|
+
return provision;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Return the Web tools callable in a session under Pi's exposure rules: active `direct`
|
|
95
|
+
* tools, plus registered `codemode` and `deferred` tools. `model-only` and `hidden`
|
|
96
|
+
* tools are not callable.
|
|
97
|
+
*/
|
|
98
|
+
export function callableChildWebToolNames(
|
|
99
|
+
pi: Pick<ExtensionAPI, "getAllTools" | "getActiveTools">,
|
|
100
|
+
): ChildWebToolName[] {
|
|
101
|
+
const active = new Set(pi.getActiveTools());
|
|
102
|
+
const exposures = new Map(
|
|
103
|
+
pi.getAllTools().map((tool) => [tool.name, tool.exposure ?? "direct"] as const),
|
|
104
|
+
);
|
|
105
|
+
return CHILD_WEB_TOOL_NAMES.filter((name) => {
|
|
106
|
+
const exposure = exposures.get(name);
|
|
107
|
+
if (exposure === "direct") return active.has(name);
|
|
108
|
+
return exposure === "codemode" || exposure === "deferred";
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// supi-core session domain — session utilities and registries.
|
|
2
2
|
|
|
3
|
+
export type {
|
|
4
|
+
ChildWebToolName,
|
|
5
|
+
ChildWebToolProvision,
|
|
6
|
+
ChildWebToolsRequestContext,
|
|
7
|
+
} from "./child-web-tools.ts";
|
|
8
|
+
export {
|
|
9
|
+
CHILD_WEB_TOOL_NAMES,
|
|
10
|
+
callableChildWebToolNames,
|
|
11
|
+
provideChildWebTools,
|
|
12
|
+
requestChildWebTools,
|
|
13
|
+
} from "./child-web-tools.ts";
|
|
3
14
|
export { createRegistry, createSessionStateRegistry } from "./registry-utils.ts";
|
|
4
15
|
export type {
|
|
5
16
|
SessionCapabilitySkill,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-consulting",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "Bounded consultations with explicit agent and workspace selection and observed evidence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"type": "module",
|
|
31
31
|
"files": [
|
|
32
|
+
"CHANGELOG.md",
|
|
32
33
|
"src/**/*.ts",
|
|
33
34
|
"scripts/live-probe.ts",
|
|
34
35
|
"README.md",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"docs/adr/*.md"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@mrclrchtr/supi-core": "11.
|
|
41
|
+
"@mrclrchtr/supi-core": "11.3.0"
|
|
41
42
|
},
|
|
42
43
|
"bundledDependencies": [
|
|
43
44
|
"@mrclrchtr/supi-core"
|
package/scripts/live-probe.ts
CHANGED
|
@@ -4,11 +4,20 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join, relative } from "node:path";
|
|
5
5
|
import { createAntigravityAdapter } from "../src/agents/antigravity/adapter.ts";
|
|
6
6
|
import { getIsolatedAntigravityPaths } from "../src/agents/antigravity/isolated-home.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { runAntigravityConversation } from "../src/agents/antigravity/process/runner.ts";
|
|
8
|
+
import { ANTIGRAVITY_ANSWER_SCHEMA } from "../src/agents/antigravity/structured-output.ts";
|
|
9
|
+
import type { AntigravityExecutionFacts } from "../src/agents/antigravity/types.ts";
|
|
10
|
+
import {
|
|
11
|
+
classifyAnswerEvidence,
|
|
12
|
+
isHttpUrl,
|
|
13
|
+
isSafeWorkspacePath,
|
|
14
|
+
} from "../src/tool/consulting_run/evidence.ts";
|
|
15
|
+
import type { ConsultationExecutionFacts } from "../src/types.ts";
|
|
8
16
|
|
|
9
17
|
const LIVE_ENVIRONMENT = "SUPI_CONSULTING_LIVE";
|
|
10
18
|
const MODEL = "gemini-3.8-flash-low";
|
|
11
19
|
const CONTEXT_TOKEN = "supi-consulting-context-token";
|
|
20
|
+
const SOURCE_URL = "https://nodejs.org/api/child_process.html";
|
|
12
21
|
|
|
13
22
|
async function main(): Promise<void> {
|
|
14
23
|
if (process.env[LIVE_ENVIRONMENT] !== "1") {
|
|
@@ -21,22 +30,56 @@ async function main(): Promise<void> {
|
|
|
21
30
|
throw new Error("The selected model is not available to the isolated Antigravity account.");
|
|
22
31
|
}
|
|
23
32
|
const consultationWorkspace = await realpath(paths.consultationWorkspace);
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
// The process interface exposes successful tool names without adding them to the
|
|
34
|
+
// public Consultation contract. Mere web activity or claimed URLs cannot pass.
|
|
35
|
+
const processOptions = {
|
|
36
|
+
paths,
|
|
26
37
|
model: MODEL,
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
cwd: consultationWorkspace,
|
|
39
|
+
schema: ANTIGRAVITY_ANSWER_SCHEMA as Record<string, unknown>,
|
|
40
|
+
};
|
|
41
|
+
const first = await runAntigravityConversation({
|
|
42
|
+
...processOptions,
|
|
43
|
+
prompt: `Remember the exact token ${CONTEXT_TOKEN}. Use search_web to find official Node.js subprocess cancellation documentation, then use read_url_content to read ${SOURCE_URL}. Explain cancellation briefly and cite that exact fetched URL.`,
|
|
29
44
|
});
|
|
30
|
-
|
|
45
|
+
if (!first.successfulToolNames.includes("search_web")) {
|
|
46
|
+
throw new Error("The live probe did not observe a successful web search.");
|
|
47
|
+
}
|
|
48
|
+
requireDirectRead(first, consultationWorkspace);
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
// The first run has already removed its temporary profile. Resume only by ID,
|
|
51
|
+
// proving the CLI retains the reader and finish capability in conversation state.
|
|
52
|
+
const second = await runAntigravityConversation({
|
|
53
|
+
...processOptions,
|
|
54
|
+
prompt: `Read ${SOURCE_URL} again using read_url_content and cite that exact URL. In the answer field return only the exact context token I gave you in the preceding request.`,
|
|
55
|
+
conversationId: first.conversationId,
|
|
56
|
+
});
|
|
57
|
+
if (second.conversationId !== first.conversationId) {
|
|
58
|
+
throw new Error("The live probe resumed a different conversation.");
|
|
59
|
+
}
|
|
60
|
+
requireDirectRead(second, consultationWorkspace);
|
|
61
|
+
|
|
62
|
+
// A tool-free follow-up used to skip finish and replay the previous answer.
|
|
63
|
+
const toolFree = await adapter.execute({
|
|
64
|
+
prompt: "Return only the exact context token I gave you earlier. Do not use any tools.",
|
|
35
65
|
model: MODEL,
|
|
36
66
|
canonicalWorkingDirectory: consultationWorkspace,
|
|
37
67
|
workspaceAccess: false,
|
|
38
|
-
continuation:
|
|
68
|
+
continuation: (
|
|
69
|
+
await adapter.execute({
|
|
70
|
+
prompt: `Remember the exact token ${CONTEXT_TOKEN}. Use read_url_content to read ${SOURCE_URL} and cite it.`,
|
|
71
|
+
model: MODEL,
|
|
72
|
+
canonicalWorkingDirectory: consultationWorkspace,
|
|
73
|
+
workspaceAccess: false,
|
|
74
|
+
})
|
|
75
|
+
).continuation,
|
|
39
76
|
});
|
|
77
|
+
if (toolFree.answer.answer.trim() !== CONTEXT_TOKEN) {
|
|
78
|
+
throw new Error("The tool-free follow-up did not return the current context token.");
|
|
79
|
+
}
|
|
80
|
+
if (toolFree.workspaceUsed) {
|
|
81
|
+
throw new Error("Cached page reads were misreported as workspace use.");
|
|
82
|
+
}
|
|
40
83
|
if (second.answer.answer.trim() !== CONTEXT_TOKEN) {
|
|
41
84
|
throw new Error(
|
|
42
85
|
"The follow-up did not return the requested current-turn answer from conversation context.",
|
|
@@ -56,7 +99,7 @@ async function main(): Promise<void> {
|
|
|
56
99
|
canonicalWorkingDirectory: fixture,
|
|
57
100
|
workspaceAccess: true,
|
|
58
101
|
});
|
|
59
|
-
requireWebEvidence(workspace);
|
|
102
|
+
requireWebEvidence(workspace, fixture);
|
|
60
103
|
requireWorkspaceEvidence(workspace, fixture);
|
|
61
104
|
if (!workspace.workspaceUsed) {
|
|
62
105
|
throw new Error("The workspace probe did not observe workspace activity.");
|
|
@@ -68,40 +111,42 @@ async function main(): Promise<void> {
|
|
|
68
111
|
}
|
|
69
112
|
}
|
|
70
113
|
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
type EvidenceFacts = Pick<
|
|
115
|
+
ConsultationExecutionFacts,
|
|
116
|
+
"answer" | "observedSourceHashes" | "observedWorkspacePathHashes"
|
|
117
|
+
>;
|
|
118
|
+
|
|
119
|
+
function requireDirectRead(result: AntigravityExecutionFacts, workspace: string): void {
|
|
120
|
+
if (!result.successfulToolNames.includes("read_url_content")) {
|
|
121
|
+
throw new Error("The live probe did not observe a successful direct URL read.");
|
|
122
|
+
}
|
|
123
|
+
requireWebEvidence(result, workspace);
|
|
124
|
+
const evidence = classifyAnswerEvidence(result.answer, result, workspace);
|
|
125
|
+
if (!evidence.observedSources.some((source) => source.url === SOURCE_URL)) {
|
|
126
|
+
throw new Error("The live probe did not return the fetched URL as observed evidence.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requireWebEvidence(result: EvidenceFacts, workspace: string): void {
|
|
131
|
+
const evidence = classifyAnswerEvidence(result.answer, result, workspace);
|
|
132
|
+
if (evidence.observedSources.length === 0) {
|
|
73
133
|
throw new Error("The live probe did not observe web evidence.");
|
|
74
134
|
}
|
|
75
|
-
if (
|
|
76
|
-
!result.answer.sources.every(
|
|
77
|
-
(source) => isRecord(source) && typeof source.url === "string" && isHttpUrl(source.url),
|
|
78
|
-
)
|
|
79
|
-
) {
|
|
135
|
+
if (!result.answer.sources.every((source) => isHttpUrl(source.url))) {
|
|
80
136
|
throw new Error("The live probe returned an invalid source URL.");
|
|
81
137
|
}
|
|
82
138
|
}
|
|
83
139
|
|
|
84
|
-
function requireWorkspaceEvidence(
|
|
85
|
-
|
|
86
|
-
workspace: string,
|
|
87
|
-
): void {
|
|
140
|
+
function requireWorkspaceEvidence(result: EvidenceFacts, workspace: string): void {
|
|
141
|
+
const evidence = classifyAnswerEvidence(result.answer, result, workspace);
|
|
88
142
|
if (
|
|
89
|
-
|
|
90
|
-
!result.answer.workspaceEvidence.every(
|
|
91
|
-
(item) =>
|
|
92
|
-
isRecord(item) &&
|
|
93
|
-
typeof item.path === "string" &&
|
|
94
|
-
isSafeWorkspacePath(item.path, workspace),
|
|
95
|
-
)
|
|
143
|
+
evidence.observedWorkspaceEvidence.length === 0 ||
|
|
144
|
+
!result.answer.workspaceEvidence.every((item) => isSafeWorkspacePath(item.path, workspace))
|
|
96
145
|
) {
|
|
97
|
-
throw new Error("The live probe did not return valid workspace evidence.");
|
|
146
|
+
throw new Error("The live probe did not return observed, valid workspace evidence.");
|
|
98
147
|
}
|
|
99
148
|
}
|
|
100
149
|
|
|
101
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
102
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
150
|
async function digestTree(directory: string): Promise<string> {
|
|
106
151
|
const hash = createHash("sha256");
|
|
107
152
|
await digestDirectory(directory, directory, hash);
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "../types.ts";
|
|
10
10
|
import { isWebToolName, isWorkspaceToolName } from "./activity.ts";
|
|
11
11
|
import { discoverAntigravityAvailability } from "./availability.ts";
|
|
12
|
+
import { INSPECTION_AGENT_FINGERPRINT } from "./inspection-agent.ts";
|
|
12
13
|
import {
|
|
13
14
|
getIsolatedAntigravityPaths,
|
|
14
15
|
type IsolatedAntigravityPaths,
|
|
@@ -22,7 +23,12 @@ import { ANTIGRAVITY_ANSWER_SCHEMA } from "./structured-output.ts";
|
|
|
22
23
|
const MAX_RETAINED_REFERENCE_HASHES = 512;
|
|
23
24
|
const MAX_WARNING_COUNT = 24;
|
|
24
25
|
const MAX_WARNING_CHARS = 500;
|
|
25
|
-
|
|
26
|
+
// A resumed CLI retains its original profile and ignores --agent overrides.
|
|
27
|
+
// Reject pre-profile conversations and changed contracts before any process work.
|
|
28
|
+
const CONTINUATION_PREFIX = `agy2.${INSPECTION_AGENT_FINGERPRINT}.`;
|
|
29
|
+
// Follow-ups saying "do not use any tools" can skip finish; agy then replays the
|
|
30
|
+
// previous turn's structured answer, which the response token rejects. Tested live.
|
|
31
|
+
const FOLLOW_UP_RESPONSE_CONTRACT = "\n\n[Always end with finish using the current schema.]";
|
|
26
32
|
|
|
27
33
|
type ExecutionPhase = "preparation" | "hook-probe" | "conversation" | "validation";
|
|
28
34
|
|
|
@@ -78,12 +84,12 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
|
|
|
78
84
|
});
|
|
79
85
|
} catch (error) {
|
|
80
86
|
const failure = safeExecutionFailure(error);
|
|
81
|
-
const
|
|
87
|
+
const outcome = failureOutcome(failure.kind);
|
|
82
88
|
timer.finish(
|
|
83
89
|
() => ({
|
|
84
90
|
source: "consulting",
|
|
85
|
-
level:
|
|
86
|
-
category:
|
|
91
|
+
level: outcome.level,
|
|
92
|
+
category: outcome.category,
|
|
87
93
|
message: failure.message,
|
|
88
94
|
cwd: options.canonicalWorkingDirectory,
|
|
89
95
|
data: {
|
|
@@ -121,7 +127,9 @@ class AntigravityAdapter implements ConsultingAgentAdapter {
|
|
|
121
127
|
const rawFacts = await runAntigravityConversation({
|
|
122
128
|
paths: this.#paths,
|
|
123
129
|
cwd: options.canonicalWorkingDirectory,
|
|
124
|
-
prompt:
|
|
130
|
+
prompt: expectedConversationId
|
|
131
|
+
? `${options.prompt}${FOLLOW_UP_RESPONSE_CONTRACT}`
|
|
132
|
+
: options.prompt,
|
|
125
133
|
model: options.model,
|
|
126
134
|
...(expectedConversationId ? { conversationId: expectedConversationId } : {}),
|
|
127
135
|
...(options.workspaceAccess ? { workspaceDirectory: options.canonicalWorkingDirectory } : {}),
|
|
@@ -220,6 +228,19 @@ function decodeContinuation(value: string): string {
|
|
|
220
228
|
return conversationId;
|
|
221
229
|
}
|
|
222
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Cancellations and stale handles are expected outcomes, not execution failures:
|
|
233
|
+
* an incompatible continuation is rejected before any process starts.
|
|
234
|
+
*/
|
|
235
|
+
function failureOutcome(kind: ConsultingAgentFailureKind): {
|
|
236
|
+
level: "info" | "error";
|
|
237
|
+
category: string;
|
|
238
|
+
} {
|
|
239
|
+
if (kind === "cancelled") return { level: "info", category: "consultation.cancelled" };
|
|
240
|
+
if (kind === "invalid-continuation") return { level: "info", category: "consultation.rejected" };
|
|
241
|
+
return { level: "error", category: "consultation.failed" };
|
|
242
|
+
}
|
|
243
|
+
|
|
223
244
|
/** Keep only fixed reasons and safe facts, including preparation and callback failures. */
|
|
224
245
|
function safeExecutionFailure(error: unknown): ConsultingAgentError {
|
|
225
246
|
if (error instanceof ConsultingAgentError) {
|
|
@@ -7,8 +7,8 @@ import { CURATED_MODELS, type CuratedModel, isCuratedModel } from "./models.ts";
|
|
|
7
7
|
import type { AntigravityProbeResult } from "./process/runner.ts";
|
|
8
8
|
import { AntigravityProcessError, runAntigravityProbe } from "./process/runner.ts";
|
|
9
9
|
|
|
10
|
-
/** Minimum
|
|
11
|
-
export const MINIMUM_ANTIGRAVITY_VERSION = Object.freeze({ major: 1, minor:
|
|
10
|
+
/** Minimum CLI qualified for explicit inspection profiles, finish, and profile-free resumes. */
|
|
11
|
+
export const MINIMUM_ANTIGRAVITY_VERSION = Object.freeze({ major: 1, minor: 4, patch: 9 });
|
|
12
12
|
const MAX_DISCOVERY_OUTPUT_BYTES = 128 * 1024;
|
|
13
13
|
|
|
14
14
|
/** Parsed Antigravity CLI version. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// The generated name is deliberately excluded: it prevents workspace-name collisions,
|
|
6
|
+
// but is not part of the saved conversation's behavior or compatibility contract.
|
|
7
|
+
const DEFINITION = `description: Bounded read-only consultations with observed evidence.
|
|
8
|
+
mainAgent: true
|
|
9
|
+
subagent: false
|
|
10
|
+
tools:
|
|
11
|
+
- search_web
|
|
12
|
+
- read_url_content
|
|
13
|
+
- view_file
|
|
14
|
+
- list_dir
|
|
15
|
+
- find_by_name
|
|
16
|
+
- grep_search
|
|
17
|
+
- finish
|
|
18
|
+
---
|
|
19
|
+
Provide bounded, read-only consultations using the available inspection tools.
|
|
20
|
+
Do not run commands, modify files, automate browsers, or delegate work.
|
|
21
|
+
For web research, use search_web to discover sources and read_url_content to verify relevant pages before citing them. Report limitations instead of claiming unperformed reads.
|
|
22
|
+
Return workspace evidence as workspace-relative paths; omit references outside the selected workspace.
|
|
23
|
+
Complete each request with finish using the current output schema.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
/** Compatibility marker for the static profile, not an attestation from Antigravity. */
|
|
27
|
+
export const INSPECTION_AGENT_FINGERPRINT = createHash("sha256").update(DEFINITION).digest("hex");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create an exclusively owned profile for a new conversation. Antigravity retains
|
|
31
|
+
* its definition in conversation state, so callers remove it after the process ends.
|
|
32
|
+
* Keep this out of general home preparation: discovery and follow-ups need no profile.
|
|
33
|
+
*/
|
|
34
|
+
export async function createInspectionAgent(
|
|
35
|
+
homeDir: string,
|
|
36
|
+
): Promise<{ name: string; dispose: () => Promise<void> }> {
|
|
37
|
+
const parent = join(homeDir, ".gemini", "config", "agents");
|
|
38
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
39
|
+
const name = `supi-consulting-${randomUUID()}`;
|
|
40
|
+
const directory = join(parent, name);
|
|
41
|
+
await mkdir(directory, { mode: 0o700 });
|
|
42
|
+
const dispose = () => rm(directory, { recursive: true, force: true });
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(join(directory, "agent.md"), `---\nname: ${name}\n${DEFINITION}`, {
|
|
45
|
+
mode: 0o600,
|
|
46
|
+
flag: "wx",
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await dispose();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return { name, dispose };
|
|
53
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
FileResources,
|
|
28
28
|
type FileTargetObservation,
|
|
29
29
|
fileTargetUpdate,
|
|
30
|
+
isAgentInternalFileActivity,
|
|
30
31
|
isPermissionDenial,
|
|
31
32
|
terminalDeniedOperations,
|
|
32
33
|
} from "./permission-diagnostics.ts";
|
|
@@ -326,6 +327,7 @@ export class AntigravityEventAccumulator {
|
|
|
326
327
|
|
|
327
328
|
#recordSuccessfulTool(name: string, event: Record<string, unknown>, pending?: PendingTool): void {
|
|
328
329
|
if (!pending?.observed) this.#recordObservedTool(name);
|
|
330
|
+
if (isAgentInternalFileActivity(name, event, this.#readScope, pending)) return;
|
|
329
331
|
this.#successfulToolNames.add(name);
|
|
330
332
|
const input = toolInput(event) ?? event;
|
|
331
333
|
const sourceHash = pending?.urlHash ?? hashValue(extractUrl(input));
|
|
@@ -203,6 +203,21 @@ export function fileTargetUpdate(
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Antigravity caches fetched pages in its own conversation directory and reads them
|
|
208
|
+
* back with file tools. A successful file call proven outside the approved roots is
|
|
209
|
+
* agent-internal, not workspace use. Unknown or missing paths stay counted.
|
|
210
|
+
*/
|
|
211
|
+
export function isAgentInternalFileActivity(
|
|
212
|
+
name: string,
|
|
213
|
+
event: Record<string, unknown>,
|
|
214
|
+
scope: DiagnosticReadScope,
|
|
215
|
+
pending?: FileTargetObservation,
|
|
216
|
+
): boolean {
|
|
217
|
+
const target = pending?.requestedResource ? pending : fileTargetUpdate(name, event, scope);
|
|
218
|
+
return target.outsideReadRoots === true;
|
|
219
|
+
}
|
|
220
|
+
|
|
206
221
|
/**
|
|
207
222
|
* Classify the requested file location independently of the denial cause. Resolve
|
|
208
223
|
* aliases (including missing targets' existing parents); inaccessible paths and
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { createInspectionAgent } from "../inspection-agent.ts";
|
|
5
6
|
import { type IsolatedAntigravityPaths, prepareIsolatedAntigravityHome } from "../isolated-home.ts";
|
|
6
7
|
import type { AntigravityExecutionFacts, AntigravityProgressCallback } from "../types.ts";
|
|
7
8
|
import { eventType, isRecord, toolName } from "./event-values.ts";
|
|
@@ -24,6 +25,8 @@ export { AntigravityProcessError } from "./subprocess.ts";
|
|
|
24
25
|
export function buildAntigravityRunArguments(options: {
|
|
25
26
|
schemaPath: string;
|
|
26
27
|
model: string;
|
|
28
|
+
/** Only new conversations select a profile; resumes retain their saved definition. */
|
|
29
|
+
agentName?: string;
|
|
27
30
|
conversationId?: string;
|
|
28
31
|
workspaceDirectory?: string;
|
|
29
32
|
additionalReadOnlyDirectories?: readonly string[];
|
|
@@ -47,7 +50,11 @@ export function buildAntigravityRunArguments(options: {
|
|
|
47
50
|
]),
|
|
48
51
|
].flatMap((directory) => ["--add-dir", directory]),
|
|
49
52
|
"--disable-slash-commands",
|
|
50
|
-
...(options.conversationId
|
|
53
|
+
...(options.conversationId
|
|
54
|
+
? ["--conversation", options.conversationId]
|
|
55
|
+
: options.agentName
|
|
56
|
+
? ["--agent", options.agentName]
|
|
57
|
+
: []),
|
|
51
58
|
];
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -81,10 +88,8 @@ export async function runAntigravityConversation(options: {
|
|
|
81
88
|
onProcessStart?: () => void;
|
|
82
89
|
}): Promise<AntigravityExecutionFacts> {
|
|
83
90
|
await prepareIsolatedAntigravityHome(options.paths);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
const schemaPath = join(schemaDirectory, "answer.json");
|
|
91
|
+
let schemaDirectory: string | undefined;
|
|
92
|
+
let inspectionAgent: Awaited<ReturnType<typeof createInspectionAgent>> | undefined;
|
|
88
93
|
const responseToken = randomUUID();
|
|
89
94
|
const accumulator = new AntigravityEventAccumulator({
|
|
90
95
|
expectedResponseToken: responseToken,
|
|
@@ -93,6 +98,13 @@ export async function runAntigravityConversation(options: {
|
|
|
93
98
|
});
|
|
94
99
|
let eventCount = 0;
|
|
95
100
|
try {
|
|
101
|
+
if (!options.conversationId) {
|
|
102
|
+
inspectionAgent = await createInspectionAgent(options.paths.homeDir);
|
|
103
|
+
}
|
|
104
|
+
schemaDirectory = await mkdtemp(
|
|
105
|
+
join(options.schemaDirectoryParent ?? tmpdir(), "supi-consulting-schema-"),
|
|
106
|
+
);
|
|
107
|
+
const schemaPath = join(schemaDirectory, "answer.json");
|
|
96
108
|
// A resumed CLI can replay its previous structured output. Bind and locally
|
|
97
109
|
// validate this invocation rather than trusting terminal success or schema shape.
|
|
98
110
|
const schema = {
|
|
@@ -110,6 +122,7 @@ export async function runAntigravityConversation(options: {
|
|
|
110
122
|
const args = buildAntigravityRunArguments({
|
|
111
123
|
schemaPath,
|
|
112
124
|
model: options.model,
|
|
125
|
+
agentName: inspectionAgent?.name,
|
|
113
126
|
conversationId: options.conversationId,
|
|
114
127
|
workspaceDirectory: options.workspaceDirectory,
|
|
115
128
|
additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
|
|
@@ -149,7 +162,10 @@ export async function runAntigravityConversation(options: {
|
|
|
149
162
|
},
|
|
150
163
|
);
|
|
151
164
|
} finally {
|
|
152
|
-
await
|
|
165
|
+
await Promise.all([
|
|
166
|
+
inspectionAgent?.dispose(),
|
|
167
|
+
schemaDirectory ? rm(schemaDirectory, { recursive: true, force: true }) : undefined,
|
|
168
|
+
]);
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
171
|
|
package/src/agents/errors.ts
CHANGED
|
@@ -3,7 +3,8 @@ const FAILURE_MESSAGES = {
|
|
|
3
3
|
timeout: "The Consultation timed out.",
|
|
4
4
|
cancelled: "The Consultation was canceled.",
|
|
5
5
|
"invalid-response": "The Consulting Agent returned an invalid response.",
|
|
6
|
-
"invalid-continuation":
|
|
6
|
+
"invalid-continuation":
|
|
7
|
+
"The Conversation Handle has invalid or incompatible continuation state. Start a new Consultation.",
|
|
7
8
|
"conversation-mismatch":
|
|
8
9
|
"The Consulting Agent returned a different conversation for this handle.",
|
|
9
10
|
execution: "Consultation execution failed.",
|
package/src/catalogue.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { TSchema } from "typebox";
|
|
1
|
+
import { type TSchema, Type } from "typebox";
|
|
3
2
|
|
|
4
3
|
/** Build a provider-facing model enum from one immutable agent catalogue. */
|
|
5
4
|
export function buildModelCatalogueEnum(catalogue: readonly string[]): TSchema {
|
|
6
5
|
if (catalogue.length === 0) {
|
|
7
6
|
throw new Error("Cannot build a model enum from an empty Consulting Agent catalogue.");
|
|
8
7
|
}
|
|
9
|
-
return
|
|
8
|
+
return Type.Enum([...catalogue] as [string, ...string[]], {
|
|
9
|
+
type: "string",
|
|
10
10
|
description: "Model available in the current Consulting Agent catalogue.",
|
|
11
11
|
});
|
|
12
12
|
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionAPI,
|
|
5
5
|
ExtensionContext,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { ConsultingAgentError } from "../../agents/errors.ts";
|
|
7
8
|
import type { ConsultingAgentAdapter, ConsultingAgentAvailability } from "../../agents/types.ts";
|
|
8
9
|
import { resolveAdditionalReadOnlyDirectories } from "../../config.ts";
|
|
9
10
|
import {
|
|
@@ -167,9 +168,19 @@ async function runContinue(
|
|
|
167
168
|
...(hookWarning ? { hookWarning } : {}),
|
|
168
169
|
});
|
|
169
170
|
} catch (error) {
|
|
170
|
-
|
|
171
|
+
// Incompatible continuation state can never succeed, so retire it even though
|
|
172
|
+
// the adapter rejected it before any process work.
|
|
173
|
+
const incompatible =
|
|
174
|
+
error instanceof ConsultingAgentError && error.kind === "invalid-continuation";
|
|
175
|
+
if (processStarted || incompatible) {
|
|
171
176
|
const retired = dependencies.handles.retire(record.handle);
|
|
172
|
-
if (retired)
|
|
177
|
+
if (retired) {
|
|
178
|
+
appendHandleRetired(
|
|
179
|
+
dependencies.pi,
|
|
180
|
+
retired,
|
|
181
|
+
incompatible ? "Incompatible continuation state" : "Consultation follow-up failed",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
173
184
|
}
|
|
174
185
|
throw error;
|
|
175
186
|
} finally {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
1
|
import { type TSchema, Type } from "typebox";
|
|
3
2
|
import { Value } from "typebox/value";
|
|
4
3
|
import { buildModelCatalogueEnum, isAvailableModel } from "../../catalogue.ts";
|
|
@@ -40,7 +39,8 @@ export function buildConsultingRunSchema(agent: string, catalogue: readonly stri
|
|
|
40
39
|
new: Type.Optional(
|
|
41
40
|
Type.Object(
|
|
42
41
|
{
|
|
43
|
-
agent:
|
|
42
|
+
agent: Type.Enum([agent], {
|
|
43
|
+
type: "string",
|
|
44
44
|
description: "The Consulting Agent that will perform this Consultation.",
|
|
45
45
|
}),
|
|
46
46
|
model: buildModelCatalogueEnum(catalogue),
|