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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { ChalinFooterState } from "./ui-status.ts";
|
|
2
|
+
import type { RouteDecision, RunState, RunStatus } from "./schemas.ts";
|
|
3
|
+
|
|
4
|
+
type ChalinRouteWidgetStep = {
|
|
5
|
+
id?: string;
|
|
6
|
+
agent: string;
|
|
7
|
+
task?: string;
|
|
8
|
+
status?: RunStatus;
|
|
9
|
+
model?: string;
|
|
10
|
+
thinkingLevel?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
handoff?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ChalinRouteWidgetDetails = {
|
|
16
|
+
route?: RouteDecision;
|
|
17
|
+
run?: {
|
|
18
|
+
id: string;
|
|
19
|
+
status: RunStatus;
|
|
20
|
+
steps: ChalinRouteWidgetStep[];
|
|
21
|
+
metrics?: RunState["metrics"];
|
|
22
|
+
warnings?: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ChalinRouteToolParams = {
|
|
27
|
+
task: string;
|
|
28
|
+
topology: "single" | "chain" | "parallel" | "dag" | "memory-only";
|
|
29
|
+
steps?: Array<{ id?: string; agent: string; task: string; budget?: "tight" | "normal" | "deep" | "extended" }>;
|
|
30
|
+
stages?: Array<{ id?: string; name?: string; tasks: Array<{ id?: string; agent: string; task: string; budget?: "tight" | "normal" | "deep" | "extended" }> }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function formatChalinRoutePlanWidget(params: ChalinRouteToolParams): string {
|
|
34
|
+
const steps = plannedWidgetSteps(params);
|
|
35
|
+
const title = routeTitle(params.topology, params.task);
|
|
36
|
+
const agents = steps.map((step) => step.agent).filter(Boolean);
|
|
37
|
+
return [
|
|
38
|
+
`pi-chalin · ${title}`,
|
|
39
|
+
agents.length ? `agents: ${compactAgentPath(agents)} · 0/${steps.length || 1}` : "agents: memory · 0/1",
|
|
40
|
+
...steps.slice(0, 8).map((step, index) => `${treePrefix(index, steps.length)} ${statusGlyph(step.status ?? "pending")} ${step.agent} — ${truncate(step.task ?? "waiting", 76)}`),
|
|
41
|
+
steps.length > 8 ? `└ … +${steps.length - 8} more` : undefined,
|
|
42
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatChalinRunWidget(run: RunState): string {
|
|
46
|
+
return formatChalinRunWidgetFromDetails(chalinRouteUpdateDetails(run));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatChalinRunWidgetFromDetails(details: ChalinRouteWidgetDetails): string {
|
|
50
|
+
const run = details.run;
|
|
51
|
+
if (!run) return "pi-chalin · no run";
|
|
52
|
+
const route = details.route;
|
|
53
|
+
const steps = run.steps;
|
|
54
|
+
const completed = steps.filter((step) => isUsableStepStatus(step.status)).length;
|
|
55
|
+
const active = activeWidgetStep(run.status, steps);
|
|
56
|
+
const title = route ? routeIntent(route) : "workflow";
|
|
57
|
+
const displayStatus = run.status === "budget-capped" && completed === (steps.length || 1) ? "done" : statusLabel(run.status);
|
|
58
|
+
const activeLabel = run.status === "failed" ? "blocked" : "current";
|
|
59
|
+
return [
|
|
60
|
+
`pi-chalin · ${title} · ${displayStatus} · ${completed}/${steps.length || 1}`,
|
|
61
|
+
active ? `${activeLabel}: ${active.agent} — ${truncate(active.error ?? active.task ?? statusLabel(active.status ?? "pending"), 86)}` : undefined,
|
|
62
|
+
...steps.slice(0, 8).map((step, index) => formatWidgetStep(step, index, steps.length, run.status)),
|
|
63
|
+
steps.length > 8 ? `└ … +${steps.length - 8} more` : undefined,
|
|
64
|
+
formatWidgetGuards(run.metrics),
|
|
65
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function activeWidgetStep(runStatus: RunStatus, steps: ChalinRouteWidgetStep[]): ChalinRouteWidgetStep | undefined {
|
|
69
|
+
if (runStatus === "failed") return steps.find((step) => step.status === "failed");
|
|
70
|
+
if (runStatus === "running" || runStatus === "pending") {
|
|
71
|
+
return steps.find((step) => step.status === "running") ?? steps.find((step) => step.status === "pending");
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function plannedWidgetRun(route: RouteDecision): ChalinRouteWidgetDetails["run"] {
|
|
77
|
+
return {
|
|
78
|
+
id: "planned",
|
|
79
|
+
status: "pending",
|
|
80
|
+
steps: route.plan ? plannedStepsFromRoute(route).map((step) => ({ ...step, status: "pending" })) : [],
|
|
81
|
+
warnings: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function chalinRouteUpdateDetails(run: RunState): ChalinRouteWidgetDetails {
|
|
86
|
+
return {
|
|
87
|
+
route: run.route,
|
|
88
|
+
run: {
|
|
89
|
+
id: run.id,
|
|
90
|
+
status: run.status,
|
|
91
|
+
metrics: run.metrics,
|
|
92
|
+
warnings: run.warnings,
|
|
93
|
+
steps: run.steps.map((step) => ({
|
|
94
|
+
id: step.id,
|
|
95
|
+
agent: step.agent,
|
|
96
|
+
task: step.task,
|
|
97
|
+
status: step.status,
|
|
98
|
+
model: step.model,
|
|
99
|
+
thinkingLevel: step.thinkingLevel,
|
|
100
|
+
error: step.error,
|
|
101
|
+
handoff: truncate(step.output?.handoff || step.output?.text || "", 180),
|
|
102
|
+
})),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function plannedStepsFromRoute(route: RouteDecision): ChalinRouteWidgetStep[] {
|
|
108
|
+
const plan = route.plan;
|
|
109
|
+
if (!plan) return [];
|
|
110
|
+
if (plan.kind === "single") return [{ agent: plan.agent, task: plan.task }];
|
|
111
|
+
if (plan.kind === "chain") return plan.steps;
|
|
112
|
+
if (plan.kind === "parallel") return plan.tasks;
|
|
113
|
+
return plan.stages.flatMap((stage) => stage.tasks.map((step) => ({ ...step, id: `${stage.id}:${step.id ?? step.agent}` })));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function plannedWidgetSteps(params: ChalinRouteToolParams): ChalinRouteWidgetStep[] {
|
|
117
|
+
if (params.topology === "single") {
|
|
118
|
+
const first = params.steps?.[0];
|
|
119
|
+
return first ? [first] : [];
|
|
120
|
+
}
|
|
121
|
+
if (params.topology === "chain" || params.topology === "parallel") return params.steps ?? [];
|
|
122
|
+
if (params.topology === "dag") return params.stages?.flatMap((stage) => stage.tasks.map((step) => ({ ...step, id: `${stage.id ?? "stage"}:${step.id ?? step.agent}` }))) ?? [];
|
|
123
|
+
return [{ agent: "memory", task: params.task, status: "pending" }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatWidgetStep(step: ChalinRouteWidgetStep, index: number, total: number, runStatus?: RunStatus): string {
|
|
127
|
+
const detail = step.status === "complete"
|
|
128
|
+
? step.handoff || "done"
|
|
129
|
+
: step.status === "failed"
|
|
130
|
+
? step.error || "failed"
|
|
131
|
+
: step.status === "paused"
|
|
132
|
+
? step.error || "paused"
|
|
133
|
+
: step.status === "budget-capped"
|
|
134
|
+
? step.handoff || step.error || step.task || "checkpoint saved"
|
|
135
|
+
: step.status === "pending" && runStatus === "failed"
|
|
136
|
+
? "skipped after failure"
|
|
137
|
+
: step.task || "working";
|
|
138
|
+
const suffix = step.status === "budget-capped" ? " · budget limit reached" : "";
|
|
139
|
+
return `${treePrefix(index, total)} ${statusGlyph(step.status)} ${step.agent} — ${truncate(detail, 88)}${suffix}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function statusGlyph(status: RunStatus | undefined): string {
|
|
143
|
+
if (status === "complete" || status === "budget-capped") return "✓";
|
|
144
|
+
if (status === "running") return "◆";
|
|
145
|
+
if (status === "failed") return "×";
|
|
146
|
+
if (status === "paused") return "■";
|
|
147
|
+
return "○";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function statusLabel(status: RunStatus): string {
|
|
151
|
+
if (status === "complete") return "done";
|
|
152
|
+
if (status === "failed") return "failed";
|
|
153
|
+
if (status === "paused") return "paused";
|
|
154
|
+
if (status === "budget-capped") return "checkpointed";
|
|
155
|
+
if (status === "running") return "running";
|
|
156
|
+
return "pending";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isUsableStepStatus(status: RunStatus | undefined): boolean {
|
|
160
|
+
return status === "complete" || status === "budget-capped";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function treePrefix(index: number, total: number): string {
|
|
164
|
+
return index === total - 1 ? "└" : "├";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function routeTitle(topology: ChalinRouteToolParams["topology"], task: string): string {
|
|
168
|
+
if (topology === "memory-only") return "memory lookup";
|
|
169
|
+
return `${topology} · ${truncate(task, 52)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function compactAgentPath(agents: string[]): string {
|
|
173
|
+
const compact = agents.slice(0, 5).join(" → ");
|
|
174
|
+
return agents.length > 5 ? `${compact} → +${agents.length - 5}` : compact;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function colorizeChalinWidget(text: string, theme: { fg(scope: string, value: string): string; bold(value: string): string }): string {
|
|
178
|
+
return text.split("\n").map((line, index) => {
|
|
179
|
+
if (index === 0) return theme.fg("toolTitle", theme.bold(line));
|
|
180
|
+
if (/current:/.test(line)) return theme.fg("muted", line);
|
|
181
|
+
if (/✓/.test(line)) return theme.fg("success", line);
|
|
182
|
+
if (/×|failed|attention/.test(line)) return theme.fg("error", line);
|
|
183
|
+
if (/budget: limit reached/.test(line)) return theme.fg("warning", line);
|
|
184
|
+
if (/◆/.test(line)) return theme.fg("accent", line);
|
|
185
|
+
return theme.fg("dim", line);
|
|
186
|
+
}).join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatWidgetGuards(metrics: RunState["metrics"] | undefined): string {
|
|
190
|
+
if (!metrics) return "tools: 0 · guards: checking";
|
|
191
|
+
const policyViolations = metrics.policyViolations?.length ?? 0;
|
|
192
|
+
const budgetStops = metrics.budgetStopCount ?? 0;
|
|
193
|
+
if (policyViolations > 0) return `tools: ${metrics.toolCalls} · guards: attention · ${policyViolations} policy`;
|
|
194
|
+
if (budgetStops > 0) return `tools: ${metrics.toolCalls} · guards: ok · budget: limit reached (${budgetStops} stops)`;
|
|
195
|
+
return `tools: ${metrics.toolCalls} · guards: ok`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function footerStateForRun(run: RunState): ChalinFooterState {
|
|
199
|
+
const active = run.steps.find((step) => step.status === "running") ?? run.steps.find((step) => step.status === "pending");
|
|
200
|
+
const completed = run.steps.filter((step) => isUsableStepStatus(step.status)).length;
|
|
201
|
+
const total = run.steps.length || 1;
|
|
202
|
+
if (run.status === "complete") return { kind: "complete", intent: routeIntent(run.route) };
|
|
203
|
+
if (run.status === "paused") return { kind: "stopped" };
|
|
204
|
+
if (run.status === "failed") return { kind: "failed" };
|
|
205
|
+
return { kind: "running", intent: routeIntent(run.route), agent: active?.agent ?? run.route.agents[0] ?? run.status, completed, total };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function routeIntent(route: RouteDecision): string {
|
|
209
|
+
if (route.kind === "memory-only") return "memory lookup";
|
|
210
|
+
if (route.agents.includes("worker")) return "implement safely";
|
|
211
|
+
if (route.agents.includes("reviewer")) return "review";
|
|
212
|
+
if (route.agents.includes("context-builder")) return "understand";
|
|
213
|
+
if (route.agents.includes("planner")) return "plan";
|
|
214
|
+
return route.agents[0] ?? route.kind;
|
|
215
|
+
}
|
|
216
|
+
function truncate(text: string, max: number): string {
|
|
217
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
218
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}…`;
|
|
219
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { AgentCapability, AgentDefinition, RouteKind, ToolBudgetProfile } from "./schemas.ts";
|
|
2
|
+
import { policyForStep } from "./budget.ts";
|
|
3
|
+
import { buildProjectDiscoveryIndex, formatProjectDiscoveryIndex } from "./discovery.ts";
|
|
4
|
+
import type { RunStepState } from "./schemas.ts";
|
|
5
|
+
|
|
6
|
+
export interface SdkPromptOptions {
|
|
7
|
+
priorFilesRead?: string[];
|
|
8
|
+
synthesisGapReadLimit?: number;
|
|
9
|
+
memoryContext?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildSdkPrompt(
|
|
13
|
+
agent: AgentDefinition | undefined,
|
|
14
|
+
task: string,
|
|
15
|
+
cwd: string,
|
|
16
|
+
previous?: string,
|
|
17
|
+
budget: ReturnType<typeof policyForStep> | number = toolBudgetForAgent(agent),
|
|
18
|
+
budgetProfile: ToolBudgetProfile = "normal",
|
|
19
|
+
options: SdkPromptOptions = {},
|
|
20
|
+
): string {
|
|
21
|
+
const discoveryIndex = formatProjectDiscoveryIndex(buildProjectDiscoveryIndex(cwd));
|
|
22
|
+
const capabilities = agent?.capabilities ?? [];
|
|
23
|
+
const deepProjectAnalysis = isDeepProjectAnalysisTask(task) || budgetProfile === "deep" || (typeof budget !== "number" && budget.profile === "deep");
|
|
24
|
+
const handoffGapMode = isHandoffGapReadMode(agent, task, previous);
|
|
25
|
+
const budgetPolicy = typeof budget === "number"
|
|
26
|
+
? policyForStep(agent, { agent: agent?.name ?? "agent", task, budget: budgetProfile }, "single-agent")
|
|
27
|
+
: budget;
|
|
28
|
+
const maxTools = typeof budget === "number" ? budget : budget.caps.maxToolCalls;
|
|
29
|
+
const profile = typeof budget === "number" ? budgetProfile : budget.profile;
|
|
30
|
+
return [
|
|
31
|
+
compactAgentInstructions(agent),
|
|
32
|
+
"",
|
|
33
|
+
"## pi-chalin concern/capability policy",
|
|
34
|
+
`- Concern: ${agent?.concern ?? "delegation"}.`,
|
|
35
|
+
`- Capabilities: ${capabilities.join(", ") || "inspect-files, search-files"}.`,
|
|
36
|
+
"- Runtime tools are derived from capabilities; do not assume a tool exists because another agent has it.",
|
|
37
|
+
"",
|
|
38
|
+
"## pi-chalin child tool policy",
|
|
39
|
+
"- Use Pi-native tools directly: read/find/grep/ls for inspection, edit for minimal line-level changes.",
|
|
40
|
+
"- Use chalin_project_discovery first for broad project understanding. It is a raw file index, not semantic truth; read evidence files before making claims.",
|
|
41
|
+
"- Use chalin_project_snapshot only as legacy compact stack/git context or for branch-summary reconnaissance; never treat it as proof of architecture.",
|
|
42
|
+
"- Bash is guarded and only for safe inspection or explicit validation commands: git status/log/diff/show/rev-parse, pwd, ls, find, grep/rg, cat for one explicit small file, and known test/typecheck commands.",
|
|
43
|
+
"- Never create temporary Python/Node/shell scripts to read, inspect, summarize, or modify project files.",
|
|
44
|
+
"- Never modify files through bash. No redirection, tee, sed -i, rm/cp/mv/mkdir/touch/chmod, or generated scripts.",
|
|
45
|
+
"- For existing files, never rewrite the whole file when a targeted edit is possible. Use edit with the smallest exact old/new block. Use write only for new files.",
|
|
46
|
+
"",
|
|
47
|
+
"## pi-chalin runtime budget",
|
|
48
|
+
`- Tool budget profile: ${profile}. Max tool calls for this child turn: ${maxTools}.`,
|
|
49
|
+
`- Budget caps: ${budgetPolicy.caps.maxSeconds}s, $${budgetPolicy.caps.maxUsd}, ${budgetPolicy.caps.maxTurns} turns, ${budgetPolicy.caps.maxOutputChars} output chars, ${budgetPolicy.caps.maxReadBytes} read bytes, ${budgetPolicy.caps.maxFilesTouched} files touched, ${budgetPolicy.caps.maxRetriesPerTool} retries/tool.`,
|
|
50
|
+
"- Stay bounded. Do not perform an exhaustive repository crawl unless the task explicitly requires it.",
|
|
51
|
+
agent?.concern === "recon" || deepProjectAnalysis
|
|
52
|
+
? "- AGENTS/JIT-first: when the discovery index lists AGENTS.md, CONTEXT.md, ADRs, or package instruction files, read the root instructions first and then only the package instruction files relevant to the task before broad source reads."
|
|
53
|
+
: undefined,
|
|
54
|
+
profile === "tight"
|
|
55
|
+
? "- Tight profile: use the discovery index first, then inspect only the smallest evidence set needed to answer."
|
|
56
|
+
: profile === "deep" || profile === "extended"
|
|
57
|
+
? "- Deep/autonomous profile: use the discovery index first, formulate an inspection plan, then inspect breadth-first with compact notes; checkpoint/compress at stage boundaries instead of exhaustive context stuffing."
|
|
58
|
+
: "- Normal profile: use the discovery index first, then inspect the evidence files needed; avoid exhaustive crawls unless the task requires it.",
|
|
59
|
+
"- Prefer concise findings with evidence. Stop after the highest-value actionable issues; do not spend budget proving low-value metadata already present in the snapshot.",
|
|
60
|
+
`- Use at most ${maxTools} tool calls for this role. If you hit the budget, stop and report partial findings plus uncertainty.`,
|
|
61
|
+
"- If you hit any budget cap, treat it as a checkpoint boundary, not a failure: return partial handoff, uncertainty, and the next split/continue recommendation.",
|
|
62
|
+
"- For hours/days-long autonomous work, do not try to solve everything inside one child turn. Write artifacts/checkpoints, return a handoff, and let the orchestrator continue with another bounded stage.",
|
|
63
|
+
"- Do not browse the web unless this agent role and task explicitly request fresh external context.",
|
|
64
|
+
deepProjectAnalysis
|
|
65
|
+
? "- Output budget for deep analysis: `## Findings` max 10 evidence-backed bullets, `## Handoff` max 14 bullets or 2600 characters, `## Memory Candidates` max 3 bullets. Accuracy beats brevity; do not pad."
|
|
66
|
+
: "- Output budget: `## Findings` max 5 bullets, `## Handoff` max 8 bullets or 1200 characters, `## Memory Candidates` max 3 bullets.",
|
|
67
|
+
"- For long-running work, use chalin_artifact_write only at meaningful boundaries: feature-state at start, checkpoint after a completed handoff, validation-contract before reviewer/worker handoff, worker-skill for reusable feature-specific rules.",
|
|
68
|
+
memoryPolicyForAgent(agent),
|
|
69
|
+
"- Do not paste raw command output or long code snippets. Cite file paths and line-level evidence when useful.",
|
|
70
|
+
deepProjectAnalysis ? deepProjectAnalysisContract() : undefined,
|
|
71
|
+
handoffGapMode ? handoffGapReadContract(options, agent) : undefined,
|
|
72
|
+
"",
|
|
73
|
+
"## Stop conditions",
|
|
74
|
+
stopConditionsForAgent(agent, task),
|
|
75
|
+
"",
|
|
76
|
+
previous ? "## Previous Handoff" : undefined,
|
|
77
|
+
previous || undefined,
|
|
78
|
+
previous ? "" : undefined,
|
|
79
|
+
previous && options.priorFilesRead?.length ? "## Already Covered Evidence Paths" : undefined,
|
|
80
|
+
previous && options.priorFilesRead?.length ? formatPriorFilesRead(options.priorFilesRead) : undefined,
|
|
81
|
+
previous && options.priorFilesRead?.length ? "" : undefined,
|
|
82
|
+
options.memoryContext ? "## Compact Memory Context" : undefined,
|
|
83
|
+
options.memoryContext || undefined,
|
|
84
|
+
options.memoryContext ? "" : undefined,
|
|
85
|
+
"## Task",
|
|
86
|
+
task,
|
|
87
|
+
"",
|
|
88
|
+
"## Cached Project Discovery Index",
|
|
89
|
+
previous ? "Discovery index omitted because Previous Handoff is available. Call chalin_project_discovery only if the handoff lacks required repo facts." : discoveryIndex,
|
|
90
|
+
"",
|
|
91
|
+
"Return a concise result with these sections when useful:",
|
|
92
|
+
"## Findings",
|
|
93
|
+
deepProjectAnalysis
|
|
94
|
+
? "- Evidence-backed discoveries that the orchestrator should show the user. Include claim + evidence; do not merge unsupported guesses."
|
|
95
|
+
: "- Evidence-backed discoveries that the orchestrator should show the user. Max 5 bullets.",
|
|
96
|
+
"## Handoff",
|
|
97
|
+
deepProjectAnalysis
|
|
98
|
+
? "- Preserve the Coverage Matrix, Evidence Table, Unknowns/Gaps, and final synthesis material. Do not drop domain-critical subsystems."
|
|
99
|
+
: "- A compact summary for the next agent or the orchestrator. Max 8 bullets or 1200 characters.",
|
|
100
|
+
"## Memory Candidates",
|
|
101
|
+
"- Only durable, human-readable project knowledge that will help future work.",
|
|
102
|
+
"- Max 3 bullets.",
|
|
103
|
+
"- Use 1-3 complete sentences per bullet. Prefer categories like `project-fact:`, `pattern:`, `tooling:`, `testing:`, `workflow:`, `bugfix:`, `decision:`, or `preference:`.",
|
|
104
|
+
"- Good: `tooling: This project uses Bun for tests, and tests should avoid setTimeout-based waits because they are flaky.`",
|
|
105
|
+
"- Good: `workflow: Long-running feature work should checkpoint validation contracts after each stage so later agents can resume safely.`",
|
|
106
|
+
"- Bad: commands, logs, code snippets, raw stdout/stderr, stack traces, task completion notes, or obvious one-line facts.",
|
|
107
|
+
"- Write `- None.` when there is nothing worth remembering.",
|
|
108
|
+
].filter(Boolean).join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function childToolNames(agent: AgentDefinition | undefined, task = "", needsArtifacts = false, hasPrevious = false): string[] {
|
|
112
|
+
if (hasPrevious && shouldUseHandoffOnlyMode(task, agent)) return taskNeedsArtifactWrite(task) && needsArtifacts ? ["chalin_artifact_write"] : [];
|
|
113
|
+
if (isSnapshotOnlyRecon(task, agent)) return ["chalin_project_discovery", "chalin_project_snapshot"];
|
|
114
|
+
if (!agent?.capabilities.length) {
|
|
115
|
+
const fallback = new Set(agent?.tools.length ? agent.tools : ["read", "grep", "find", "ls"]);
|
|
116
|
+
fallback.add("chalin_project_discovery");
|
|
117
|
+
return [...fallback];
|
|
118
|
+
}
|
|
119
|
+
const names = new Set<string>();
|
|
120
|
+
if (hasAnyCapability(agent, ["inspect-files"])) {
|
|
121
|
+
names.add("read");
|
|
122
|
+
names.add("ls");
|
|
123
|
+
}
|
|
124
|
+
if (hasAnyCapability(agent, ["search-files"])) {
|
|
125
|
+
names.add("grep");
|
|
126
|
+
names.add("find");
|
|
127
|
+
}
|
|
128
|
+
if (hasAnyCapability(agent, ["run-safe-bash", "validate"]) && taskNeedsBash(task, agent)) names.add("bash");
|
|
129
|
+
if (hasAnyCapability(agent, ["edit-files"])) names.add("edit");
|
|
130
|
+
if (hasAnyCapability(agent, ["write-new-files"])) names.add("write");
|
|
131
|
+
if (hasAnyCapability(agent, ["external-context"]) && taskNeedsExternalContext(task, agent)) names.add("chalin_web_search");
|
|
132
|
+
if (needsArtifacts && taskNeedsArtifactWrite(task) && hasAnyCapability(agent, ["memory-write", "coordinate", "validate", "edit-files"])) names.add("chalin_artifact_write");
|
|
133
|
+
if (agent?.memory.read !== false && hasAnyCapability(agent, ["memory-read"])) names.add("chalin_memory_search");
|
|
134
|
+
if (agent?.memory.write !== "never" && hasAnyCapability(agent, ["memory-write"])) {
|
|
135
|
+
names.add("chalin_memory_write");
|
|
136
|
+
names.add("chalin_memory_revise");
|
|
137
|
+
}
|
|
138
|
+
names.add("chalin_project_discovery");
|
|
139
|
+
return [...names];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function toolBudgetForStep(agent: AgentDefinition | undefined, step: Pick<RunStepState, "agent" | "task" | "budget">, routeKind: RouteKind = "single-agent"): number {
|
|
143
|
+
const env = Number(process.env.PI_CHALIN_CHILD_TOOL_BUDGET);
|
|
144
|
+
if (Number.isFinite(env) && env > 0) return Math.floor(env);
|
|
145
|
+
return policyForStep(agent, step, routeKind).caps.maxToolCalls;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveStepCompletionStatus(step: Pick<RunStepState, "metrics" | "output" | "error">): RunStepState["status"] {
|
|
149
|
+
if (!step.metrics?.budgetStopCount) return "complete";
|
|
150
|
+
if (hasUsableHandoff(step)) return "complete";
|
|
151
|
+
return "budget-capped";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isHandoffGapReadMode(agent: AgentDefinition | undefined, task: string, previous?: string): boolean {
|
|
155
|
+
return isSynthesisGapReadMode(agent, task, previous) || isReviewGapReadMode(agent, task, previous);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function synthesisToolCallLimit(): number {
|
|
159
|
+
const parsed = Number(process.env.PI_CHALIN_SYNTHESIS_TOOL_LIMIT);
|
|
160
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 35;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function handoffReviewToolCallLimit(): number {
|
|
164
|
+
const parsed = Number(process.env.PI_CHALIN_REVIEW_TOOL_LIMIT);
|
|
165
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 12;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function synthesisGapReadLimit(): number {
|
|
169
|
+
const parsed = Number(process.env.PI_CHALIN_SYNTHESIS_GAP_READ_LIMIT);
|
|
170
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 12;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function synthesisCrossStepDuplicateReadLimit(agent?: AgentDefinition): number {
|
|
174
|
+
const parsed = Number(process.env.PI_CHALIN_SYNTHESIS_CROSS_READ_LIMIT);
|
|
175
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : agent?.concern === "review" ? 1 : 3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function compactAgentInstructions(agent: AgentDefinition | undefined): string | undefined {
|
|
179
|
+
if (!agent) return undefined;
|
|
180
|
+
const rules = extractAgentSection(agent.systemPrompt, "Rules", "Tool discipline")
|
|
181
|
+
.split("\n")
|
|
182
|
+
.map((line) => line.trim())
|
|
183
|
+
.filter((line) => line.startsWith("-"))
|
|
184
|
+
.slice(0, 4);
|
|
185
|
+
return [
|
|
186
|
+
`You are pi-chalin ${agent.name}: ${agent.description}`,
|
|
187
|
+
rules.length ? "Role rules:" : undefined,
|
|
188
|
+
...rules,
|
|
189
|
+
].filter(Boolean).join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function memoryPolicyForAgent(agent: AgentDefinition | undefined): string | undefined {
|
|
193
|
+
if (!agent || (!agent.memory.read && agent.memory.write === "never")) return undefined;
|
|
194
|
+
return [
|
|
195
|
+
"## pi-chalin autonomous memory policy",
|
|
196
|
+
agent.memory.read
|
|
197
|
+
? "- You may use `chalin_memory_search` without waiting for a human instruction when prior decisions, project facts, workflows, or preferences can reduce exploration or prevent repeated mistakes."
|
|
198
|
+
: undefined,
|
|
199
|
+
agent.memory.write !== "never"
|
|
200
|
+
? "- You may use `chalin_memory_write` for compact durable knowledge and `chalin_memory_revise` when current evidence proves a memory stale or wrong. The WriteGuard decides active, pending, or rejected."
|
|
201
|
+
: undefined,
|
|
202
|
+
"- Keep memory token spend low: search with short queries, request evidence only for review/contradiction work, and never write logs, command output, code dumps, or trivial completion notes.",
|
|
203
|
+
"- Memory is guidance, not proof. Current repository evidence and explicit user instructions override retrieved memory.",
|
|
204
|
+
].filter(Boolean).join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function extractAgentSection(text: string, start: string, end: string): string {
|
|
208
|
+
const pattern = new RegExp(`${RegExp.escape(start)}:\\s*([\\s\\S]*?)(?:\\n\\s*${RegExp.escape(end)}:|$)`, "i");
|
|
209
|
+
return pattern.exec(text)?.[1]?.trim() ?? "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isSnapshotOnlyRecon(task: string, agent: AgentDefinition | undefined): boolean {
|
|
213
|
+
return agent?.concern === "recon"
|
|
214
|
+
&& /\b(branch|diff|git state|status|recent commits?|changed files?)\b/i.test(task)
|
|
215
|
+
&& !/\b(implement|fix|edit|security|deep|exact behavior|line-level)\b/i.test(task);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function shouldUseHandoffOnlyMode(task: string, agent: AgentDefinition | undefined): boolean {
|
|
219
|
+
if (!agent || agent.concern === "implementation") return false;
|
|
220
|
+
if (isDeepProjectAnalysisTask(task)) return false;
|
|
221
|
+
const explicitDeepInspection = /\b(exact line|line-level|verify|validate|run tests?|execute tests?|security|correctness|must inspect|full review)\b/i.test(task);
|
|
222
|
+
if (explicitDeepInspection) return false;
|
|
223
|
+
if (agent.concern === "context-building") return /\b(synthesize|summarize|explain|final answer|answer material|consolidate|plain language|package|using scout findings|changed files enough)\b/i.test(task);
|
|
224
|
+
return /\b(synthesize|summarize|explain|final answer|answer material|consolidate|package)\b/i.test(task);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function taskNeedsArtifactWrite(task: string): boolean {
|
|
228
|
+
return /\b(artifact|checkpoint|validation contract|worker skill|resume|continuation|long-running|long running)\b/i.test(task);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function taskNeedsExternalContext(task: string, agent: AgentDefinition): boolean {
|
|
232
|
+
if (agent.concern === "research") return true;
|
|
233
|
+
return /\b(web|internet|online|current|latest|recent|docs?|url|https?:\/\/|exa|source|sources)\b/i.test(task);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function taskNeedsBash(task: string, agent: AgentDefinition): boolean {
|
|
237
|
+
if (agent.concern === "implementation") return true;
|
|
238
|
+
return /\b(test|validate|validation|lint|typecheck|git|branch|diff|commit|status|log)\b/i.test(task);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toolBudgetForAgent(agent: AgentDefinition | undefined, fallbackName?: string): number {
|
|
242
|
+
const env = Number(process.env.PI_CHALIN_CHILD_TOOL_BUDGET);
|
|
243
|
+
if (Number.isFinite(env) && env > 0) return Math.floor(env);
|
|
244
|
+
return baseToolBudget(agent, fallbackName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hasUsableHandoff(step: Pick<RunStepState, "output" | "error">): boolean {
|
|
248
|
+
const text = [step.output?.handoff, step.output?.text].filter(Boolean).join("\n").trim();
|
|
249
|
+
if (text.length < 80) return false;
|
|
250
|
+
if (/^(done|complete|ok|no output)\.?$/i.test(text)) return false;
|
|
251
|
+
return /\b(file|path|module|test|risk|finding|because|uses|contains|should|next|changed|review|implementation|architecture|project)\b/i.test(text);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function baseToolBudget(agent: AgentDefinition | undefined, fallbackName?: string): number {
|
|
255
|
+
if (agent?.concern === "recon") return 40;
|
|
256
|
+
if (agent?.concern === "context-building") return 60;
|
|
257
|
+
if (agent?.concern === "planning") return 25;
|
|
258
|
+
if (agent?.concern === "review") return 50;
|
|
259
|
+
if (agent?.concern === "implementation") return 80;
|
|
260
|
+
if (agent?.concern === "research") return 60;
|
|
261
|
+
if (agent?.concern === "decision-consistency") return 8;
|
|
262
|
+
if (agent?.concern === "conflict-resolution") return 16;
|
|
263
|
+
if (fallbackName === "scout") return 40;
|
|
264
|
+
if (fallbackName === "context-builder") return 60;
|
|
265
|
+
if (fallbackName === "planner") return 25;
|
|
266
|
+
if (fallbackName === "reviewer") return 50;
|
|
267
|
+
if (fallbackName === "worker") return 80;
|
|
268
|
+
return 40;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function stopConditionsForAgent(agent: AgentDefinition | undefined, task = ""): string {
|
|
272
|
+
if (isDeepProjectAnalysisTask(task) && agent?.concern === "recon") {
|
|
273
|
+
return "- Stop only after producing a coverage map across top-level functional areas: entrypoints, commands/tools/routes, storage/sync, integrations, UI/cloud surfaces, tests/evals/tooling, and explicit unknowns.";
|
|
274
|
+
}
|
|
275
|
+
if (isDeepProjectAnalysisTask(task) && (agent?.concern === "context-building" || agent?.concern === "review")) {
|
|
276
|
+
return "- Stop only after the Coverage Matrix marks each critical surface as covered with evidence, not present with evidence, or unknown/gap.";
|
|
277
|
+
}
|
|
278
|
+
if (agent?.concern === "recon") return "- Stop once stack signals, test/build commands, entrypoints, changed files, and 3-5 high-signal files are identified.";
|
|
279
|
+
if (agent?.concern === "context-building") return "- Stop once the next agent has enough facts, constraints, relevant paths, and uncertainties to act without re-scanning.";
|
|
280
|
+
if (agent?.concern === "planning") return "- Stop once the plan has ordered phases, likely files, validation, risks, and rollback notes; do not inspect implementation details deeply.";
|
|
281
|
+
if (agent?.concern === "review") return "- Stop after the top 3-5 evidence-backed risks/findings; do not keep searching for marginal issues.";
|
|
282
|
+
if (agent?.concern === "implementation") return "- Stop after the scoped change and nearest validation are complete; do not broaden scope or rewrite unrelated code.";
|
|
283
|
+
if (agent?.concern === "research") return "- Stop after current sourced context is enough; do not browse or fetch beyond the task scope.";
|
|
284
|
+
return "- Stop when the bounded task can be answered with evidence and remaining uncertainty is explicit.";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isDeepProjectAnalysisTask(task: string): boolean {
|
|
288
|
+
return /\b(deep|thorough|in[- ]depth|profundidad|profundo|profunda|revisa este proyecto|review this project|what (does|is) this project|que hace este proyecto|analiza este (repo|proyecto)|understand this project|project analysis)\b/i.test(task);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function deepProjectAnalysisContract(): string {
|
|
292
|
+
return [
|
|
293
|
+
"",
|
|
294
|
+
"## Deep project analysis accuracy contract",
|
|
295
|
+
"- Optimize for accuracy, not length. A short answer that misses core subsystems is wrong; a long answer without evidence is also wrong.",
|
|
296
|
+
"- Produce a Coverage Matrix before synthesis. Required surfaces: runtime/entrypoints; commands/tools/routes; data/storage/sync; local project detection; external integrations/MCP/tools; HTTP/API routes; UI/dashboard/cloud surfaces; memory/conflict/governance; tests/evals/tooling; known gaps.",
|
|
297
|
+
"- Mark every Coverage Matrix item as one of: covered with evidence, not present with evidence, or unknown/gap. Do not pretend an unknown is absent.",
|
|
298
|
+
"- Produce an Evidence Table using claim + evidence + confidence + gap. Evidence should include file paths and symbol/function/route/config keys when available.",
|
|
299
|
+
"- For memory/agent/orchestration projects, explicitly check: local/project detection, memory persistence and sync, MCP/tool surface, HTTP/API surface, conflict detection/surfacing, external integrations, UI/dashboard/cloud, and test/eval status.",
|
|
300
|
+
"- For command/tool/route surfaces, include representative exact commands, endpoints, and tool names (for example `engram mcp`, `/observations`, or `mem_save`) instead of generic labels only.",
|
|
301
|
+
"- For local-first persistence/sync, state what is the source of truth and name concrete sync artifacts such as manifests/chunks when present.",
|
|
302
|
+
"- Do not merge a claim into final synthesis unless it has evidence or is explicitly labeled as inference.",
|
|
303
|
+
"- Final synthesis must preserve domain-critical subsystems discovered in docs, routes, tools, tests, or config.",
|
|
304
|
+
].join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isSynthesisGapReadMode(agent: AgentDefinition | undefined, task: string, previous?: string): boolean {
|
|
308
|
+
if (!previous?.trim()) return false;
|
|
309
|
+
if (agent?.concern !== "context-building") return false;
|
|
310
|
+
return isDeepProjectAnalysisTask(task) || /\b(synthesize|summarize|explain|final answer|answer material|consolidate|context-builder|a partir del handoff|scout findings|síntesis|sintetiza|resumen)\b/i.test(task);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isReviewGapReadMode(agent: AgentDefinition | undefined, task: string, previous?: string): boolean {
|
|
314
|
+
if (!previous?.trim()) return false;
|
|
315
|
+
if (agent?.concern !== "review") return false;
|
|
316
|
+
return isDeepProjectAnalysisTask(task) || /\b(review|validate|verify|audit|risk|gap|quality|correctness|revisa|verifica|valida|riesgos?|gaps?)\b/i.test(task);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handoffGapReadContract(options: SdkPromptOptions, agent: AgentDefinition | undefined): string {
|
|
320
|
+
const gapReadLimit = options.synthesisGapReadLimit ?? synthesisGapReadLimit();
|
|
321
|
+
const reviewMode = agent?.concern === "review";
|
|
322
|
+
return [
|
|
323
|
+
"",
|
|
324
|
+
reviewMode ? "## Handoff-first review / sampled-audit contract" : "## Handoff-first synthesis / gap-read contract",
|
|
325
|
+
reviewMode
|
|
326
|
+
? "- Treat `Previous Handoff` as the primary evidence map. Your job is targeted quality audit, not a second repository crawl."
|
|
327
|
+
: "- Treat `Previous Handoff` as the primary evidence map. Your job is synthesis, not a second repository crawl.",
|
|
328
|
+
`- You may do at most ${gapReadLimit} gap reads/searches when the handoff has a concrete unknown, contradiction, or missing evidence needed for the final answer.`,
|
|
329
|
+
reviewMode ? "- For review, sample only the highest-risk or least-supported claims. Prefer grep/find for exact symbols/config keys; avoid full reads of already-covered files." : undefined,
|
|
330
|
+
"- Do not reread files listed in `Already Covered Evidence Paths` unless you name the specific missing symbol/line/claim you are verifying.",
|
|
331
|
+
"- If a read is blocked by the cross-step duplicate-read policy, do not retry variants of the same evidence path; use the handoff evidence and mark the claim as sampled/not rechecked.",
|
|
332
|
+
"- Prefer citing evidence already present in the handoff. Use new reads only to close explicit gaps, then stop.",
|
|
333
|
+
"- If coverage is incomplete, say exactly what remains unknown instead of expanding into a broad crawl.",
|
|
334
|
+
"- Return final answer material plus a compact handoff; do not emit a second raw exploration log.",
|
|
335
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatPriorFilesRead(files: string[]): string {
|
|
339
|
+
const unique = [...new Set(files)].slice(0, 40);
|
|
340
|
+
const extra = files.length > unique.length ? `\n- …${files.length - unique.length} more` : "";
|
|
341
|
+
return `${unique.map((file) => `- ${file}`).join("\n")}${extra}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasAnyCapability(agent: AgentDefinition, capabilities: AgentCapability[]): boolean {
|
|
345
|
+
return capabilities.some((capability) => agent.capabilities.includes(capability));
|
|
346
|
+
}
|