@jameslovespancakes/pi-plus 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSessionFromServices,
|
|
4
|
+
createAgentSessionServices,
|
|
5
|
+
defineTool,
|
|
6
|
+
SessionManager,
|
|
7
|
+
type CreateAgentSessionOptions,
|
|
8
|
+
type ModelRegistry,
|
|
9
|
+
type Skill,
|
|
10
|
+
type ToolDefinition,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { prepareAgentSkillResources } from "./agent-skills.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AgentExecutionOptions,
|
|
15
|
+
AgentRunTags,
|
|
16
|
+
AgentRunnerSession,
|
|
17
|
+
RunContext,
|
|
18
|
+
} from "./agent-runner-types.ts";
|
|
19
|
+
import { raceWithAbort, throwIfAborted } from "./cancellation.ts";
|
|
20
|
+
import {
|
|
21
|
+
MAX_SCHEMA_REPAIR_ATTEMPTS,
|
|
22
|
+
WorkflowStructuredOutputError,
|
|
23
|
+
} from "./structured-output.ts";
|
|
24
|
+
import { providerErrorFromMessages } from "./agent-retry.ts";
|
|
25
|
+
import { synchronizeWorkflowModelRuntime } from "./agent-session-providers.ts";
|
|
26
|
+
import { matchesAgentToolHint, WorkflowToolHintUnavailableError } from "./tool-capabilities.ts";
|
|
27
|
+
import type { AgentToolHint } from "./types.ts";
|
|
28
|
+
|
|
29
|
+
export const FINAL_TOOL = "final_answer";
|
|
30
|
+
|
|
31
|
+
const SCHEMA_REPROMPT =
|
|
32
|
+
`You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
|
|
33
|
+
`Call ${FINAL_TOOL} now with your final answer as its arguments. Do not reply with plain text.`;
|
|
34
|
+
const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
35
|
+
|
|
36
|
+
export interface ResolvedAgentModelRequest {
|
|
37
|
+
readonly ref: string;
|
|
38
|
+
readonly provider: string;
|
|
39
|
+
readonly id: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResolvedAgentModel {
|
|
43
|
+
readonly model: Model<Api> | undefined;
|
|
44
|
+
readonly requested: ResolvedAgentModelRequest | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentSessionHandle {
|
|
48
|
+
readonly session: AgentRunnerSession;
|
|
49
|
+
readonly selectedSkills: readonly Skill[];
|
|
50
|
+
hasStructuredResult(): boolean;
|
|
51
|
+
structuredResult(): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveAgentModel(
|
|
55
|
+
modelRef: string | undefined,
|
|
56
|
+
modelRegistry: Pick<ModelRegistry, "find">,
|
|
57
|
+
hostModel: Model<Api> | undefined,
|
|
58
|
+
): ResolvedAgentModel {
|
|
59
|
+
if (modelRef === undefined) return { model: hostModel, requested: undefined };
|
|
60
|
+
|
|
61
|
+
const parsed = parseAgentModelRef(modelRef);
|
|
62
|
+
const found = modelRegistry.find(parsed.provider, parsed.id);
|
|
63
|
+
if (!found) {
|
|
64
|
+
throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
|
|
65
|
+
}
|
|
66
|
+
return { model: found, requested: parsed };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function openAgentSession(input: {
|
|
70
|
+
readonly rc: RunContext;
|
|
71
|
+
readonly prompt: string;
|
|
72
|
+
readonly opts: AgentExecutionOptions;
|
|
73
|
+
readonly cwd: string;
|
|
74
|
+
readonly model: Model<Api> | undefined;
|
|
75
|
+
readonly label: string;
|
|
76
|
+
readonly tags: AgentRunTags;
|
|
77
|
+
}): Promise<AgentSessionHandle> {
|
|
78
|
+
const { rc, prompt, opts, cwd, model, label, tags } = input;
|
|
79
|
+
let captured = false;
|
|
80
|
+
let structuredResult: unknown;
|
|
81
|
+
const customTools: ToolDefinition[] = opts.schema
|
|
82
|
+
? [
|
|
83
|
+
defineTool({
|
|
84
|
+
name: FINAL_TOOL,
|
|
85
|
+
label: "Final Answer",
|
|
86
|
+
description:
|
|
87
|
+
"Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
|
|
88
|
+
parameters: opts.schema,
|
|
89
|
+
async execute(_toolCallId, params) {
|
|
90
|
+
captured = true;
|
|
91
|
+
structuredResult = params;
|
|
92
|
+
return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
]
|
|
96
|
+
: [];
|
|
97
|
+
let session: AgentRunnerSession | undefined;
|
|
98
|
+
try {
|
|
99
|
+
throwIfAborted(rc.signal);
|
|
100
|
+
const resourceInput = { rc, prompt, opts, cwd, model, customTools, label };
|
|
101
|
+
const resources = rc.createSession
|
|
102
|
+
? await prepareAgentSessionResources(resourceInput)
|
|
103
|
+
: await rc.perf.time(
|
|
104
|
+
"agent.session_resources_ms",
|
|
105
|
+
() => prepareAgentSessionResources(resourceInput),
|
|
106
|
+
tags,
|
|
107
|
+
);
|
|
108
|
+
const toolSelection = buildToolSelection(opts, resources.selectedSkills.length > 0);
|
|
109
|
+
session = (
|
|
110
|
+
await rc.perf.time(
|
|
111
|
+
"agent.create_session_ms",
|
|
112
|
+
() => resources.createSession(toolSelection.sessionOptions),
|
|
113
|
+
tags,
|
|
114
|
+
)
|
|
115
|
+
).session;
|
|
116
|
+
session.setAutoRetryEnabled(false);
|
|
117
|
+
const matchedToolHints = toolSelection.toolHints.length === 0
|
|
118
|
+
? new Set<AgentToolHint>()
|
|
119
|
+
: applyDynamicToolHints(session, toolSelection);
|
|
120
|
+
if (opts.requireToolHints) {
|
|
121
|
+
const missing = toolSelection.toolHints.filter((hint) => !matchedToolHints.has(hint));
|
|
122
|
+
if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing);
|
|
123
|
+
}
|
|
124
|
+
throwIfAborted(rc.signal);
|
|
125
|
+
return {
|
|
126
|
+
session,
|
|
127
|
+
selectedSkills: resources.selectedSkills,
|
|
128
|
+
hasStructuredResult: () => captured,
|
|
129
|
+
structuredResult: () => structuredResult,
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (session) {
|
|
133
|
+
const activeSession = session;
|
|
134
|
+
session = undefined;
|
|
135
|
+
rc.perf.timeSync("agent.dispose_ms", () => activeSession.dispose(), tags);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function promptAgentSession(input: {
|
|
142
|
+
readonly rc: RunContext;
|
|
143
|
+
readonly handle: AgentSessionHandle;
|
|
144
|
+
readonly prompt: string;
|
|
145
|
+
readonly opts: AgentExecutionOptions;
|
|
146
|
+
readonly label: string;
|
|
147
|
+
readonly rowId: number;
|
|
148
|
+
readonly tags: AgentRunTags;
|
|
149
|
+
}): Promise<unknown> {
|
|
150
|
+
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
151
|
+
const { session } = handle;
|
|
152
|
+
const unbindFollowUp = rc.progress.bindAgentFollowUp(rowId, async (message) => {
|
|
153
|
+
if (!session.isStreaming) throw new Error("This agent has already finished its current turn.");
|
|
154
|
+
await session.followUp(message);
|
|
155
|
+
});
|
|
156
|
+
const unsubscribe = session.subscribe((event) => {
|
|
157
|
+
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
158
|
+
rc.progress.agentTool(label, event.toolName, rowId);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
162
|
+
const text = event.message.content
|
|
163
|
+
.filter((part) => part.type === "text")
|
|
164
|
+
.map((part) => part.text)
|
|
165
|
+
.join("\n")
|
|
166
|
+
.trim();
|
|
167
|
+
if (text) rc.progress.agentMessage(rowId, "assistant", text);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const finalPrompt = opts.schema
|
|
173
|
+
? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
174
|
+
: prompt;
|
|
175
|
+
throwIfAborted(rc.signal);
|
|
176
|
+
const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
|
|
177
|
+
try {
|
|
178
|
+
try {
|
|
179
|
+
const promptSession = async (text: string) => {
|
|
180
|
+
await rc.perf.time("agent.prompt_ms", () => raceWithAbort(() => session.prompt(text), rc.signal), tags);
|
|
181
|
+
const failure = providerErrorFromMessages(session.messages, {
|
|
182
|
+
pauseOnUsageLimit: rc.pauseOnProviderUsageLimit,
|
|
183
|
+
});
|
|
184
|
+
if (failure) throw failure;
|
|
185
|
+
};
|
|
186
|
+
await promptSession(finalPrompt);
|
|
187
|
+
if (opts.schema) {
|
|
188
|
+
for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
|
|
189
|
+
throwIfAborted(rc.signal);
|
|
190
|
+
session.setActiveToolsByName([FINAL_TOOL]);
|
|
191
|
+
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
|
|
192
|
+
rc.perf.counter("agent.structured_reprompt", 1, tags);
|
|
193
|
+
await promptSession(SCHEMA_REPROMPT);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throwIfAborted(rc.signal);
|
|
198
|
+
return rc.perf.timeSync(
|
|
199
|
+
"agent.extract_result_ms",
|
|
200
|
+
() => {
|
|
201
|
+
if (!opts.schema) {
|
|
202
|
+
return session.getLastAssistantText() ?? "";
|
|
203
|
+
}
|
|
204
|
+
if (!handle.hasStructuredResult()) {
|
|
205
|
+
rc.progress.log(`${label}: no structured answer returned`);
|
|
206
|
+
rc.perf.counter("agent.structured_missing", 1, tags);
|
|
207
|
+
throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
|
|
208
|
+
}
|
|
209
|
+
return handle.structuredResult();
|
|
210
|
+
},
|
|
211
|
+
tags,
|
|
212
|
+
);
|
|
213
|
+
} finally {
|
|
214
|
+
rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.messages });
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
unlinkPromptAbort();
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
unbindFollowUp();
|
|
221
|
+
unsubscribe();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
226
|
+
const normalized = modelRef.trim();
|
|
227
|
+
if (normalized.length === 0) {
|
|
228
|
+
throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
|
|
229
|
+
}
|
|
230
|
+
if (normalized !== modelRef) {
|
|
231
|
+
throw new Error(`Invalid agent model ref "${modelRef}": remove leading or trailing whitespace.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const slash = modelRef.indexOf("/");
|
|
235
|
+
if (slash === -1) return { ref: modelRef, provider: "anthropic", id: modelRef };
|
|
236
|
+
const provider = modelRef.slice(0, slash);
|
|
237
|
+
const id = modelRef.slice(slash + 1);
|
|
238
|
+
if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
|
|
239
|
+
throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
|
|
240
|
+
}
|
|
241
|
+
return { ref: modelRef, provider, id };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface AgentSessionResources {
|
|
245
|
+
readonly selectedSkills: readonly Skill[];
|
|
246
|
+
createSession(sessionOptions: ToolSessionOptions): Promise<{ session: AgentRunnerSession }>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function prepareAgentSessionResources(input: {
|
|
250
|
+
readonly rc: RunContext;
|
|
251
|
+
readonly prompt: string;
|
|
252
|
+
readonly opts: AgentExecutionOptions;
|
|
253
|
+
readonly cwd: string;
|
|
254
|
+
readonly model: Model<Api> | undefined;
|
|
255
|
+
readonly customTools: ToolDefinition[];
|
|
256
|
+
readonly label: string;
|
|
257
|
+
}): Promise<AgentSessionResources> {
|
|
258
|
+
const { rc, prompt, opts, cwd, model, customTools, label } = input;
|
|
259
|
+
const commonSessionOptions = (sessionOptions: ToolSessionOptions) => ({
|
|
260
|
+
model,
|
|
261
|
+
thinkingLevel: opts.thinkingLevel,
|
|
262
|
+
noTools: sessionOptions.noTools,
|
|
263
|
+
tools: sessionOptions.tools,
|
|
264
|
+
customTools,
|
|
265
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
266
|
+
});
|
|
267
|
+
const createSession = rc.createSession;
|
|
268
|
+
if (createSession) {
|
|
269
|
+
return {
|
|
270
|
+
selectedSkills: [],
|
|
271
|
+
createSession: (sessionOptions) =>
|
|
272
|
+
createSession({
|
|
273
|
+
cwd,
|
|
274
|
+
...commonSessionOptions(sessionOptions),
|
|
275
|
+
}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const skillOptions = {
|
|
280
|
+
prompt,
|
|
281
|
+
skills: opts.skills,
|
|
282
|
+
log: (message: string) => rc.progress.log(`${label}: ${message}`),
|
|
283
|
+
};
|
|
284
|
+
const preparedSkills = prepareAgentSkillResources(skillOptions);
|
|
285
|
+
const services = await createAgentSessionServices({
|
|
286
|
+
cwd,
|
|
287
|
+
resourceLoaderOptions: preparedSkills.resourceLoaderOptions,
|
|
288
|
+
});
|
|
289
|
+
await synchronizeWorkflowModelRuntime({
|
|
290
|
+
host: rc.modelRegistry,
|
|
291
|
+
child: services.modelRuntime,
|
|
292
|
+
selectedModel: model,
|
|
293
|
+
// Shared-cwd sessions mirror live removals; isolated cwd sessions retain target-only providers.
|
|
294
|
+
removeChildOnlyProviders: cwd === rc.cwd,
|
|
295
|
+
});
|
|
296
|
+
for (const diagnostic of services.diagnostics) {
|
|
297
|
+
rc.progress.log(`${label}: session ${diagnostic.type}: ${diagnostic.message}`);
|
|
298
|
+
}
|
|
299
|
+
const selectedSkills = preparedSkills.resolve(services.resourceLoader);
|
|
300
|
+
return {
|
|
301
|
+
selectedSkills,
|
|
302
|
+
createSession: (sessionOptions) =>
|
|
303
|
+
createAgentSessionFromServices({
|
|
304
|
+
services,
|
|
305
|
+
...commonSessionOptions(sessionOptions),
|
|
306
|
+
}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
|
|
311
|
+
if (!signal) return () => {};
|
|
312
|
+
const onAbort = () => {
|
|
313
|
+
void session.abort().catch(() => undefined);
|
|
314
|
+
};
|
|
315
|
+
if (signal.aborted) {
|
|
316
|
+
onAbort();
|
|
317
|
+
return () => {};
|
|
318
|
+
}
|
|
319
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
320
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "noTools">;
|
|
324
|
+
|
|
325
|
+
interface ToolSelection {
|
|
326
|
+
readonly sessionOptions: ToolSessionOptions;
|
|
327
|
+
readonly activeTools?: readonly string[];
|
|
328
|
+
readonly toolHints: NonNullable<AgentExecutionOptions["toolHints"]>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean): ToolSelection {
|
|
332
|
+
const toolHints = opts.toolHints ?? [];
|
|
333
|
+
const fallback = toolHints.includes("search")
|
|
334
|
+
? DEFAULT_SEARCH_BASE_TOOLS
|
|
335
|
+
: toolHints.includes("external-search")
|
|
336
|
+
? ["read"]
|
|
337
|
+
: undefined;
|
|
338
|
+
const activeTools = buildToolList(opts, skillsEnabled, fallback);
|
|
339
|
+
if (toolHints.length === 0) {
|
|
340
|
+
return {
|
|
341
|
+
sessionOptions: { tools: activeTools ? [...activeTools] : undefined },
|
|
342
|
+
activeTools,
|
|
343
|
+
toolHints,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
sessionOptions: { noTools: "builtin" },
|
|
349
|
+
activeTools,
|
|
350
|
+
toolHints,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildToolList(
|
|
355
|
+
opts: AgentExecutionOptions,
|
|
356
|
+
skillsEnabled: boolean,
|
|
357
|
+
fallback?: readonly string[],
|
|
358
|
+
): string[] | undefined {
|
|
359
|
+
const configured = opts.tools ?? fallback;
|
|
360
|
+
const allow = configured ? [...configured] : undefined;
|
|
361
|
+
if (!allow) return undefined;
|
|
362
|
+
if (skillsEnabled && !allow.includes("read")) allow.push("read");
|
|
363
|
+
if (opts.schema && !allow.includes(FINAL_TOOL)) allow.push(FINAL_TOOL);
|
|
364
|
+
return allow;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyDynamicToolHints(
|
|
368
|
+
session: AgentRunnerSession,
|
|
369
|
+
selection: ToolSelection,
|
|
370
|
+
): ReadonlySet<AgentToolHint> {
|
|
371
|
+
const active = new Set(selection.activeTools ?? []);
|
|
372
|
+
const matched = new Set<AgentToolHint>();
|
|
373
|
+
for (const tool of session.getAllTools()) {
|
|
374
|
+
for (const hint of selection.toolHints) {
|
|
375
|
+
if (!matchesAgentToolHint(tool, hint)) continue;
|
|
376
|
+
matched.add(hint);
|
|
377
|
+
active.add(tool.name);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
session.setActiveToolsByName([...active]);
|
|
381
|
+
return matched;
|
|
382
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateAgentSessionServicesOptions,
|
|
3
|
+
ResourceDiagnostic,
|
|
4
|
+
ResourceLoader,
|
|
5
|
+
Skill,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { logicalWorkspacePath, type ReplayWorkspaceRoots } from "./replay-path-identity.ts";
|
|
9
|
+
import { captureTreeFingerprint } from "./tree-fingerprint.ts";
|
|
10
|
+
import type { ResolvedSkillIdentity } from "./resume-context.ts";
|
|
11
|
+
|
|
12
|
+
interface AgentSkillRequest {
|
|
13
|
+
readonly selectors: readonly string[];
|
|
14
|
+
readonly strict: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentSkillResolution {
|
|
18
|
+
readonly selected: readonly Skill[];
|
|
19
|
+
readonly unmatched: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type AgentSkillResourceLoaderOptions = Pick<
|
|
23
|
+
NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>,
|
|
24
|
+
"noSkills" | "skillsOverride"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
interface PreparedAgentSkillResources {
|
|
28
|
+
readonly resourceLoaderOptions: AgentSkillResourceLoaderOptions;
|
|
29
|
+
resolve(resourceLoader: Pick<ResourceLoader, "getSkills">): readonly Skill[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AgentSkillIdentityCapture =
|
|
33
|
+
| { readonly kind: "verified"; readonly skills: readonly ResolvedSkillIdentity[] }
|
|
34
|
+
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
35
|
+
|
|
36
|
+
export interface AgentSkillIdentityOptions extends ReplayWorkspaceRoots {
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SKILL_FINGERPRINT_EXCLUSIONS = new Set([".git", "node_modules"]);
|
|
41
|
+
const SKILL_FINGERPRINT_MAX_BYTES = 8 << 20;
|
|
42
|
+
const SKILL_FINGERPRINT_MAX_FILES = 1024;
|
|
43
|
+
|
|
44
|
+
const SLASH_SKILL_PATTERN = /\/skill:([a-z0-9][a-z0-9-]{0,63})/gi;
|
|
45
|
+
const SKILLS_FIELD_PATTERN = /(?<!\/)\bskills?\s*[:=]\s*([^\n.;]+)/gi;
|
|
46
|
+
const VERB_SKILL_LIST_PATTERN = /\b(?:include|use|enable|load|provide|allow|grant|with)\s+(?:the\s+|this\s+|these\s+)?skills?\s+([^\n.;]+)/gi;
|
|
47
|
+
const VERB_NAMED_SKILL_PATTERN = /\b(?:include|use|enable|load|provide|allow|grant|with)\s+(?:the\s+|this\s+|these\s+)?(["'`]?)([a-z0-9][a-z0-9 _-]{0,80}?)\1\s+skills?\b/gi;
|
|
48
|
+
const PURPOSE_BOUNDARY_PATTERN = /\s+\b(?:for|to|when|while|so|because|in\s+order\s+to)\b.*$/i;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve which subagent skills were requested.
|
|
52
|
+
*
|
|
53
|
+
* Subagents are skillless by default. Explicit `opts.skills` wins and suppresses prompt inference;
|
|
54
|
+
* when it is omitted we only infer from clear natural-language opt-ins such as `/skill:name`,
|
|
55
|
+
* `include skill diagnose`, `include this Skill diagnose`, or `use the diagnose skill`.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveAgentSkillRequest(prompt: string, explicitSkills: unknown): AgentSkillRequest {
|
|
58
|
+
if (explicitSkills !== undefined) {
|
|
59
|
+
return { selectors: normalizeExplicitSkillSelectors(explicitSkills), strict: true };
|
|
60
|
+
}
|
|
61
|
+
return { selectors: extractSkillSelectorsFromText(prompt), strict: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function extractSkillSelectorsFromText(text: string): string[] {
|
|
65
|
+
const selectors: string[] = [];
|
|
66
|
+
collectPatternMatches(text, SLASH_SKILL_PATTERN, selectors, 1);
|
|
67
|
+
collectListPatternMatches(text, SKILLS_FIELD_PATTERN, selectors);
|
|
68
|
+
collectListPatternMatches(text, VERB_SKILL_LIST_PATTERN, selectors);
|
|
69
|
+
collectPatternMatches(text, VERB_NAMED_SKILL_PATTERN, selectors, 2);
|
|
70
|
+
return uniqueSelectors(selectors);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeSkillSelector(value: string): string {
|
|
74
|
+
return value
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/^\/skill:/i, "")
|
|
77
|
+
.replace(/^skills?\s*[:=]?\s*/i, "")
|
|
78
|
+
.replace(/^(?:named|called)\s+/i, "")
|
|
79
|
+
.replace(/^(?:the|this|these)\s+/i, "")
|
|
80
|
+
.replace(/\s+skills?$/i, "")
|
|
81
|
+
.replace(/^[`"']+|[`"']+$/g, "")
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[\s_]+/g, "-")
|
|
85
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
86
|
+
.replace(/-+/g, "-")
|
|
87
|
+
.replace(/^-|-$/g, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function selectAgentSkills(skills: readonly Skill[], selectors: readonly string[]): AgentSkillResolution {
|
|
91
|
+
const selected = new Map<string, Skill>();
|
|
92
|
+
const unmatched: string[] = [];
|
|
93
|
+
|
|
94
|
+
for (const selector of selectors) {
|
|
95
|
+
const match = matchSkillSelector(skills, selector);
|
|
96
|
+
if (!match) {
|
|
97
|
+
unmatched.push(selector);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
selected.set(match.name, makeSkillModelInvocable(match));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { selected: [...selected.values()], unmatched };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Prepare the skill-selection hooks consumed by pi's session-service factory.
|
|
108
|
+
* The returned resolver is scoped to one resource loader and must not be reused
|
|
109
|
+
* across concurrently-created session services.
|
|
110
|
+
*/
|
|
111
|
+
export function prepareAgentSkillResources(options: {
|
|
112
|
+
readonly prompt: string;
|
|
113
|
+
readonly skills: unknown;
|
|
114
|
+
readonly log?: (message: string) => void;
|
|
115
|
+
}): PreparedAgentSkillResources {
|
|
116
|
+
const request = resolveAgentSkillRequest(options.prompt, options.skills);
|
|
117
|
+
let resolution: AgentSkillResolution = { selected: [], unmatched: [] };
|
|
118
|
+
let availableSkillNames: readonly string[] = [];
|
|
119
|
+
|
|
120
|
+
const resourceLoaderOptions: AgentSkillResourceLoaderOptions = {
|
|
121
|
+
noSkills: request.selectors.length === 0,
|
|
122
|
+
skillsOverride:
|
|
123
|
+
request.selectors.length === 0
|
|
124
|
+
? undefined
|
|
125
|
+
: (current) => {
|
|
126
|
+
availableSkillNames = current.skills.map((skill) => skill.name).sort();
|
|
127
|
+
resolution = selectAgentSkills(current.skills, request.selectors);
|
|
128
|
+
return { skills: [...resolution.selected], diagnostics: current.diagnostics };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
resourceLoaderOptions,
|
|
134
|
+
resolve(resourceLoader) {
|
|
135
|
+
const { diagnostics } = resourceLoader.getSkills();
|
|
136
|
+
logSkillDiagnostics(options.log, diagnostics, request.selectors.length > 0);
|
|
137
|
+
|
|
138
|
+
if (request.strict && resolution.unmatched.length > 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Unknown subagent skill${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}. Available: ${
|
|
141
|
+
availableSkillNames.join(", ") || "(none)"
|
|
142
|
+
}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!request.strict && resolution.unmatched.length > 0) {
|
|
147
|
+
options.log?.(`Ignoring unrecognized subagent skill mention${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resolution.selected;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Capture the exact selected skill directories before journal lookup. */
|
|
156
|
+
export async function captureAgentSkillIdentities(
|
|
157
|
+
skills: readonly Skill[],
|
|
158
|
+
options: AgentSkillIdentityOptions,
|
|
159
|
+
): Promise<AgentSkillIdentityCapture> {
|
|
160
|
+
const identities: ResolvedSkillIdentity[] = [];
|
|
161
|
+
for (const skill of skills) {
|
|
162
|
+
const root = resolve(dirname(skill.filePath));
|
|
163
|
+
const capture = await captureTreeFingerprint({
|
|
164
|
+
root,
|
|
165
|
+
excludedRelativePaths: SKILL_FINGERPRINT_EXCLUSIONS,
|
|
166
|
+
maxBytes: SKILL_FINGERPRINT_MAX_BYTES,
|
|
167
|
+
maxFiles: SKILL_FINGERPRINT_MAX_FILES,
|
|
168
|
+
signal: options.signal,
|
|
169
|
+
});
|
|
170
|
+
if (capture.kind === "unverifiable") {
|
|
171
|
+
return { kind: "unverifiable", reason: `skill ${skill.name} could not be fingerprinted: ${capture.reason}` };
|
|
172
|
+
}
|
|
173
|
+
identities.push({
|
|
174
|
+
name: skill.name,
|
|
175
|
+
path: logicalWorkspacePath(root, options) ?? `external:${skill.name}`,
|
|
176
|
+
fingerprint: capture.fingerprint,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { kind: "verified", skills: identities };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function collectPatternMatches(text: string, pattern: RegExp, selectors: string[], captureIndex: number): void {
|
|
183
|
+
pattern.lastIndex = 0;
|
|
184
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
185
|
+
const value = match[captureIndex];
|
|
186
|
+
if (value !== undefined) selectors.push(value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectListPatternMatches(text: string, pattern: RegExp, selectors: string[]): void {
|
|
191
|
+
pattern.lastIndex = 0;
|
|
192
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
193
|
+
const value = match[1];
|
|
194
|
+
if (value === undefined) continue;
|
|
195
|
+
selectors.push(...parseSkillList(value));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseSkillList(fragment: string): string[] {
|
|
200
|
+
const scoped = fragment.replace(PURPOSE_BOUNDARY_PATTERN, "");
|
|
201
|
+
return scoped
|
|
202
|
+
.split(/\s*(?:,|\s+\band\b\s+|&|\+)\s*/i)
|
|
203
|
+
.map((part) => part.trim())
|
|
204
|
+
.filter((part) => part.length > 0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
throw new Error("Invalid subagent skills option: expected an array of skill names.");
|
|
210
|
+
}
|
|
211
|
+
const seen = new Set<string>();
|
|
212
|
+
const result: string[] = [];
|
|
213
|
+
for (const item of value) {
|
|
214
|
+
if (typeof item !== "string") {
|
|
215
|
+
throw new Error("Invalid subagent skills option: every skill name must be a string.");
|
|
216
|
+
}
|
|
217
|
+
const normalized = normalizeSkillSelector(item);
|
|
218
|
+
if (!normalized || isGenericSkillWord(normalized)) {
|
|
219
|
+
throw new Error(`Invalid subagent skills option: "${item}" is not a valid skill name.`);
|
|
220
|
+
}
|
|
221
|
+
if (seen.has(normalized)) continue;
|
|
222
|
+
seen.add(normalized);
|
|
223
|
+
result.push(normalized);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagnostics: readonly ResourceDiagnostic[], requested: boolean): void {
|
|
229
|
+
if (!requested || !log) return;
|
|
230
|
+
for (const diagnostic of diagnostics) {
|
|
231
|
+
const where = diagnostic.path ? ` (${diagnostic.path})` : "";
|
|
232
|
+
log(`skill ${diagnostic.type}: ${diagnostic.message}${where}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function uniqueSelectors(values: Iterable<string>): string[] {
|
|
237
|
+
const seen = new Set<string>();
|
|
238
|
+
const result: string[] = [];
|
|
239
|
+
for (const value of values) {
|
|
240
|
+
const normalized = normalizeSkillSelector(value);
|
|
241
|
+
if (!normalized || isGenericSkillWord(normalized) || seen.has(normalized)) continue;
|
|
242
|
+
seen.add(normalized);
|
|
243
|
+
result.push(normalized);
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isGenericSkillWord(value: string): boolean {
|
|
249
|
+
return value === "skill" || value === "skills" || value === "the" || value === "this" || value === "these" || value === "that";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function matchSkillSelector(skills: readonly Skill[], selector: string): Skill | undefined {
|
|
253
|
+
const normalized = normalizeSkillSelector(selector);
|
|
254
|
+
if (!normalized) return undefined;
|
|
255
|
+
|
|
256
|
+
const exact = skills.find((skill) => skill.name.toLowerCase() === normalized);
|
|
257
|
+
if (exact) return exact;
|
|
258
|
+
|
|
259
|
+
const selectorWords = normalized.replace(/-/g, " ");
|
|
260
|
+
const fuzzy = skills.filter((skill) => {
|
|
261
|
+
const name = skill.name.toLowerCase();
|
|
262
|
+
const words = name.replace(/-/g, " ");
|
|
263
|
+
return name.includes(normalized) || words.includes(selectorWords);
|
|
264
|
+
});
|
|
265
|
+
return fuzzy.length === 1 ? fuzzy[0] : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function makeSkillModelInvocable(skill: Skill): Skill {
|
|
269
|
+
return skill.disableModelInvocation ? { ...skill, disableModelInvocation: false } : skill;
|
|
270
|
+
}
|