@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.
- package/CLAUDE.md +23 -0
- package/CONTEXT.md +53 -0
- package/README.md +104 -2
- package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +137 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +77 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +42 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +342 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +17 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +313 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +38 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +242 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +47 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +454 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tui.ts +141 -0
- package/package.json +78 -4
- package/scripts/live-probe.ts +139 -0
- package/src/agents/antigravity/activity.ts +22 -0
- package/src/agents/antigravity/adapter.ts +196 -0
- package/src/agents/antigravity/availability.ts +231 -0
- package/src/agents/antigravity/isolated-home.ts +346 -0
- package/src/agents/antigravity/models.ts +15 -0
- package/src/agents/antigravity/process/environment.ts +33 -0
- package/src/agents/antigravity/process/event-values.ts +273 -0
- package/src/agents/antigravity/process/events.ts +366 -0
- package/src/agents/antigravity/process/hooks.ts +219 -0
- package/src/agents/antigravity/process/ndjson.ts +120 -0
- package/src/agents/antigravity/process/protocol.ts +69 -0
- package/src/agents/antigravity/process/runner.ts +147 -0
- package/src/agents/antigravity/process/subprocess.ts +278 -0
- package/src/agents/antigravity/process/usage.ts +50 -0
- package/src/agents/antigravity/structured-output.ts +58 -0
- package/src/agents/antigravity/types.ts +17 -0
- package/src/agents/errors.ts +27 -0
- package/src/agents/types.ts +43 -0
- package/src/catalogue.ts +17 -0
- package/src/config.ts +26 -0
- package/src/conversation/handles.ts +187 -0
- package/src/evidence-hash.ts +6 -0
- package/src/extension.ts +26 -0
- package/src/footer-constants.ts +5 -0
- package/src/footer.ts +25 -0
- package/src/runtime.ts +244 -0
- package/src/settings.ts +38 -0
- package/src/tool/consulting_run/evidence.ts +169 -0
- package/src/tool/consulting_run/execute.ts +201 -0
- package/src/tool/consulting_run/guidance.ts +3 -0
- package/src/tool/consulting_run/input.ts +117 -0
- package/src/tool/consulting_run/register.ts +36 -0
- package/src/tool/consulting_run/render.ts +261 -0
- package/src/tool/consulting_run/result.ts +185 -0
- package/src/tool/consulting_run/spec.ts +20 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, normalize as normalizePath, relative, resolve } from "node:path";
|
|
3
|
+
import { hashEvidence } from "../../evidence-hash.ts";
|
|
4
|
+
import type {
|
|
5
|
+
ClassifiedSource,
|
|
6
|
+
ClassifiedWorkspaceEvidence,
|
|
7
|
+
ConsultationAnswer,
|
|
8
|
+
ConsultationExecutionFacts,
|
|
9
|
+
} from "../../types.ts";
|
|
10
|
+
|
|
11
|
+
/** Evidence split and safe warnings produced from one Consultation answer. */
|
|
12
|
+
export interface ClassifiedEvidence {
|
|
13
|
+
observedSources: ClassifiedSource[];
|
|
14
|
+
claimedSources: ClassifiedSource[];
|
|
15
|
+
observedWorkspaceEvidence: ClassifiedWorkspaceEvidence[];
|
|
16
|
+
claimedWorkspaceEvidence: ClassifiedWorkspaceEvidence[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Validate references, classify observed facts, and omit unsafe references. */
|
|
21
|
+
export function classifyAnswerEvidence(
|
|
22
|
+
answer: ConsultationAnswer,
|
|
23
|
+
facts: Pick<ConsultationExecutionFacts, "observedSourceHashes" | "observedWorkspacePathHashes">,
|
|
24
|
+
workspaceDirectory: string,
|
|
25
|
+
): ClassifiedEvidence {
|
|
26
|
+
const sources = classifySources(answer.sources, new Set(facts.observedSourceHashes));
|
|
27
|
+
const workspace = classifyWorkspaceEvidence(
|
|
28
|
+
answer.workspaceEvidence,
|
|
29
|
+
new Set(facts.observedWorkspacePathHashes),
|
|
30
|
+
workspaceDirectory,
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
observedSources: sources.observed,
|
|
34
|
+
claimedSources: sources.claimed,
|
|
35
|
+
observedWorkspaceEvidence: workspace.observed,
|
|
36
|
+
claimedWorkspaceEvidence: workspace.claimed,
|
|
37
|
+
warnings: [...new Set([...sources.warnings, ...workspace.warnings])],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ClassifiedSourceLists {
|
|
42
|
+
observed: ClassifiedSource[];
|
|
43
|
+
claimed: ClassifiedSource[];
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function classifySources(
|
|
48
|
+
sources: readonly ConsultationAnswer["sources"][number][],
|
|
49
|
+
observedHashes: ReadonlySet<string>,
|
|
50
|
+
): ClassifiedSourceLists {
|
|
51
|
+
const result: ClassifiedSourceLists = { observed: [], claimed: [], warnings: [] };
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
const url = source.url.trim();
|
|
54
|
+
if (!isHttpUrl(url) || !source.title.trim()) {
|
|
55
|
+
result.warnings.push("Some source references were omitted because they were invalid.");
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const classified: ClassifiedSource = {
|
|
59
|
+
title: source.title.trim(),
|
|
60
|
+
url,
|
|
61
|
+
evidence: observedHashes.has(hashEvidence(url)) ? "observed" : "claimed",
|
|
62
|
+
};
|
|
63
|
+
addClassified(result, classified);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function classifyWorkspaceEvidence(
|
|
69
|
+
evidence: readonly ConsultationAnswer["workspaceEvidence"][number][],
|
|
70
|
+
observedHashes: ReadonlySet<string>,
|
|
71
|
+
workspaceDirectory: string,
|
|
72
|
+
): {
|
|
73
|
+
observed: ClassifiedWorkspaceEvidence[];
|
|
74
|
+
claimed: ClassifiedWorkspaceEvidence[];
|
|
75
|
+
warnings: string[];
|
|
76
|
+
} {
|
|
77
|
+
const result = { observed: [], claimed: [], warnings: [] } as {
|
|
78
|
+
observed: ClassifiedWorkspaceEvidence[];
|
|
79
|
+
claimed: ClassifiedWorkspaceEvidence[];
|
|
80
|
+
warnings: string[];
|
|
81
|
+
};
|
|
82
|
+
for (const item of evidence) {
|
|
83
|
+
if (!isSafeWorkspacePath(item.path, workspaceDirectory) || !item.summary.trim()) {
|
|
84
|
+
result.warnings.push(
|
|
85
|
+
"Some workspace references were omitted because they were outside the selected workspace.",
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const normalizedPath = normalizeWorkspacePath(item.path);
|
|
90
|
+
const classified: ClassifiedWorkspaceEvidence = {
|
|
91
|
+
path: normalizedPath,
|
|
92
|
+
summary: item.summary.trim(),
|
|
93
|
+
evidence: observedHashes.has(hashEvidence(normalizedPath)) ? "observed" : "claimed",
|
|
94
|
+
};
|
|
95
|
+
if (classified.evidence === "observed") result.observed.push(classified);
|
|
96
|
+
else result.claimed.push(classified);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addClassified(result: ClassifiedSourceLists, source: ClassifiedSource): void {
|
|
102
|
+
if (source.evidence === "observed") result.observed.push(source);
|
|
103
|
+
else result.claimed.push(source);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Accept only HTTP and HTTPS source URLs. */
|
|
107
|
+
export function isHttpUrl(value: string): boolean {
|
|
108
|
+
if (hasControlCharacters(value)) return false;
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(value);
|
|
111
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Check that a returned path is relative and contained by a workspace. */
|
|
118
|
+
export function isSafeWorkspacePath(value: string, workspaceDirectory: string): boolean {
|
|
119
|
+
if (
|
|
120
|
+
!value ||
|
|
121
|
+
hasControlCharacters(value) ||
|
|
122
|
+
value.startsWith("~") ||
|
|
123
|
+
isAbsolute(value) ||
|
|
124
|
+
/^[A-Za-z]:[\\/]/.test(value)
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const candidate = normalizeWorkspacePath(value);
|
|
129
|
+
if (candidate === "." || candidate === ".." || candidate.startsWith("../")) return false;
|
|
130
|
+
const resolvedWorkspace = safeRealpath(workspaceDirectory);
|
|
131
|
+
const resolvedCandidate = resolve(resolvedWorkspace, candidate);
|
|
132
|
+
if (!isContainedPath(resolvedWorkspace, resolvedCandidate)) return false;
|
|
133
|
+
const existingAncestor = findExistingAncestor(resolvedCandidate);
|
|
134
|
+
return isContainedPath(resolvedWorkspace, safeRealpath(existingAncestor));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasControlCharacters(value: string): boolean {
|
|
138
|
+
return [...value].some((character) => {
|
|
139
|
+
const code = character.charCodeAt(0);
|
|
140
|
+
return code < 32 || code === 127;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeWorkspacePath(value: string): string {
|
|
145
|
+
return normalizePath(value.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function safeRealpath(value: string): string {
|
|
149
|
+
try {
|
|
150
|
+
return realpathSync(value);
|
|
151
|
+
} catch {
|
|
152
|
+
return resolve(value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findExistingAncestor(value: string): string {
|
|
157
|
+
let candidate = value;
|
|
158
|
+
while (!existsSync(candidate)) {
|
|
159
|
+
const parent = dirname(candidate);
|
|
160
|
+
if (parent === candidate) return value;
|
|
161
|
+
candidate = parent;
|
|
162
|
+
}
|
|
163
|
+
return candidate;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isContainedPath(workspace: string, candidate: string): boolean {
|
|
167
|
+
const distance = relative(workspace, candidate);
|
|
168
|
+
return distance !== ".." && !distance.startsWith("../") && !isAbsolute(distance);
|
|
169
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolUpdateCallback,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ConsultingAgentAdapter, ConsultingAgentAvailability } from "../../agents/types.ts";
|
|
8
|
+
import {
|
|
9
|
+
appendHandleCreated,
|
|
10
|
+
appendHandleRetired,
|
|
11
|
+
type ConversationHandleStore,
|
|
12
|
+
} from "../../conversation/handles.ts";
|
|
13
|
+
import { type ConsultingRunInput, parseConsultingRunInput } from "./input.ts";
|
|
14
|
+
import { buildConsultationResult } from "./result.ts";
|
|
15
|
+
|
|
16
|
+
/** Dependencies captured by one dynamically registered consulting_run tool. */
|
|
17
|
+
export interface ConsultingRunDependencies {
|
|
18
|
+
pi: ExtensionAPI;
|
|
19
|
+
adapter: ConsultingAgentAdapter;
|
|
20
|
+
availability: ConsultingAgentAvailability;
|
|
21
|
+
handles: ConversationHandleStore;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ConsultingExecute = NonNullable<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>;
|
|
25
|
+
|
|
26
|
+
/** Build the execute function for one immutable agent availability snapshot. */
|
|
27
|
+
export function makeConsultingRunExecute(
|
|
28
|
+
dependencies: ConsultingRunDependencies,
|
|
29
|
+
): ConsultingExecute {
|
|
30
|
+
// biome-ignore lint/complexity/useMaxParams: Pi execute contract requires five parameters.
|
|
31
|
+
return async (_toolCallId, params, signal, onUpdate, ctx) => {
|
|
32
|
+
const input = parseConsultingRunInput(
|
|
33
|
+
params,
|
|
34
|
+
dependencies.availability.agent,
|
|
35
|
+
dependencies.availability.catalogue,
|
|
36
|
+
);
|
|
37
|
+
const startedAt = Date.now();
|
|
38
|
+
if (input.new) {
|
|
39
|
+
return runNew({ input, dependencies, signal, onUpdate, ctx, startedAt });
|
|
40
|
+
}
|
|
41
|
+
if (!input.continue) throw new Error("consulting_run requires exactly one of new or continue.");
|
|
42
|
+
return runContinue({
|
|
43
|
+
continuation: input.continue,
|
|
44
|
+
prompt: input.prompt,
|
|
45
|
+
dependencies,
|
|
46
|
+
signal,
|
|
47
|
+
onUpdate,
|
|
48
|
+
ctx,
|
|
49
|
+
startedAt,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CommonRunOptions {
|
|
55
|
+
dependencies: ConsultingRunDependencies;
|
|
56
|
+
signal: AbortSignal | undefined;
|
|
57
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined;
|
|
58
|
+
ctx: ExtensionContext;
|
|
59
|
+
startedAt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface NewRunOptions extends CommonRunOptions {
|
|
63
|
+
input: Extract<ConsultingRunInput, { new: object }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runNew(options: NewRunOptions): Promise<ReturnType<typeof buildConsultationResult>> {
|
|
67
|
+
const { input, dependencies, signal, onUpdate, ctx, startedAt } = options;
|
|
68
|
+
const workspaceAccess = input.new.workspace;
|
|
69
|
+
const cwd = await resolveWorkingDirectory(
|
|
70
|
+
workspaceAccess,
|
|
71
|
+
ctx.cwd,
|
|
72
|
+
dependencies.adapter.consultationWorkspace,
|
|
73
|
+
);
|
|
74
|
+
const progress = createProgressReporter(
|
|
75
|
+
onUpdate,
|
|
76
|
+
dependencies.adapter.identity,
|
|
77
|
+
input.new.model,
|
|
78
|
+
workspaceAccess,
|
|
79
|
+
);
|
|
80
|
+
progress("starting Consultation");
|
|
81
|
+
let hookWarning: string | undefined;
|
|
82
|
+
const facts = await dependencies.adapter.execute({
|
|
83
|
+
prompt: input.prompt,
|
|
84
|
+
model: input.new.model,
|
|
85
|
+
canonicalWorkingDirectory: cwd,
|
|
86
|
+
workspaceAccess,
|
|
87
|
+
signal,
|
|
88
|
+
onProgress: progress,
|
|
89
|
+
onWarning: (warning) => {
|
|
90
|
+
hookWarning = warning;
|
|
91
|
+
notifyWarning(ctx, warning);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const record = dependencies.handles.create({
|
|
95
|
+
agent: dependencies.adapter.identity,
|
|
96
|
+
model: input.new.model,
|
|
97
|
+
continuation: facts.continuation,
|
|
98
|
+
canonicalWorkingDirectory: cwd,
|
|
99
|
+
workspaceAccess,
|
|
100
|
+
agentVersion: dependencies.availability.agentVersion,
|
|
101
|
+
});
|
|
102
|
+
appendHandleCreated(dependencies.pi, record);
|
|
103
|
+
return buildConsultationResult({
|
|
104
|
+
facts,
|
|
105
|
+
handle: record,
|
|
106
|
+
durationMs: Date.now() - startedAt,
|
|
107
|
+
...(hookWarning ? { hookWarning } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface ContinueRunOptions extends CommonRunOptions {
|
|
112
|
+
continuation: Extract<ConsultingRunInput, { continue: object }>["continue"];
|
|
113
|
+
prompt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runContinue(
|
|
117
|
+
options: ContinueRunOptions,
|
|
118
|
+
): Promise<ReturnType<typeof buildConsultationResult>> {
|
|
119
|
+
const { continuation, prompt, dependencies, signal, onUpdate, ctx, startedAt } = options;
|
|
120
|
+
const record = dependencies.handles.acquire(continuation.handle, dependencies.adapter.identity);
|
|
121
|
+
let processStarted = false;
|
|
122
|
+
try {
|
|
123
|
+
if (!dependencies.availability.catalogue.includes(record.model)) {
|
|
124
|
+
throw new Error("The Conversation Handle model is not available in the current catalogue.");
|
|
125
|
+
}
|
|
126
|
+
const progress = createProgressReporter(
|
|
127
|
+
onUpdate,
|
|
128
|
+
record.agent,
|
|
129
|
+
record.model,
|
|
130
|
+
record.workspaceAccess,
|
|
131
|
+
);
|
|
132
|
+
progress("starting Consultation follow-up");
|
|
133
|
+
let hookWarning: string | undefined;
|
|
134
|
+
const facts = await dependencies.adapter.execute({
|
|
135
|
+
prompt,
|
|
136
|
+
model: record.model,
|
|
137
|
+
canonicalWorkingDirectory: record.canonicalWorkingDirectory,
|
|
138
|
+
workspaceAccess: record.workspaceAccess,
|
|
139
|
+
continuation: record.continuation,
|
|
140
|
+
signal,
|
|
141
|
+
onProgress: progress,
|
|
142
|
+
onWarning: (warning) => {
|
|
143
|
+
hookWarning = warning;
|
|
144
|
+
notifyWarning(ctx, warning);
|
|
145
|
+
},
|
|
146
|
+
onProcessStart: () => {
|
|
147
|
+
processStarted = true;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return buildConsultationResult({
|
|
151
|
+
facts,
|
|
152
|
+
handle: record,
|
|
153
|
+
durationMs: Date.now() - startedAt,
|
|
154
|
+
...(hookWarning ? { hookWarning } : {}),
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (processStarted) {
|
|
158
|
+
const retired = dependencies.handles.retire(record.handle);
|
|
159
|
+
if (retired) appendHandleRetired(dependencies.pi, retired, "Consultation follow-up failed");
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
} finally {
|
|
163
|
+
dependencies.handles.release(record.handle);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function resolveWorkingDirectory(
|
|
168
|
+
workspaceAccess: boolean,
|
|
169
|
+
currentDirectory: string,
|
|
170
|
+
consultationWorkspace: string,
|
|
171
|
+
): Promise<string> {
|
|
172
|
+
return realpath(workspaceAccess ? currentDirectory : consultationWorkspace);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createProgressReporter(
|
|
176
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
177
|
+
agent: string,
|
|
178
|
+
model: string,
|
|
179
|
+
workspaceAccess: boolean,
|
|
180
|
+
): (activity: string) => void {
|
|
181
|
+
return (activity: string) => {
|
|
182
|
+
onUpdate?.({
|
|
183
|
+
content: [{ type: "text", text: "Consulting is working…" }],
|
|
184
|
+
details: {
|
|
185
|
+
agent,
|
|
186
|
+
model,
|
|
187
|
+
workingDirectoryKind: workspaceAccess ? "workspace" : "consultation",
|
|
188
|
+
workspaceAccess,
|
|
189
|
+
latestActivity: activity,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function notifyWarning(ctx: ExtensionContext, warning: string): void {
|
|
196
|
+
try {
|
|
197
|
+
ctx.ui.notify(warning, "warning");
|
|
198
|
+
} catch {
|
|
199
|
+
// A closed UI must not change Consultation execution or handle state.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Selection guidance for the dynamically activated consulting_run tool. */
|
|
2
|
+
export const toolDescription =
|
|
3
|
+
"Consult a supported external agent for focused web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts that do not need external analysis. Expose the current workspace only when the Consultation needs repository evidence.";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type TSchema, Type } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import { buildModelCatalogueEnum, isAvailableModel } from "../../catalogue.ts";
|
|
5
|
+
import type { ConsultingModel } from "../../types.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_PROMPT_CHARS = 32_000;
|
|
8
|
+
const MAX_HANDLE_CHARS = 128;
|
|
9
|
+
|
|
10
|
+
/** Input for a new Consultation. */
|
|
11
|
+
export interface NewConsultationInput {
|
|
12
|
+
agent: string;
|
|
13
|
+
model: ConsultingModel;
|
|
14
|
+
workspace: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Input for a Conversation Handle follow-up. */
|
|
18
|
+
export interface ContinueConsultationInput {
|
|
19
|
+
handle: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Exact-one consulting_run input contract. */
|
|
23
|
+
export type ConsultingRunInput =
|
|
24
|
+
| { prompt: string; new: NewConsultationInput; continue?: never }
|
|
25
|
+
| { prompt: string; new?: never; continue: ContinueConsultationInput };
|
|
26
|
+
|
|
27
|
+
/** Build the dynamic tool schema from one immutable agent and model snapshot. */
|
|
28
|
+
export function buildConsultingRunSchema(agent: string, catalogue: readonly string[]): TSchema {
|
|
29
|
+
if (!agent || catalogue.length === 0) {
|
|
30
|
+
throw new Error("Cannot register consulting_run without an available agent and model.");
|
|
31
|
+
}
|
|
32
|
+
const prompt = Type.String({
|
|
33
|
+
minLength: 1,
|
|
34
|
+
maxLength: MAX_PROMPT_CHARS,
|
|
35
|
+
description: "The bounded request for one Consultation.",
|
|
36
|
+
});
|
|
37
|
+
return Type.Object(
|
|
38
|
+
{
|
|
39
|
+
prompt,
|
|
40
|
+
new: Type.Optional(
|
|
41
|
+
Type.Object(
|
|
42
|
+
{
|
|
43
|
+
agent: StringEnum([agent], {
|
|
44
|
+
description: "The Consulting Agent that will perform this Consultation.",
|
|
45
|
+
}),
|
|
46
|
+
model: buildModelCatalogueEnum(catalogue),
|
|
47
|
+
workspace: Type.Boolean({
|
|
48
|
+
description:
|
|
49
|
+
"true exposes the current PI workspace; false uses the empty Consultation Workspace.",
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{ additionalProperties: false },
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
continue: Type.Optional(
|
|
56
|
+
Type.Object(
|
|
57
|
+
{
|
|
58
|
+
handle: Type.String({
|
|
59
|
+
minLength: 1,
|
|
60
|
+
maxLength: MAX_HANDLE_CHARS,
|
|
61
|
+
description: "Conversation Handle returned by consulting_run.",
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
description: "Continue the existing Consultation with its original selection.",
|
|
67
|
+
},
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
minProperties: 2,
|
|
73
|
+
maxProperties: 2,
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Validate exact-one input against the immutable availability snapshot. */
|
|
80
|
+
export function parseConsultingRunInput(
|
|
81
|
+
value: unknown,
|
|
82
|
+
agent: string,
|
|
83
|
+
catalogue: readonly string[],
|
|
84
|
+
): ConsultingRunInput {
|
|
85
|
+
if (!isRecord(value)) throw new Error("Invalid consulting_run input.");
|
|
86
|
+
const hasNew = Object.hasOwn(value, "new");
|
|
87
|
+
const hasContinue = Object.hasOwn(value, "continue");
|
|
88
|
+
if (hasNew === hasContinue) {
|
|
89
|
+
throw new Error("consulting_run requires exactly one of new or continue.");
|
|
90
|
+
}
|
|
91
|
+
const schema = buildConsultingRunSchema(agent, catalogue);
|
|
92
|
+
if (!Value.Check(schema, value)) throw new Error("Invalid consulting_run input.");
|
|
93
|
+
const newInput = isRecord(value.new) ? value.new : undefined;
|
|
94
|
+
const continueInput = isRecord(value.continue) ? value.continue : undefined;
|
|
95
|
+
if (newInput) {
|
|
96
|
+
if (newInput.agent !== agent) throw new Error("The selected Consulting Agent is unavailable.");
|
|
97
|
+
if (typeof newInput.workspace !== "boolean" || !isAvailableModel(newInput.model, catalogue)) {
|
|
98
|
+
throw new Error("The selected model is not available in the Consulting Agent catalogue.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
prompt: value.prompt as string,
|
|
102
|
+
new: {
|
|
103
|
+
agent: newInput.agent,
|
|
104
|
+
model: newInput.model,
|
|
105
|
+
workspace: newInput.workspace,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!continueInput || typeof continueInput.handle !== "string") {
|
|
110
|
+
throw new Error("Invalid Conversation Handle.");
|
|
111
|
+
}
|
|
112
|
+
return { prompt: value.prompt as string, continue: { handle: continueInput.handle } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ConsultingAgentAdapter, ConsultingAgentAvailability } from "../../agents/types.ts";
|
|
3
|
+
import type { ConversationHandleStore } from "../../conversation/handles.ts";
|
|
4
|
+
import { makeConsultingRunExecute } from "./execute.ts";
|
|
5
|
+
import { toolDescription } from "./guidance.ts";
|
|
6
|
+
import { renderConsultingCall, renderConsultingResult } from "./render.ts";
|
|
7
|
+
import { buildConsultingRunParameters, consultingRunSpec } from "./spec.ts";
|
|
8
|
+
|
|
9
|
+
/** Dependencies needed by the dynamically registered tool. */
|
|
10
|
+
export interface RegisterConsultingRunOptions {
|
|
11
|
+
pi: ExtensionAPI;
|
|
12
|
+
adapter: ConsultingAgentAdapter;
|
|
13
|
+
availability: ConsultingAgentAvailability;
|
|
14
|
+
handles: ConversationHandleStore;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Register consulting_run from one immutable availability snapshot. */
|
|
18
|
+
export function registerConsultingRunTool(options: RegisterConsultingRunOptions): void {
|
|
19
|
+
const parameters = buildConsultingRunParameters(
|
|
20
|
+
options.availability.agent,
|
|
21
|
+
options.availability.catalogue,
|
|
22
|
+
);
|
|
23
|
+
options.pi.registerTool({
|
|
24
|
+
...consultingRunSpec,
|
|
25
|
+
description: toolDescription,
|
|
26
|
+
parameters,
|
|
27
|
+
renderCall: renderConsultingCall,
|
|
28
|
+
renderResult: renderConsultingResult,
|
|
29
|
+
execute: makeConsultingRunExecute({
|
|
30
|
+
pi: options.pi,
|
|
31
|
+
adapter: options.adapter,
|
|
32
|
+
availability: options.availability,
|
|
33
|
+
handles: options.handles,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
}
|