pi-chalin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -0
- package/agents/conflict-resolver.md +28 -0
- package/agents/context-builder.md +31 -0
- package/agents/delegate.md +28 -0
- package/agents/oracle.md +28 -0
- package/agents/planner.md +28 -0
- package/agents/researcher.md +29 -0
- package/agents/reviewer.md +30 -0
- package/agents/scout.md +32 -0
- package/agents/worker.md +29 -0
- package/package.json +91 -0
- package/src/agent-overrides.ts +12 -0
- package/src/agents.ts +274 -0
- package/src/artifacts.ts +326 -0
- package/src/autoroute.ts +274 -0
- package/src/budget.ts +333 -0
- package/src/child-sessions.ts +108 -0
- package/src/child-tools.ts +796 -0
- package/src/commands.ts +140 -0
- package/src/config.ts +189 -0
- package/src/discovery.ts +190 -0
- package/src/index.ts +40 -0
- package/src/interview.ts +202 -0
- package/src/kernel.ts +254 -0
- package/src/memory.ts +945 -0
- package/src/model-resolution.ts +106 -0
- package/src/orchestration.ts +99 -0
- package/src/paths.ts +50 -0
- package/src/route-format.ts +149 -0
- package/src/route-guards.ts +92 -0
- package/src/route-widget.ts +219 -0
- package/src/runner-prompt.ts +346 -0
- package/src/runner-state.ts +105 -0
- package/src/runner.ts +1185 -0
- package/src/runtime-state.ts +175 -0
- package/src/schemas.ts +316 -0
- package/src/snapshot.ts +282 -0
- package/src/sql-js-fts5.d.ts +4 -0
- package/src/tools.ts +558 -0
- package/src/ui-agents.ts +338 -0
- package/src/ui-status.ts +87 -0
- package/src/ui.ts +875 -0
- package/src/webfetch.ts +294 -0
- package/src/worktrees.ts +113 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { mergedSessionModelOverrides, mergedSessionThinkingOverrides } from "./agent-overrides.ts";
|
|
5
|
+
import { AgentCatalog } from "./agents.ts";
|
|
6
|
+
import { ArtifactStore } from "./artifacts.ts";
|
|
7
|
+
import { loadEffectiveConfig } from "./config.ts";
|
|
8
|
+
import { ChalinKernel, routeFromPlan } from "./kernel.ts";
|
|
9
|
+
import { createMemoryCandidate, MemoryStore } from "./memory.ts";
|
|
10
|
+
import { formatInterviewResult, runChalinInterview, type InterviewRequestInput } from "./interview.ts";
|
|
11
|
+
import { loadResumableRunState } from "./runner-state.ts";
|
|
12
|
+
import { beginChalinRouteInvocation, finishChalinRouteInvocation, setLatestRun } from "./runtime-state.ts";
|
|
13
|
+
import { openSafetyApproval } from "./ui.ts";
|
|
14
|
+
import { clearLegacyChalinControlWidget, setChalinStatus } from "./ui-status.ts";
|
|
15
|
+
import { chalinRouteUpdateDetails, colorizeChalinWidget, footerStateForRun, formatChalinRoutePlanWidget, formatChalinRunWidget, formatChalinRunWidgetFromDetails, isUsableStepStatus, plannedWidgetRun, routeIntent, type ChalinRouteWidgetDetails } from "./route-widget.ts";
|
|
16
|
+
import { fetchWebUrls, formatWebBundle, searchWeb } from "./webfetch.ts";
|
|
17
|
+
import type { RouteDecision, RunState } from "./schemas.ts";
|
|
18
|
+
import { directExecutionRecommendation, ensureMutationRouteHasWorker } from "./route-guards.ts";
|
|
19
|
+
import { compactRouteDetails, finalAnswerMaterial, formatDirectRecommendation, formatRoute, outcomeForResult } from "./route-format.ts";
|
|
20
|
+
|
|
21
|
+
const AgentStepParams = Type.Object({
|
|
22
|
+
id: Type.Optional(Type.String({ description: "Optional stable step id for DAG stages." })),
|
|
23
|
+
agent: Type.String({ description: "Agent name from the pi-chalin catalog, e.g. scout, planner, worker, reviewer." }),
|
|
24
|
+
task: Type.String({ description: "Concrete bounded task and success criteria for this subagent." }),
|
|
25
|
+
budget: Type.Optional(Type.Union([
|
|
26
|
+
Type.Literal("tight"),
|
|
27
|
+
Type.Literal("normal"),
|
|
28
|
+
Type.Literal("deep"),
|
|
29
|
+
Type.Literal("extended"),
|
|
30
|
+
], { description: "Per-subagent tool-call budget profile. Use normal by default, deep for project-wide/folder fan-out analysis, extended only for long autonomous stages with artifacts/checkpoints." })),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const AgentStageParams = Type.Object({
|
|
34
|
+
id: Type.Optional(Type.String({ description: "Stable stage id, e.g. discover, fanout, review." })),
|
|
35
|
+
name: Type.Optional(Type.String({ description: "Human-readable stage name." })),
|
|
36
|
+
tasks: Type.Array(AgentStepParams, { description: "Tasks that can run in parallel inside this stage." }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const InterviewChoiceParams = Type.Object({
|
|
41
|
+
label: Type.String({ description: "Short answer option shown to the user." }),
|
|
42
|
+
value: Type.Optional(Type.String({ description: "Optional expanded value saved when this option is selected." })),
|
|
43
|
+
recommended: Type.Optional(Type.Boolean({ description: "Mark the orchestrator-recommended answer. At most one per question." })),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const InterviewQuestionParams = Type.Object({
|
|
47
|
+
id: Type.Optional(Type.String({ description: "Stable short id, e.g. scope, risk, unknown-term." })),
|
|
48
|
+
question: Type.String({ description: "Clear concise question. Avoid jargon." }),
|
|
49
|
+
choices: Type.Array(InterviewChoiceParams, { description: "Two to five concise choices. Include a recommended option." }),
|
|
50
|
+
allowCustom: Type.Optional(Type.Boolean({ description: "Allow a free-form custom answer. Defaults to true." })),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ChalinInterviewParams = Type.Object({
|
|
54
|
+
featureId: Type.Optional(Type.String({ description: "Artifact id to persist interview answers under. Defaults from task." })),
|
|
55
|
+
task: Type.String({ description: "Original user request or feature being clarified." }),
|
|
56
|
+
reason: Type.String({ description: "Why clarification is required before planning or running agents." }),
|
|
57
|
+
questions: Type.Array(InterviewQuestionParams, { description: "Batch of up to five blocking clarification questions." }),
|
|
58
|
+
batchSize: Type.Optional(Type.Number({ description: "Maximum questions to ask in this batch. Default 5, hard max 5." })),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const ChalinRouteParams = Type.Object({
|
|
62
|
+
task: Type.String({ description: "The user's request being handled." }),
|
|
63
|
+
topology: Type.Union([
|
|
64
|
+
Type.Literal("single"),
|
|
65
|
+
Type.Literal("chain"),
|
|
66
|
+
Type.Literal("parallel"),
|
|
67
|
+
Type.Literal("dag"),
|
|
68
|
+
Type.Literal("memory-only"),
|
|
69
|
+
], { description: "The workflow shape chosen by the primary Pi agent." }),
|
|
70
|
+
steps: Type.Optional(Type.Array(AgentStepParams, { description: "Ordered steps for single/chain, independent tasks for parallel. Omit for memory-only." })),
|
|
71
|
+
stages: Type.Optional(Type.Array(AgentStageParams, { description: "For dag topology: ordered stages; tasks inside each stage run in parallel after prior stage completes." })),
|
|
72
|
+
risk: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("critical")], { description: "Risk estimated by the primary Pi agent." })),
|
|
73
|
+
needsMemory: Type.Optional(Type.Boolean({ description: "Whether pi-chalin should retrieve relevant project/user memory before running." })),
|
|
74
|
+
needsArtifacts: Type.Optional(Type.Boolean({ description: "Whether the workflow is expected to inspect or create artifacts/files." })),
|
|
75
|
+
reason: Type.Optional(Type.String({ description: "Short rationale for using chalin instead of answering directly." })),
|
|
76
|
+
dryRun: Type.Optional(Type.Boolean({ description: "If true, validate and preview the chosen route without running subagents." })),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
type ChalinRouteToolParams = {
|
|
80
|
+
task: string;
|
|
81
|
+
topology: "single" | "chain" | "parallel" | "dag" | "memory-only";
|
|
82
|
+
steps?: Array<{ id?: string; agent: string; task: string; budget?: "tight" | "normal" | "deep" | "extended" }>;
|
|
83
|
+
stages?: Array<{ id?: string; name?: string; tasks: Array<{ id?: string; agent: string; task: string; budget?: "tight" | "normal" | "deep" | "extended" }> }>;
|
|
84
|
+
risk?: RouteDecision["risk"];
|
|
85
|
+
needsMemory?: boolean;
|
|
86
|
+
needsArtifacts?: boolean;
|
|
87
|
+
reason?: string;
|
|
88
|
+
dryRun?: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const WebFreshnessParam = Type.Optional(Type.Union([
|
|
92
|
+
Type.Literal("cache-ok"),
|
|
93
|
+
Type.Literal("prefer-fresh"),
|
|
94
|
+
Type.Literal("must-be-fresh"),
|
|
95
|
+
], { description: "Freshness policy. cache-ok uses local cache; prefer-fresh refreshes when possible; must-be-fresh bypasses cache." }));
|
|
96
|
+
|
|
97
|
+
const ChalinWebSearchParams = Type.Object({
|
|
98
|
+
query: Type.Optional(Type.String({ description: "Web search query." })),
|
|
99
|
+
url: Type.Optional(Type.String({ description: "Single URL to fetch as clean web context." })),
|
|
100
|
+
urls: Type.Optional(Type.Array(Type.String(), { description: "URLs to fetch as clean web context." })),
|
|
101
|
+
maxSources: Type.Optional(Type.Number({ description: "Maximum search sources, default 5, max 10." })),
|
|
102
|
+
depth: Type.Optional(Type.Union([Type.Literal("snippets"), Type.Literal("content")], { description: "snippets by default; content asks Exa for more page text." })),
|
|
103
|
+
freshness: WebFreshnessParam,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const ChalinMemorySearchParams = Type.Object({
|
|
107
|
+
query: Type.String({ description: "Compact local memory search query." }),
|
|
108
|
+
limit: Type.Optional(Type.Number({ description: "Maximum memories to return. Default 6, max 10." })),
|
|
109
|
+
tokenBudget: Type.Optional(Type.Number({ description: "Approximate token budget for returned context. Default 700, max 1600." })),
|
|
110
|
+
includeEvidence: Type.Optional(Type.Boolean({ description: "Include evidence when checking contradictions or reviewing memory quality." })),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const ChalinMemoryWriteParams = Type.Object({
|
|
114
|
+
category: Type.String({ description: "Memory category such as testing, architecture, workflow, user-preference, or tooling." }),
|
|
115
|
+
content: Type.String({ description: "Compact durable project/user knowledge. Do not write logs, command output, or trivial completion notes." }),
|
|
116
|
+
confidence: Type.Optional(Type.Number({ description: "Confidence from 0 to 1. Default 0.85." })),
|
|
117
|
+
evidence: Type.Optional(Type.String({ description: "Short evidence for why this memory is durable." })),
|
|
118
|
+
topicKey: Type.Optional(Type.String({ description: "Optional stable topic key for revision/deduplication." })),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const ChalinMemoryReviseParams = Type.Object({
|
|
122
|
+
id: Type.String({ description: "Existing memory record id to revise." }),
|
|
123
|
+
content: Type.String({ description: "Corrected compact memory content." }),
|
|
124
|
+
category: Type.Optional(Type.String({ description: "Optional replacement category." })),
|
|
125
|
+
confidence: Type.Optional(Type.Number({ description: "Confidence from 0 to 1. Default 0.9." })),
|
|
126
|
+
evidence: Type.Optional(Type.String({ description: "Evidence proving the old memory is stale or weaker." })),
|
|
127
|
+
reason: Type.String({ description: "Why the revision is more accurate or useful than the prior memory." }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
const ChalinArtifactResumeParams = Type.Object({
|
|
132
|
+
featureId: Type.String({ description: "Feature/task artifact id to resume, e.g. memory-and-artifacts." }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const ChalinResumeParams = Type.Object({
|
|
136
|
+
runId: Type.Optional(Type.String({ description: "Optional pi-chalin run id. If omitted, resumes the latest paused/stale run." })),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
type ChalinArtifactResumeToolParams = {
|
|
140
|
+
featureId: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type ChalinResumeToolParams = {
|
|
144
|
+
runId?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ChalinWebSearchToolParams = {
|
|
148
|
+
query?: string;
|
|
149
|
+
url?: string;
|
|
150
|
+
urls?: string[];
|
|
151
|
+
maxSources?: number;
|
|
152
|
+
depth?: "snippets" | "content";
|
|
153
|
+
freshness?: "cache-ok" | "prefer-fresh" | "must-be-fresh";
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type ChalinMemorySearchToolParams = {
|
|
157
|
+
query: string;
|
|
158
|
+
limit?: number;
|
|
159
|
+
tokenBudget?: number;
|
|
160
|
+
includeEvidence?: boolean;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
type ChalinMemoryWriteToolParams = {
|
|
164
|
+
category: string;
|
|
165
|
+
content: string;
|
|
166
|
+
confidence?: number;
|
|
167
|
+
evidence?: string;
|
|
168
|
+
topicKey?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type ChalinMemoryReviseToolParams = {
|
|
172
|
+
id: string;
|
|
173
|
+
content: string;
|
|
174
|
+
category?: string;
|
|
175
|
+
confidence?: number;
|
|
176
|
+
evidence?: string;
|
|
177
|
+
reason: string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export function registerChalinTools(pi: ExtensionAPI): void {
|
|
181
|
+
pi.registerTool({
|
|
182
|
+
name: "chalin_interview",
|
|
183
|
+
label: "Chalin Interview",
|
|
184
|
+
description: "Ask blocking clarification questions in the TUI and persist answers as pi-chalin artifacts before planning or running subagents.",
|
|
185
|
+
promptSnippet: "chalin_interview: when the request is ambiguous or missing critical information, ask concise multiple-choice questions before chalin_route.",
|
|
186
|
+
promptGuidelines: [
|
|
187
|
+
"Use chalin_interview before chalin_route when the user's request has unknown terms, missing scope, uncovered constraints, destructive/risky choices, or multiple valid directions with meaningful tradeoffs.",
|
|
188
|
+
"Ask only what blocks correct planning. Prefer one to five questions per batch. Each question must have two to five concise options and exactly one recommended option when possible.",
|
|
189
|
+
"Always allow a custom answer unless the answer space must be constrained for safety.",
|
|
190
|
+
"After chalin_interview returns, use the persisted answers as artifact context and continue with planning or chalin_route only when you are confident enough.",
|
|
191
|
+
],
|
|
192
|
+
parameters: ChalinInterviewParams,
|
|
193
|
+
async execute(_toolCallId, params: InterviewRequestInput, _signal, _onUpdate, ctx) {
|
|
194
|
+
const store = new ArtifactStore({ cwd: ctx.cwd });
|
|
195
|
+
const result = await runChalinInterview(ctx, store, params);
|
|
196
|
+
return textResult(formatInterviewResult(result), { interview: result });
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
pi.registerTool({
|
|
201
|
+
name: "chalin_route",
|
|
202
|
+
label: "Chalin Route",
|
|
203
|
+
description: [
|
|
204
|
+
"Run a pi-chalin workflow chosen by the primary Pi agent.",
|
|
205
|
+
"The caller decides whether chalin is needed, which agents to use, and whether the workflow is single, chained, parallel, DAG/staged, or memory-only.",
|
|
206
|
+
"Do not use for bounded explicit-file refactors/bugfixes; native tools are faster and pi-chalin will recommend direct execution for those.",
|
|
207
|
+
].join(" "),
|
|
208
|
+
promptSnippet: "chalin_route: delegate broad/risky/deep workflows to pi-chalin subagents; do not use for bounded explicit-file edits.",
|
|
209
|
+
promptGuidelines: [
|
|
210
|
+
"Use chalin_route only when subagents materially improve quality, confidence, context isolation, or review; do not use it for simple direct answers.",
|
|
211
|
+
"Do not call chalin_route for explicit-file refactor/fix/add-test tasks with one to three named files unless the prompt says broad, risky, long-file, security/auth, migration, or no-rewrite.",
|
|
212
|
+
"Do not call chalin_route for bounded read-only mini-project reviews that explicitly forbid file modification; inspect directly and answer with path evidence.",
|
|
213
|
+
"When using chalin_route, choose the minimal agent topology yourself and provide concrete tasks with success criteria.",
|
|
214
|
+
"Use dag topology for staged fan-out/fan-in: for example scout first, multiple folder/module agents in parallel, then reviewer/context-builder synthesis.",
|
|
215
|
+
"After chalin_route returns, immediately answer the user from its Final answer material; do not call more tools unless it explicitly says a critical gap remains.",
|
|
216
|
+
],
|
|
217
|
+
parameters: ChalinRouteParams,
|
|
218
|
+
async execute(_toolCallId, params: ChalinRouteToolParams, signal, onUpdate, ctx) {
|
|
219
|
+
const loaded = loadEffectiveConfig({ cwd: ctx.cwd });
|
|
220
|
+
const catalog = AgentCatalog.load({ cwd: ctx.cwd });
|
|
221
|
+
const memory = new MemoryStore({ cwd: ctx.cwd });
|
|
222
|
+
const kernel = new ChalinKernel({
|
|
223
|
+
cwd: ctx.cwd,
|
|
224
|
+
config: loaded.config,
|
|
225
|
+
catalog,
|
|
226
|
+
memory,
|
|
227
|
+
modelOverrides: mergedSessionModelOverrides(loaded.config.agents.modelOverrides),
|
|
228
|
+
thinkingOverrides: mergedSessionThinkingOverrides(loaded.config.agents.thinkingOverrides),
|
|
229
|
+
});
|
|
230
|
+
let route = routeFromPlan(params);
|
|
231
|
+
if (loaded.config.safety.mutationExpectationGuard) {
|
|
232
|
+
route = ensureMutationRouteHasWorker(route, params.task);
|
|
233
|
+
}
|
|
234
|
+
const agents = catalog.list();
|
|
235
|
+
const unknownAgents = route.agents.filter((agent) => !catalog.resolve(agent).agent);
|
|
236
|
+
|
|
237
|
+
if (!loaded.config.enabled) {
|
|
238
|
+
return textResult("pi-chalin is disabled for this project. Answer directly or ask the user to run /chalin on.", { route, diagnostics: loaded.diagnostics });
|
|
239
|
+
}
|
|
240
|
+
if (unknownAgents.length > 0) {
|
|
241
|
+
return textResult(`Unknown pi-chalin agent(s): ${unknownAgents.join(", ")}\nAvailable agents: ${agents.map((agent) => agent.name).join(", ")}`, { route, diagnostics: catalog.diagnostics });
|
|
242
|
+
}
|
|
243
|
+
if (route.kind === "ask-user") {
|
|
244
|
+
return textResult(route.reason, { route });
|
|
245
|
+
}
|
|
246
|
+
const directRecommendation = directExecutionRecommendation(params.task, route);
|
|
247
|
+
if (directRecommendation) {
|
|
248
|
+
const guard = beginChalinRouteInvocation({ dryRun: false, route });
|
|
249
|
+
if (!guard.allowed) {
|
|
250
|
+
return textResult(guard.reason ?? "chalin_route already executed for this prompt.", { route, guard });
|
|
251
|
+
}
|
|
252
|
+
finishChalinRouteInvocation(guard.invocationId, "dry-run");
|
|
253
|
+
setChalinStatus(ctx, { kind: "idle" });
|
|
254
|
+
return textResult(formatDirectRecommendation(route, directRecommendation), {
|
|
255
|
+
route,
|
|
256
|
+
routeGuard: { action: "direct-recommended", reason: directRecommendation },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const guard = beginChalinRouteInvocation({ dryRun: Boolean(params.dryRun), route });
|
|
260
|
+
if (!guard.allowed) {
|
|
261
|
+
return textResult(guard.reason ?? "chalin_route already executed for this prompt.", { route, guard });
|
|
262
|
+
}
|
|
263
|
+
if (params.dryRun) {
|
|
264
|
+
finishChalinRouteInvocation(guard.invocationId, "dry-run");
|
|
265
|
+
return textResult(formatRoute(route, undefined, { availableAgents: agents.map((agent) => agent.name) }), { route, diagnostics: [...loaded.diagnostics, catalog.diagnostics] });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
clearLegacyChalinControlWidget(ctx);
|
|
269
|
+
onUpdate?.({
|
|
270
|
+
content: [{ type: "text", text: formatChalinRoutePlanWidget(params) }],
|
|
271
|
+
details: { route, run: plannedWidgetRun(route) },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const preApproval = await kernel.approvalFor(route);
|
|
275
|
+
const approvalOverride = preApproval.action === "ask" && await openSafetyApproval(ctx, route, preApproval)
|
|
276
|
+
? { action: "allow" as const, reason: "Approved once through pi-chalin Safety Approval." }
|
|
277
|
+
: undefined;
|
|
278
|
+
if (preApproval.action === "block" || (preApproval.action === "ask" && !approvalOverride)) {
|
|
279
|
+
finishChalinRouteInvocation(guard.invocationId, preApproval.action);
|
|
280
|
+
setChalinStatus(ctx, preApproval.action === "block" ? { kind: "failed" } : { kind: "stopped" });
|
|
281
|
+
return textResult(formatRoute(route, { route, approval: preApproval, memories: [], diagnostics: [] }), { route, approval: preApproval });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const abortSignal = signal ?? new AbortController().signal;
|
|
285
|
+
setChalinStatus(ctx, route.plan ? { kind: "running", intent: routeIntent(route), agent: route.agents[0] ?? route.kind, completed: 0, total: Math.max(route.agents.length, 1) } : { kind: "synthesizing" });
|
|
286
|
+
let result: Awaited<ReturnType<ChalinKernel["handleRoute"]>>;
|
|
287
|
+
try {
|
|
288
|
+
result = await kernel.handleRoute(route, params.task, {
|
|
289
|
+
cwd: ctx.cwd,
|
|
290
|
+
extensionContext: ctx,
|
|
291
|
+
signal: abortSignal,
|
|
292
|
+
onUpdate: (run) => {
|
|
293
|
+
setLatestRun(run);
|
|
294
|
+
setChalinStatus(ctx, footerStateForRun(run));
|
|
295
|
+
onUpdate?.({
|
|
296
|
+
content: [{ type: "text", text: formatChalinRunWidget(run) }],
|
|
297
|
+
details: chalinRouteUpdateDetails(run),
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
}, approvalOverride);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
finishChalinRouteInvocation(guard.invocationId, "failed");
|
|
303
|
+
setChalinStatus(ctx, abortSignal.aborted ? { kind: "stopped" } : { kind: "failed" });
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
if (result.run) setLatestRun(result.run);
|
|
307
|
+
if (result.approval.action !== "allow") {
|
|
308
|
+
finishChalinRouteInvocation(guard.invocationId, outcomeForResult(result));
|
|
309
|
+
setChalinStatus(ctx, result.approval.action === "block" ? { kind: "failed" } : { kind: "stopped" });
|
|
310
|
+
return finalToolResult(ctx, formatRoute(route, result), compactRouteDetails(route, result, [...loaded.diagnostics, catalog.diagnostics]));
|
|
311
|
+
}
|
|
312
|
+
if (result.run?.status === "paused" || abortSignal.aborted) setChalinStatus(ctx, { kind: "stopped" });
|
|
313
|
+
else if (result.run?.status === "failed") setChalinStatus(ctx, { kind: "failed" });
|
|
314
|
+
else setChalinStatus(ctx, { kind: "complete", intent: routeIntent(route) });
|
|
315
|
+
finishChalinRouteInvocation(guard.invocationId, outcomeForResult(result));
|
|
316
|
+
|
|
317
|
+
return finalToolResult(ctx, formatRoute(route, result), compactRouteDetails(route, result, [...loaded.diagnostics, catalog.diagnostics]));
|
|
318
|
+
},
|
|
319
|
+
renderCall(args, theme) {
|
|
320
|
+
void args;
|
|
321
|
+
void theme;
|
|
322
|
+
// Keep the call slot intentionally empty. Pi renders call + result in the
|
|
323
|
+
// same tool component; rendering the full tree in both places creates the
|
|
324
|
+
// duplicated "planned tree + running tree" UX and layout shift while tool
|
|
325
|
+
// arguments stream in. The result slot below is the single source of UI.
|
|
326
|
+
return new Text("", 0, 0);
|
|
327
|
+
},
|
|
328
|
+
renderResult(result, _options, theme) {
|
|
329
|
+
const details = result.details as ChalinRouteWidgetDetails | undefined;
|
|
330
|
+
const rendered = details?.run ? formatChalinRunWidgetFromDetails(details) : result.content.find((part) => part.type === "text")?.text ?? "";
|
|
331
|
+
return new Text(colorizeChalinWidget(rendered, theme), 0, 0);
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
pi.registerTool({
|
|
337
|
+
name: "chalin_resume",
|
|
338
|
+
label: "Chalin Resume",
|
|
339
|
+
description: "Resume the latest paused or stale pi-chalin subagent run, preserving completed steps and continuing pending DAG/chain work.",
|
|
340
|
+
promptSnippet: "chalin_resume: resume an interrupted pi-chalin run when the user says continue/resume/continua/continúa/sigue/reanuda after ESC, abort, terminal close, or a paused run.",
|
|
341
|
+
promptGuidelines: [
|
|
342
|
+
"Use this before answering from partial findings when the user asks to continue a paused/interrupted chalin run, including short Spanish prompts like `continua`, `continúa`, `sigue`, `reanuda`, or `retoma`.",
|
|
343
|
+
"Do not create a new chalin_route for a paused run; resume the persisted run instead.",
|
|
344
|
+
"After chalin_resume returns, answer the user from the resumed Final answer material.",
|
|
345
|
+
],
|
|
346
|
+
parameters: ChalinResumeParams,
|
|
347
|
+
async execute(_toolCallId, params: ChalinResumeToolParams, signal, onUpdate, ctx) {
|
|
348
|
+
const loaded = loadEffectiveConfig({ cwd: ctx.cwd });
|
|
349
|
+
const run = loadResumableRunState({ cwd: ctx.cwd, runId: params.runId });
|
|
350
|
+
if (!run) return textResult(params.runId ? `No resumable pi-chalin run found for '${params.runId}'.` : "No paused or stale pi-chalin run found to resume.", { runId: params.runId });
|
|
351
|
+
const catalog = AgentCatalog.load({ cwd: ctx.cwd });
|
|
352
|
+
const memory = new MemoryStore({ cwd: ctx.cwd });
|
|
353
|
+
const kernel = new ChalinKernel({
|
|
354
|
+
cwd: ctx.cwd,
|
|
355
|
+
config: loaded.config,
|
|
356
|
+
catalog,
|
|
357
|
+
memory,
|
|
358
|
+
modelOverrides: mergedSessionModelOverrides(loaded.config.agents.modelOverrides),
|
|
359
|
+
thinkingOverrides: mergedSessionThinkingOverrides(loaded.config.agents.thinkingOverrides),
|
|
360
|
+
});
|
|
361
|
+
clearLegacyChalinControlWidget(ctx);
|
|
362
|
+
const abortSignal = signal ?? new AbortController().signal;
|
|
363
|
+
setChalinStatus(ctx, {
|
|
364
|
+
kind: "running",
|
|
365
|
+
intent: routeIntent(run.route),
|
|
366
|
+
agent: run.steps.find((step) => !isUsableStepStatus(step.status))?.agent ?? run.route.agents[0] ?? "chalin",
|
|
367
|
+
completed: run.steps.filter((step) => isUsableStepStatus(step.status)).length,
|
|
368
|
+
total: Math.max(run.steps.length, 1),
|
|
369
|
+
});
|
|
370
|
+
const result = await kernel.resumeRun(run, {
|
|
371
|
+
cwd: ctx.cwd,
|
|
372
|
+
extensionContext: ctx,
|
|
373
|
+
signal: abortSignal,
|
|
374
|
+
onUpdate: (updated) => {
|
|
375
|
+
setLatestRun(updated);
|
|
376
|
+
setChalinStatus(ctx, footerStateForRun(updated));
|
|
377
|
+
onUpdate?.({
|
|
378
|
+
content: [{ type: "text", text: formatChalinRunWidget(updated) }],
|
|
379
|
+
details: chalinRouteUpdateDetails(updated),
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
if (result.run) setLatestRun(result.run);
|
|
384
|
+
if (result.run?.status === "paused" || abortSignal.aborted) setChalinStatus(ctx, { kind: "stopped" });
|
|
385
|
+
else if (result.run?.status === "failed") setChalinStatus(ctx, { kind: "failed" });
|
|
386
|
+
else setChalinStatus(ctx, { kind: "complete", intent: routeIntent(result.route) });
|
|
387
|
+
return finalToolResult(ctx, formatRoute(result.route, result), compactRouteDetails(result.route, result, [...loaded.diagnostics, catalog.diagnostics]));
|
|
388
|
+
},
|
|
389
|
+
renderCall(args, theme) {
|
|
390
|
+
void args;
|
|
391
|
+
void theme;
|
|
392
|
+
return new Text("", 0, 0);
|
|
393
|
+
},
|
|
394
|
+
renderResult(result, _options, theme) {
|
|
395
|
+
const details = result.details as ChalinRouteWidgetDetails | undefined;
|
|
396
|
+
const rendered = details?.run ? formatChalinRunWidgetFromDetails(details) : result.content.find((part) => part.type === "text")?.text ?? "";
|
|
397
|
+
return new Text(colorizeChalinWidget(rendered, theme), 0, 0);
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
pi.registerTool({
|
|
403
|
+
name: "chalin_memory_search",
|
|
404
|
+
label: "Chalin Memory Search",
|
|
405
|
+
description: "Search compact durable pi-chalin memory from the primary Pi agent, including direct-mode work. Use without waiting for an explicit memory request when prior decisions, project facts, workflows, or preferences can reduce rediscovery.",
|
|
406
|
+
promptSnippet: "chalin_memory_search: recall compact durable memory during direct or routed work when prior context may help.",
|
|
407
|
+
promptGuidelines: [
|
|
408
|
+
"Use this proactively for repeated project conventions, prior decisions, user preferences, workflows, and suspected stale assumptions.",
|
|
409
|
+
"Keep queries short and tokenBudget small. Current repository evidence and explicit user instructions override memory.",
|
|
410
|
+
"Ask for evidence only when checking contradictions, reviewing memory, or deciding whether to revise a memory.",
|
|
411
|
+
],
|
|
412
|
+
parameters: ChalinMemorySearchParams,
|
|
413
|
+
async execute(_toolCallId, params: ChalinMemorySearchToolParams, _signal, _onUpdate, ctx) {
|
|
414
|
+
const memory = new MemoryStore({ cwd: ctx.cwd });
|
|
415
|
+
const bundle = await memory.retrieve({
|
|
416
|
+
query: params.query,
|
|
417
|
+
sourceAgent: "primary-pi",
|
|
418
|
+
limit: clampInteger(params.limit ?? 6, 1, 10),
|
|
419
|
+
tokenBudget: clampInteger(params.tokenBudget ?? 700, 80, 1600),
|
|
420
|
+
includeEvidence: Boolean(params.includeEvidence),
|
|
421
|
+
});
|
|
422
|
+
return textResult(bundle.text || "No memory matches.", bundle);
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
pi.registerTool({
|
|
427
|
+
name: "chalin_memory_write",
|
|
428
|
+
label: "Chalin Memory Write",
|
|
429
|
+
description: "Save compact durable project or user knowledge from the primary Pi agent. The MemoryStore WriteGuard decides active, pending, duplicate, revised, or rejected.",
|
|
430
|
+
promptSnippet: "chalin_memory_write: save durable verified knowledge discovered during direct or routed work.",
|
|
431
|
+
promptGuidelines: [
|
|
432
|
+
"Use this for durable project facts, decisions, workflows, user preferences, and lessons that should reduce future rediscovery.",
|
|
433
|
+
"Do not write logs, command output, code dumps, transient task completion notes, or facts that are not backed by evidence.",
|
|
434
|
+
"Prefer one compact sentence with evidence over multiple broad memories.",
|
|
435
|
+
],
|
|
436
|
+
parameters: ChalinMemoryWriteParams,
|
|
437
|
+
async execute(_toolCallId, params: ChalinMemoryWriteToolParams, _signal, _onUpdate, ctx) {
|
|
438
|
+
const content = params.content.trim();
|
|
439
|
+
if (content.length < 24) return textResult("memory rejected: content is too short to be durable.", { status: "rejected" });
|
|
440
|
+
const memory = new MemoryStore({ cwd: ctx.cwd });
|
|
441
|
+
const [record] = await memory.submitCandidates([createMemoryCandidate({
|
|
442
|
+
category: params.category,
|
|
443
|
+
content,
|
|
444
|
+
sourceAgent: "primary-pi",
|
|
445
|
+
confidence: clampNumber(params.confidence ?? 0.85, 0, 1),
|
|
446
|
+
evidence: params.evidence,
|
|
447
|
+
topicKey: params.topicKey,
|
|
448
|
+
scope: "project",
|
|
449
|
+
})]);
|
|
450
|
+
if (!record) return textResult("memory rejected: no durable candidate was produced.", { status: "rejected" });
|
|
451
|
+
return textResult(`memory ${record.status}: ${record.id}`, { record });
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
pi.registerTool({
|
|
456
|
+
name: "chalin_memory_revise",
|
|
457
|
+
label: "Chalin Memory Revise",
|
|
458
|
+
description: "Correct or replace an existing pi-chalin memory when current evidence proves it stale, wrong, or weaker than the new formulation.",
|
|
459
|
+
promptSnippet: "chalin_memory_revise: repair stale or incorrect durable memory with evidence.",
|
|
460
|
+
promptGuidelines: [
|
|
461
|
+
"Use this when retrieved memory contradicts repository evidence or a newer instruction is clearly better.",
|
|
462
|
+
"Always include concise evidence and a reason. Keep the revised memory compact.",
|
|
463
|
+
"Do not revise memory just to restyle wording unless utility or correctness improves.",
|
|
464
|
+
],
|
|
465
|
+
parameters: ChalinMemoryReviseParams,
|
|
466
|
+
async execute(_toolCallId, params: ChalinMemoryReviseToolParams, _signal, _onUpdate, ctx) {
|
|
467
|
+
const content = params.content.trim();
|
|
468
|
+
if (content.length < 24) return textResult("memory revision rejected: content is too short to be durable.", { status: "rejected" });
|
|
469
|
+
const memory = new MemoryStore({ cwd: ctx.cwd });
|
|
470
|
+
const record = await memory.revise(params.id, {
|
|
471
|
+
content,
|
|
472
|
+
category: params.category,
|
|
473
|
+
sourceAgent: "primary-pi",
|
|
474
|
+
confidence: clampNumber(params.confidence ?? 0.9, 0, 1),
|
|
475
|
+
evidence: params.evidence,
|
|
476
|
+
reason: params.reason,
|
|
477
|
+
});
|
|
478
|
+
if (!record) return textResult(`memory revision failed: ${params.id} was not found.`, { status: "missing", id: params.id });
|
|
479
|
+
return textResult(`memory revised: ${record.id}`, { record });
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
pi.registerTool({
|
|
484
|
+
name: "chalin_artifact_resume",
|
|
485
|
+
label: "Chalin Artifact Resume",
|
|
486
|
+
description: "Load compact resumable pi-chalin artifact context for a long-running feature/task.",
|
|
487
|
+
promptSnippet: "chalin_artifact_resume: load prior checkpoints, validation contracts, and worker skills for a long-running chalin task.",
|
|
488
|
+
promptGuidelines: [
|
|
489
|
+
"Use this before continuing a long-running or previously interrupted pi-chalin task.",
|
|
490
|
+
"Use the returned checkpoints and validation contracts as the source of truth for continuation.",
|
|
491
|
+
],
|
|
492
|
+
parameters: ChalinArtifactResumeParams,
|
|
493
|
+
async execute(_toolCallId, params: ChalinArtifactResumeToolParams, _signal, _onUpdate, ctx) {
|
|
494
|
+
const artifacts = new ArtifactStore({ cwd: ctx.cwd });
|
|
495
|
+
const text = await artifacts.resumeContext(params.featureId);
|
|
496
|
+
return textResult(text, { featureId: params.featureId });
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
pi.registerTool({
|
|
501
|
+
name: "chalin_web_search",
|
|
502
|
+
label: "Chalin Web Search",
|
|
503
|
+
description: "Search or fetch web context through Exa MCP. Use only when current external information, documentation, or a URL is required; returns compact sources, not a raw dump.",
|
|
504
|
+
promptSnippet: "chalin_web_search: search/fetch current web context through Exa MCP with compact citations.",
|
|
505
|
+
promptGuidelines: [
|
|
506
|
+
"Use chalin_web_search for current docs, recent facts, URLs, or external verification; do not use it for local repo facts.",
|
|
507
|
+
"Prefer maxSources 3-5 and snippets unless the user explicitly needs deeper content.",
|
|
508
|
+
"Cite source URLs from the tool result in your answer.",
|
|
509
|
+
],
|
|
510
|
+
parameters: ChalinWebSearchParams,
|
|
511
|
+
async execute(_toolCallId, params: ChalinWebSearchToolParams, signal, onUpdate, ctx) {
|
|
512
|
+
const urls = [...(params.urls ?? []), ...(params.url ? [params.url] : [])].filter(Boolean);
|
|
513
|
+
const label = urls.length > 0 ? `fetching ${urls.length} URL${urls.length === 1 ? "" : "s"}` : `searching ${params.query ?? "web"}`;
|
|
514
|
+
onUpdate?.({ content: [{ type: "text", text: `chalin web · ${label} via Exa MCP…` }], details: { status: "running", provider: "exa-mcp" } });
|
|
515
|
+
const bundle = urls.length > 0
|
|
516
|
+
? await fetchWebUrls({ cwd: ctx.cwd, urls, freshness: params.freshness, signal })
|
|
517
|
+
: await searchWeb({ cwd: ctx.cwd, query: params.query ?? "", maxSources: params.maxSources, depth: params.depth, freshness: params.freshness, signal });
|
|
518
|
+
return textResult(formatWebBundle(bundle), bundle);
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function clampInteger(value: number, min: number, max: number): number {
|
|
524
|
+
const parsed = Math.floor(Number(value));
|
|
525
|
+
if (!Number.isFinite(parsed)) return min;
|
|
526
|
+
return Math.min(max, Math.max(min, parsed));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
530
|
+
const parsed = Number(value);
|
|
531
|
+
if (!Number.isFinite(parsed)) return min;
|
|
532
|
+
return Math.min(max, Math.max(min, parsed));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function textResult(text: string, details: unknown) {
|
|
536
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function finalToolResult(ctx: { hasUI: boolean; abort(): void; shutdown(): void }, text: string, details: unknown) {
|
|
540
|
+
scheduleNonInteractiveShutdown(ctx);
|
|
541
|
+
return textResult(text, details);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function scheduleNonInteractiveShutdown(ctx: { hasUI: boolean; abort(): void; shutdown(): void }): void {
|
|
545
|
+
if (ctx.hasUI || process.env.PI_CHALIN_NONINTERACTIVE_SHUTDOWN === "0") return;
|
|
546
|
+
const configuredDelay = Number(process.env.PI_CHALIN_NONINTERACTIVE_SHUTDOWN_DELAY_MS);
|
|
547
|
+
const delayMs = Number.isFinite(configuredDelay) && configuredDelay >= 0 ? configuredDelay : 0;
|
|
548
|
+
const timer = setTimeout(() => {
|
|
549
|
+
try {
|
|
550
|
+
ctx.abort();
|
|
551
|
+
ctx.shutdown();
|
|
552
|
+
} catch {
|
|
553
|
+
// Pi can mark extension contexts stale while a print-mode turn exits.
|
|
554
|
+
// The tool result has already been emitted, so stale shutdown is safe to ignore.
|
|
555
|
+
}
|
|
556
|
+
}, delayMs);
|
|
557
|
+
timer.unref?.();
|
|
558
|
+
}
|