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/autoroute.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AgentCatalog } from "./agents.ts";
|
|
3
|
+
import { loadEffectiveConfig } from "./config.ts";
|
|
4
|
+
import { MemoryStore } from "./memory.ts";
|
|
5
|
+
import { buildCompactChalinCriticalSystemPrompt, buildCompactChalinOrchestratorSystemPrompt, buildCompactChalinResumeSystemPrompt, buildChalinOrchestratorSystemPrompt } from "./orchestration.ts";
|
|
6
|
+
import { isUsableStepHandoff, loadResumableRunState } from "./runner-state.ts";
|
|
7
|
+
import { beginChalinTurn, recordDirectToolCompletion } from "./runtime-state.ts";
|
|
8
|
+
import type { RunState } from "./schemas.ts";
|
|
9
|
+
import { setChalinStatus } from "./ui-status.ts";
|
|
10
|
+
|
|
11
|
+
export function registerChalinAutoRouter(pi: ExtensionAPI): void {
|
|
12
|
+
pi.on("input", async (event) => {
|
|
13
|
+
if (event.source === "extension") return { action: "continue" };
|
|
14
|
+
const text = event.text.trim();
|
|
15
|
+
if (!text || text.startsWith("/") || text.startsWith("!")) return { action: "continue" };
|
|
16
|
+
|
|
17
|
+
// Never consume the user prompt. The primary Pi agent stays in control and
|
|
18
|
+
// decides whether to call chalin_route as one of its normal tools.
|
|
19
|
+
return { action: "continue" };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
23
|
+
beginChalinTurn({ prompt: typeof event.prompt === "string" ? event.prompt : undefined });
|
|
24
|
+
const loaded = loadEffectiveConfig({ cwd: ctx.cwd });
|
|
25
|
+
if (!loaded.config.enabled) return;
|
|
26
|
+
const promptText = typeof event.prompt === "string" ? event.prompt : "";
|
|
27
|
+
const resumableRun = looksLikeContinuationPrompt(promptText) ? loadResumableRunState({ cwd: ctx.cwd }) : undefined;
|
|
28
|
+
const useCompactResumePrompt = Boolean(resumableRun);
|
|
29
|
+
const useCompactPrompt = shouldUseCompactDirectOrchestrationPrompt(promptText);
|
|
30
|
+
const useCompactCriticalPrompt = !useCompactResumePrompt && !useCompactPrompt && shouldUseCompactChalinCriticalPrompt(promptText);
|
|
31
|
+
const catalog = useCompactResumePrompt || useCompactPrompt || useCompactCriticalPrompt ? undefined : AgentCatalog.load({ cwd: ctx.cwd });
|
|
32
|
+
const orchestrationPrompt = useCompactResumePrompt
|
|
33
|
+
? buildCompactChalinResumeSystemPrompt()
|
|
34
|
+
: useCompactPrompt
|
|
35
|
+
? buildCompactChalinOrchestratorSystemPrompt()
|
|
36
|
+
: useCompactCriticalPrompt ? buildCompactChalinCriticalSystemPrompt() : buildChalinOrchestratorSystemPrompt(catalog?.list() ?? []);
|
|
37
|
+
const memoryContext = useCompactResumePrompt ? undefined : await globalMemoryContextForPrompt(ctx.cwd, promptText);
|
|
38
|
+
return {
|
|
39
|
+
systemPrompt: `${event.systemPrompt}\n\n${orchestrationPrompt}${memoryContext ? `\n\n${memoryContext}` : ""}`,
|
|
40
|
+
message: {
|
|
41
|
+
customType: useCompactResumePrompt ? "pi-chalin-resume-orchestration" : useCompactPrompt ? "pi-chalin-direct-compact-orchestration" : useCompactCriticalPrompt ? "pi-chalin-critical-compact-orchestration" : "pi-chalin-orchestration",
|
|
42
|
+
content: useCompactResumePrompt && resumableRun ? compactResumeSteeringMessage(resumableRun) : useCompactPrompt ? compactDirectSteeringMessage(ctx.hasUI) : useCompactCriticalPrompt ? compactCriticalSteeringMessage(ctx.hasUI) : [
|
|
43
|
+
"If the user says continue/resume/continua/continúa/sigue/reanuda/retoma after an interrupted pi-chalin run, call chalin_resume before answering from partial findings.",
|
|
44
|
+
"pi-chalin preflight: if this is branch/project analysis, architecture/planning, broad/project-wide review, project-wide refactor strategy, complex/risky multi-file implementation, or memory recall, call chalin_route first. Bounded read-only mini-project reviews, bounded scaffolding, named-file bugfixes, named-file refactors, and simple implementation with explicit acceptance criteria should stay direct.",
|
|
45
|
+
"For explicit small bugfix/test requests with named files, inspect the target files once, edit promptly, and verify. Do not route or dry-run unless the change is broad, destructive, a security-sensitive mutation, or ambiguous.",
|
|
46
|
+
"Also call chalin_route for risky surgical/long-file edits; use scout → planner → worker → reviewer so the edit stays targeted and verified.",
|
|
47
|
+
"If the user asks to compare independent approaches/options, choose chalin_route with parallel planners/reviewers and synthesize the recommendation afterward.",
|
|
48
|
+
"Choose topology/agents yourself. Use one chalin_route call only, then synthesize from its handoff; do not inspect files directly unless a concrete gap remains.",
|
|
49
|
+
ctx.hasUI ? undefined : "Non-interactive mode: avoid dry-run for safe bounded edits; either edit directly or run a real chalin_route. Use dryRun only for destructive/high-risk/ambiguous work that genuinely needs user review.",
|
|
50
|
+
"Simple chat, definitions, one obvious command, tiny isolated edits, bounded read-only mini-project reviews, named-file bugfixes, or bounded scaffolding/simple implementation tasks with explicit files stay direct. Direct mode must still satisfy every explicit acceptance criterion exactly, including requested helper extraction, tests, no dependency additions, and behavior preservation. If the user asks for tests, changing only implementation is incomplete even when existing tests pass; add or update the relevant test file before final verification. Those tests must prove the requested behavior with at least one non-trivial positive case and one meaningful edge/failure case when applicable; merely renaming or preserving a starter smoke/empty test is incomplete. For time/window behavior, make tests deterministic with an injected or controlled clock when possible; avoid brittle mock timer APIs unless you verify the current Bun API in this project. Do not assert exact `Date.now()`-derived milliseconds against real wall time. For dependency-free TypeScript scaffolding, write the exact requested files, keep requested APIs/exported helpers in the requested source file, prefer package.json test script `bun test`, put tests under `test/`, avoid uninstalled runners like tsx/vitest/jest, export the requested API, declare requested package.json `bin` entries that point to executable file paths, never command strings, and fix verification failures and rerun verification after the final edit before answering. For Bun CLI subprocess tests, derive target paths directly with `import.meta.url` and pass `env: { ...process.env, ...overrides }` so stripped PATH/NODE_OPTIONS cannot create false failures. After edits plus a passing final verification, answer immediately with changed files, verification result, and one note naming the requested behavior/constraint satisfied.",
|
|
51
|
+
].filter((line): line is string => Boolean(line)).join("\n"),
|
|
52
|
+
display: false,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
58
|
+
setChalinStatus(ctx, { kind: "idle" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
62
|
+
if (["chalin_route", "chalin_resume"].includes(event.toolName)) {
|
|
63
|
+
if (event.isError) return;
|
|
64
|
+
const blockedReason = chalinRouteBlockedReason(event);
|
|
65
|
+
if (blockedReason) {
|
|
66
|
+
pi.sendMessage({
|
|
67
|
+
customType: "pi-chalin-route-blocked-nudge",
|
|
68
|
+
content: `${event.toolName} did not execute work (${blockedReason}). Do not claim completion from that result. If the user's request is a safe explicit edit, continue directly with native tools; otherwise explain the blocker.`,
|
|
69
|
+
display: false,
|
|
70
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
pi.sendMessage({
|
|
74
|
+
customType: "pi-chalin-synthesis-nudge",
|
|
75
|
+
content: `${event.toolName} finished. Answer the user's original prompt now from the Final answer material in the tool result. Do not call another tool unless that material explicitly names a critical blocking gap.`,
|
|
76
|
+
display: false,
|
|
77
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
78
|
+
scheduleNonInteractiveShutdown(ctx);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const eventArgs = (event as { args?: { command?: unknown } }).args;
|
|
83
|
+
const { shouldProgressNudge, shouldReadyToVerifyNudge, shouldFailureNudge, shouldMissingTestNudge, shouldCompletionNudge, verificationCommand } = recordDirectToolCompletion({
|
|
84
|
+
toolName: event.toolName,
|
|
85
|
+
isError: event.isError,
|
|
86
|
+
command: typeof eventArgs?.command === "string" ? eventArgs.command : undefined,
|
|
87
|
+
argsText: eventArgs ? JSON.stringify(eventArgs) : undefined,
|
|
88
|
+
});
|
|
89
|
+
if (shouldProgressNudge) {
|
|
90
|
+
pi.sendMessage({
|
|
91
|
+
customType: "pi-chalin-direct-progress-nudge",
|
|
92
|
+
content: "You have changed files for a bounded direct task. If the user asked for tests and you have not changed a test/spec file, add or update the relevant test before verification. Tests must prove the requested behavior with non-trivial assertions, not only keep or rename the starter smoke/empty test. For timer code, prefer injected clocks/schedulers over brittle mock timer APIs; for Bun CLI subprocess tests, preserve process.env and derive target file URLs directly. Then run the nearest relevant verification command. If it fails, fix only the root cause and rerun verification after the last edit; then answer. The final answer must name the changed file paths and the exact verification command/result. Do not continue exploring unless a concrete acceptance criterion is still missing.",
|
|
93
|
+
display: false,
|
|
94
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
95
|
+
}
|
|
96
|
+
if (shouldReadyToVerifyNudge) {
|
|
97
|
+
pi.sendMessage({
|
|
98
|
+
customType: "pi-chalin-direct-ready-to-verify-nudge",
|
|
99
|
+
content: [
|
|
100
|
+
"Implementation and required test/doc edits are now in place for this bounded direct task.",
|
|
101
|
+
"Before verification, sanity-check that requested tests are behavior-bearing: they should assert the requested outputs/effects and relevant edge cases, not only a starter empty/smoke path.",
|
|
102
|
+
"Stop planning/exploring. Run the nearest relevant verification command now, normally `bun test` for dependency-free Bun fixtures. If it passes, answer immediately.",
|
|
103
|
+
"If verification fails, fix only the root cause and rerun the same nearest verification after the final edit. A final answer with a failed/stale Verification is invalid.",
|
|
104
|
+
].join("\n"),
|
|
105
|
+
display: false,
|
|
106
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
107
|
+
}
|
|
108
|
+
if (shouldFailureNudge) {
|
|
109
|
+
const commandText = verificationCommand ? `\`${verificationCommand}\`` : "the verification command";
|
|
110
|
+
pi.sendMessage({
|
|
111
|
+
customType: "pi-chalin-direct-verification-failed-nudge",
|
|
112
|
+
content: [
|
|
113
|
+
`${commandText} failed after file changes.`,
|
|
114
|
+
"Do NOT answer as done yet. Read the failure, fix the root cause, and rerun the nearest relevant verification after the final edit. You may not answer with a failed or stale Verification result.",
|
|
115
|
+
"If this is dependency-free TypeScript scaffolding, do not add uninstalled runners; write the exact requested files, keep requested APIs/exported helpers in the requested source file, use Bun's test runner with `bun test`, keep tests in `test/`, and fix imports/scripts so `bun test` passes.",
|
|
116
|
+
"If the failure involves time/window logic, remove wall-clock flakiness: inject/control the clock or make assertions tolerant before rerunning verification. Prefer an injected scheduler over mock timer APIs unless you verify the current Bun mock timer API.",
|
|
117
|
+
"If the failure is a Bun CLI subprocess test, preserve the parent environment with `env: { ...process.env, ...overrides }` and derive the CLI path directly from `import.meta.url`; do not compute a parent directory twice.",
|
|
118
|
+
].join("\n"),
|
|
119
|
+
display: false,
|
|
120
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
121
|
+
}
|
|
122
|
+
if (shouldMissingTestNudge) {
|
|
123
|
+
pi.sendMessage({
|
|
124
|
+
customType: "pi-chalin-direct-tests-missing-nudge",
|
|
125
|
+
content: [
|
|
126
|
+
"The user requested tests, but the changed files so far do not include a test/spec file.",
|
|
127
|
+
"Do NOT answer as done yet. Your next action must be an edit/write to the relevant test/spec file, not a final answer.",
|
|
128
|
+
"Add or update the test so it proves the requested behavior with non-trivial assertions and meaningful edge/failure coverage where applicable, rerun the nearest verification command, then answer with changed implementation and test paths.",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
display: false,
|
|
131
|
+
}, { triggerTurn: true, deliverAs: "steer" });
|
|
132
|
+
}
|
|
133
|
+
if (!shouldCompletionNudge) return;
|
|
134
|
+
const commandText = verificationCommand ? `\`${verificationCommand}\`` : "the verification command";
|
|
135
|
+
pi.sendMessage({
|
|
136
|
+
customType: "pi-chalin-direct-completion-nudge",
|
|
137
|
+
content: [
|
|
138
|
+
`You changed files and ${commandText} passed.`,
|
|
139
|
+
"If the user's acceptance criteria are satisfied and this passing verification happened after the last edit, answer now using this exact compact evidence format:",
|
|
140
|
+
"Passing tests is not enough by itself: before answering, compare the changed files against every explicit prompt requirement, including requested package metadata, bin/scripts, docs, tests, public API, and no-dependency constraints. If tests were requested but they only cover a starter smoke/empty path instead of the requested behavior, do not answer yet; improve the tests and rerun verification.",
|
|
141
|
+
"- Changed: `path/to/file`[, `path/to/test`]",
|
|
142
|
+
`- Verification: ${commandText} passed`,
|
|
143
|
+
"- Notes: one short sentence naming the requested behavior/constraint you satisfied, such as edge case covered, no external dependencies, or time reset behavior",
|
|
144
|
+
"Do not omit the Verification or Notes line. Do not call more tools unless a concrete requested requirement is still missing.",
|
|
145
|
+
].join("\n"),
|
|
146
|
+
display: false,
|
|
147
|
+
}, { triggerTurn: false, deliverAs: "steer" });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
pi.on("session_shutdown", () => {
|
|
151
|
+
// No background auto-routing workers are owned by this module anymore.
|
|
152
|
+
// Subagent execution is driven through the chalin_route tool and Pi's native
|
|
153
|
+
// abort signal.
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async function globalMemoryContextForPrompt(cwd: string, prompt: string): Promise<string | undefined> {
|
|
159
|
+
const query = prompt.trim();
|
|
160
|
+
if (query.length < 8) return undefined;
|
|
161
|
+
try {
|
|
162
|
+
const bundle = await new MemoryStore({ cwd }).retrieve({
|
|
163
|
+
query,
|
|
164
|
+
sourceAgent: "primary-pi-global",
|
|
165
|
+
limit: 5,
|
|
166
|
+
tokenBudget: 520,
|
|
167
|
+
});
|
|
168
|
+
if (bundle.results.length === 0 || !bundle.text.trim()) return undefined;
|
|
169
|
+
return [
|
|
170
|
+
"## pi-chalin global memory context",
|
|
171
|
+
bundle.text,
|
|
172
|
+
"Use these memories as soft guidance for this turn, including direct-mode work. Current repository evidence and explicit user instructions override memory; if evidence contradicts memory, prefer the evidence and repair memory when a memory tool is available.",
|
|
173
|
+
].join("\n");
|
|
174
|
+
} catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function looksLikeContinuationPrompt(prompt: string): boolean {
|
|
180
|
+
const text = prompt.trim().toLowerCase();
|
|
181
|
+
if (!text) return false;
|
|
182
|
+
return /^(continua|continúa|continuar|continue|resume|resumir|reanuda|reanudar|retoma|retomar|sigue|seguir|dale|go on|keep going)(?:\b|[.!?]*)/i.test(text);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function shouldUseCompactDirectOrchestrationPrompt(prompt: string): boolean {
|
|
186
|
+
const text = prompt.toLowerCase();
|
|
187
|
+
if (!text.trim()) return false;
|
|
188
|
+
if (looksLikeChalinOrchestrationWork(text)) return false;
|
|
189
|
+
const pathMentions = countPathMentions(prompt);
|
|
190
|
+
const hasDirectMutationVerb = /\b(implementa|implementar|implement|fix|corrige|corregir|refactor|refactoriza|añade|agrega|add|update|actualiza|scaffold|scaffoldea|crea|create|write|escribe)\b/i.test(prompt);
|
|
191
|
+
const hasScaffoldContract = /\b(scaffold|scaffoldea|greenfield|desde cero|librer[ií]a|cli|package\.json|readme|sin dependencias|no external dependencies)\b/i.test(prompt)
|
|
192
|
+
&& /\b(test|tests|prueba|pruebas|src\/|package\.json|readme|api|export)\b/i.test(prompt);
|
|
193
|
+
return (hasDirectMutationVerb && pathMentions > 0 && pathMentions <= 6) || hasScaffoldContract;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function shouldUseCompactChalinCriticalPrompt(prompt: string): boolean {
|
|
197
|
+
const text = prompt.toLowerCase();
|
|
198
|
+
return /\b(long-file|archivo largo|surgical|quir[uú]rgic|evita reescribir|avoid rewrite|auth|refresh token|security-sensitive|seguridad|dos cambios independientes|independent implementation|modulos separados|m[oó]dulos separados)\b/i.test(text)
|
|
199
|
+
&& /\b(implementa|implement|cambia|change|fix|corrige|agrega|add|tests|pruebas|worker|parallel|paralel)\b/i.test(text);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function compactResumeSteeringMessage(run: RunState): string {
|
|
203
|
+
const completed = run.steps.filter((step) => isUsableStepHandoff(step)).length;
|
|
204
|
+
const total = Math.max(run.steps.length, 1);
|
|
205
|
+
const next = run.steps.find((step) => !isUsableStepHandoff(step));
|
|
206
|
+
return [
|
|
207
|
+
"Continuation intent detected and a resumable pi-chalin run exists.",
|
|
208
|
+
`Run id: ${run.id}. Status: ${run.status}. Progress: ${completed}/${total}. Next agent: ${next?.agent ?? "unknown"}.`,
|
|
209
|
+
`First action MUST be \`chalin_resume\` with {"runId":"${run.id}"}.`,
|
|
210
|
+
"Do not call `chalin_route`; do not restart the workflow; do not answer from partial findings.",
|
|
211
|
+
"After `chalin_resume` returns, answer the user from its Final answer material.",
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function compactCriticalSteeringMessage(hasUI?: boolean): string {
|
|
216
|
+
return [
|
|
217
|
+
"pi-chalin critical preflight: this is risky/complex/surgical work. First action must be `chalin_route`; do not answer direct and do not inspect with native tools first.",
|
|
218
|
+
hasUI ? undefined : "Non-interactive mode: one real chalin_route, then stop from the chalin handoff; no post-chalin native exploration.",
|
|
219
|
+
"Use worker/reviewer discipline: decompose, assign ownership, verify, and preserve a compact final handoff.",
|
|
220
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function compactDirectSteeringMessage(hasUI?: boolean): string {
|
|
224
|
+
return [
|
|
225
|
+
"pi-chalin compact preflight: this looks like bounded direct work. Prefer native tools; do not spend budget on orchestration prose or visible planning before tool calls.",
|
|
226
|
+
hasUI ? undefined : "Non-interactive mode: first action should be a relevant tool call; inspect briefly, write promptly, verify, fix failures, rerun verification after the final edit, then final answer.",
|
|
227
|
+
"For dependency-free TypeScript scaffolding: exact requested files, Bun test runner, tests under test/, no uninstalled runners/dependencies, exported requested API. If it is a CLI package, declare the requested command in package.json `bin`, not only in `scripts`; `bin` values must be executable file paths such as `./src/cli.ts` or `./bin/name`, never runtime command strings.",
|
|
228
|
+
"If tests are requested, make them behavior-bearing: assert requested outputs/effects and edge/failure cases instead of only preserving starter smoke/empty tests.",
|
|
229
|
+
"For timer behavior, prefer injected clocks/schedulers over brittle mock timer APIs. For Bun CLI subprocess tests, preserve process.env and derive paths directly from import.meta.url.",
|
|
230
|
+
"If the prompt says review-only, docs-only, no code changes, or no mutations, obey that literally: do not add tests/source files or modify code unless the user explicitly asks.",
|
|
231
|
+
"Final answer must include Changed, Verification, and Notes. Do not continue exploring after verification passes.",
|
|
232
|
+
].filter((line): line is string => Boolean(line)).join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function looksLikeChalinOrchestrationWork(text: string): boolean {
|
|
236
|
+
return /\b(en profundidad|deep|todo el proyecto|project-wide|arquitectura|architecture|migration|migraci[oó]n|strategy|estrategia|review completo|security review|broad|riesgoso|risky|long-file|archivo largo|surgical|quir[uú]rgic|paralel|parallel|compare approaches|opciones|resume|contin[uú]a|memory|memoria)\b/i.test(text);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function countPathMentions(prompt: string): number {
|
|
240
|
+
const matches = prompt.match(/(?:^|[\s`'"])(?:[\w.-]+\/)+[\w.@-]+|(?:^|[\s`'"])(?:package\.json|README\.md|tsconfig\.json|pyproject\.toml|Cargo\.toml|go\.mod)(?=$|[\s`'".,:;)]|)/gi);
|
|
241
|
+
return matches?.length ?? 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function chalinRouteBlockedReason(event: unknown): string | undefined {
|
|
245
|
+
const details = (event as { result?: { details?: { approval?: { action?: unknown; reason?: unknown }; routeGuard?: { action?: unknown; reason?: unknown } } } }).result?.details;
|
|
246
|
+
if (details?.routeGuard?.action === "direct-recommended") {
|
|
247
|
+
const reason = details.routeGuard.reason;
|
|
248
|
+
return typeof reason === "string" && reason.trim() ? `direct-recommended: ${reason}` : "direct-recommended";
|
|
249
|
+
}
|
|
250
|
+
const action = details?.approval?.action;
|
|
251
|
+
if (typeof action === "string" && action !== "allow") {
|
|
252
|
+
const reason = details?.approval?.reason;
|
|
253
|
+
return typeof reason === "string" && reason.trim() ? `${action}: ${reason}` : action;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function scheduleNonInteractiveShutdown(ctx: { hasUI?: boolean; abort?: () => void; shutdown?: () => void }): void {
|
|
259
|
+
if (ctx.hasUI || typeof ctx.shutdown !== "function" || process.env.PI_CHALIN_NONINTERACTIVE_SHUTDOWN === "0") return;
|
|
260
|
+
const abort = ctx.abort;
|
|
261
|
+
const shutdown = ctx.shutdown;
|
|
262
|
+
const configuredDelay = Number(process.env.PI_CHALIN_NONINTERACTIVE_SHUTDOWN_DELAY_MS);
|
|
263
|
+
const delayMs = Number.isFinite(configuredDelay) && configuredDelay >= 0 ? configuredDelay : 0;
|
|
264
|
+
const timer = setTimeout(() => {
|
|
265
|
+
try {
|
|
266
|
+
abort?.();
|
|
267
|
+
shutdown();
|
|
268
|
+
} catch {
|
|
269
|
+
// Pi can mark extension contexts stale while a print-mode turn exits.
|
|
270
|
+
// The tool result has already been emitted, so stale shutdown is safe to ignore.
|
|
271
|
+
}
|
|
272
|
+
}, delayMs);
|
|
273
|
+
timer.unref?.();
|
|
274
|
+
}
|
package/src/budget.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import type { ArtifactCheckpoint, ArtifactStore } from "./artifacts.ts";
|
|
2
|
+
import type { AgentDefinition, RouteKind, RouteRisk, RunStepState, ToolBudgetProfile } from "./schemas.ts";
|
|
3
|
+
|
|
4
|
+
export type BudgetTaskKind = "recon" | "review" | "implementation" | "migration" | "long-autonomous" | "research" | "planning" | "synthesis";
|
|
5
|
+
export type BudgetHealthStatus = "ok" | "warn" | "budget-capped";
|
|
6
|
+
export type BudgetResumeStrategy = "none" | "handoff-only" | "checkpoint-and-continue" | "split-and-continue" | "stage-checkpoint-validate-memory-next";
|
|
7
|
+
|
|
8
|
+
export interface BudgetCaps {
|
|
9
|
+
maxToolCalls: number;
|
|
10
|
+
maxSeconds: number;
|
|
11
|
+
maxUsd: number;
|
|
12
|
+
maxTurns: number;
|
|
13
|
+
maxOutputChars: number;
|
|
14
|
+
maxReadBytes: number;
|
|
15
|
+
maxFilesTouched: number;
|
|
16
|
+
maxRetriesPerTool: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BudgetPolicy {
|
|
20
|
+
id: string;
|
|
21
|
+
taskKind: BudgetTaskKind;
|
|
22
|
+
profile: ToolBudgetProfile;
|
|
23
|
+
risk: RouteRisk;
|
|
24
|
+
routeKind: RouteKind;
|
|
25
|
+
caps: BudgetCaps;
|
|
26
|
+
resumeStrategy: BudgetResumeStrategy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BudgetPreflightInput {
|
|
30
|
+
task: string;
|
|
31
|
+
routeKind: RouteKind;
|
|
32
|
+
steps?: Array<{ agent: string; task: string; budget?: ToolBudgetProfile }>;
|
|
33
|
+
risk?: RouteRisk;
|
|
34
|
+
needsArtifacts?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BudgetPreflight {
|
|
38
|
+
taskKind: BudgetTaskKind;
|
|
39
|
+
expectedStages: number;
|
|
40
|
+
expectedTools: number;
|
|
41
|
+
risk: RouteRisk;
|
|
42
|
+
budgetProfile: ToolBudgetProfile;
|
|
43
|
+
resumeStrategy: BudgetResumeStrategy;
|
|
44
|
+
requiresArtifacts: boolean;
|
|
45
|
+
recommendation: string;
|
|
46
|
+
policy: BudgetPolicy;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BudgetUsage {
|
|
50
|
+
elapsedMs: number;
|
|
51
|
+
toolCalls: number;
|
|
52
|
+
totalCostUsd: number;
|
|
53
|
+
turns: number;
|
|
54
|
+
outputChars: number;
|
|
55
|
+
readBytes: number;
|
|
56
|
+
filesTouched: number;
|
|
57
|
+
retriesByTool: Record<string, number>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BudgetCapHit {
|
|
61
|
+
name: "max_tool_calls" | "max_seconds" | "max_usd" | "max_turns" | "max_output_chars" | "max_read_bytes" | "max_files_touched" | "max_retries_per_tool";
|
|
62
|
+
used: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BudgetHealth {
|
|
67
|
+
status: BudgetHealthStatus;
|
|
68
|
+
caps: BudgetCapHit[];
|
|
69
|
+
warnings: string[];
|
|
70
|
+
next: "continue" | "checkpoint-and-continue" | "split" | "escalate";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ToolUtilityInput {
|
|
74
|
+
findings: string[];
|
|
75
|
+
toolCalls: number;
|
|
76
|
+
filesRead: string[];
|
|
77
|
+
firstSignalToolCall?: number;
|
|
78
|
+
verificationDone: boolean;
|
|
79
|
+
memoryCandidates: Array<{ content: string; category?: string; confidence?: number }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ToolUtilityMetrics {
|
|
83
|
+
findingsPerTool: number;
|
|
84
|
+
filesReadPerFinding: number;
|
|
85
|
+
duplicateReads: number;
|
|
86
|
+
toolCallsBeforeFirstSignal: number;
|
|
87
|
+
verificationDone: boolean;
|
|
88
|
+
memoryCandidatesQuality: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function policyForStep(
|
|
92
|
+
agent: AgentDefinition | undefined,
|
|
93
|
+
step: Pick<RunStepState, "agent" | "task" | "budget">,
|
|
94
|
+
routeKind: RouteKind = "single-agent",
|
|
95
|
+
risk: RouteRisk = "low",
|
|
96
|
+
): BudgetPolicy {
|
|
97
|
+
const profile = step.budget ?? inferredBudgetProfile(agent, step, routeKind);
|
|
98
|
+
const taskKind = taskKindForAgent(agent, step.task);
|
|
99
|
+
const caps = scaleCaps(baseCapsForTask(taskKind, agent?.name ?? step.agent), profile, risk);
|
|
100
|
+
return {
|
|
101
|
+
id: `${taskKind}:${profile}:${risk}`,
|
|
102
|
+
taskKind,
|
|
103
|
+
profile,
|
|
104
|
+
risk,
|
|
105
|
+
routeKind,
|
|
106
|
+
caps,
|
|
107
|
+
resumeStrategy: resumeStrategyFor(taskKind, profile),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function estimateBudgetPreflight(input: BudgetPreflightInput): BudgetPreflight {
|
|
112
|
+
const taskKind = inferTaskKind(input.task, input.steps);
|
|
113
|
+
const risk = input.risk ?? inferRisk(input.task, input.steps);
|
|
114
|
+
const budgetProfile = inferPreflightProfile(input.task, taskKind, input.steps, input.routeKind);
|
|
115
|
+
const representativeStep = input.steps?.[0] ?? { agent: "delegate", task: input.task, budget: budgetProfile };
|
|
116
|
+
const policy = policyForStep(undefined, { ...representativeStep, budget: budgetProfile }, input.routeKind, risk);
|
|
117
|
+
const expectedStages = input.routeKind === "multi-agent-dag"
|
|
118
|
+
? Math.max(2, Math.min(8, input.steps?.length ?? 3))
|
|
119
|
+
: Math.max(1, input.steps?.length ?? 1);
|
|
120
|
+
const expectedTools = Math.max(policy.caps.maxToolCalls, (input.steps ?? [representativeStep]).reduce((sum, step) => {
|
|
121
|
+
const stepPolicy = policyForStep(undefined, step, input.routeKind, risk);
|
|
122
|
+
return sum + stepPolicy.caps.maxToolCalls;
|
|
123
|
+
}, 0));
|
|
124
|
+
const requiresArtifacts = Boolean(input.needsArtifacts || taskKind === "long-autonomous" || budgetProfile === "extended");
|
|
125
|
+
const resumeStrategy = requiresArtifacts ? "stage-checkpoint-validate-memory-next" : taskKind === "implementation" ? "checkpoint-and-continue" : "handoff-only";
|
|
126
|
+
return {
|
|
127
|
+
taskKind,
|
|
128
|
+
expectedStages,
|
|
129
|
+
expectedTools,
|
|
130
|
+
risk,
|
|
131
|
+
budgetProfile,
|
|
132
|
+
resumeStrategy,
|
|
133
|
+
requiresArtifacts,
|
|
134
|
+
recommendation: recommendationFor(taskKind, budgetProfile, requiresArtifacts),
|
|
135
|
+
policy: { ...policy, resumeStrategy },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function evaluateBudgetUsage(policy: BudgetPolicy, usage: BudgetUsage): BudgetHealth {
|
|
140
|
+
const caps: BudgetCapHit[] = [];
|
|
141
|
+
compare(caps, "max_tool_calls", usage.toolCalls, policy.caps.maxToolCalls);
|
|
142
|
+
compare(caps, "max_seconds", Math.ceil(usage.elapsedMs / 1000), policy.caps.maxSeconds);
|
|
143
|
+
compare(caps, "max_usd", usage.totalCostUsd, policy.caps.maxUsd);
|
|
144
|
+
compare(caps, "max_turns", usage.turns, policy.caps.maxTurns);
|
|
145
|
+
compare(caps, "max_output_chars", usage.outputChars, policy.caps.maxOutputChars);
|
|
146
|
+
compare(caps, "max_read_bytes", usage.readBytes, policy.caps.maxReadBytes);
|
|
147
|
+
compare(caps, "max_files_touched", usage.filesTouched, policy.caps.maxFilesTouched);
|
|
148
|
+
const maxRetries = Math.max(0, ...Object.values(usage.retriesByTool));
|
|
149
|
+
compare(caps, "max_retries_per_tool", maxRetries, policy.caps.maxRetriesPerTool);
|
|
150
|
+
|
|
151
|
+
if (caps.length === 0) return { status: "ok", caps, warnings: [], next: "continue" };
|
|
152
|
+
const hard = caps.some((cap) => ["max_seconds", "max_usd", "max_turns"].includes(cap.name));
|
|
153
|
+
const status: BudgetHealthStatus = hard ? "budget-capped" : "warn";
|
|
154
|
+
return {
|
|
155
|
+
status,
|
|
156
|
+
caps,
|
|
157
|
+
warnings: caps.map((cap) => `${cap.name} used ${formatNumber(cap.used)} over limit ${formatNumber(cap.limit)}`),
|
|
158
|
+
next: status === "budget-capped"
|
|
159
|
+
? policy.resumeStrategy === "stage-checkpoint-validate-memory-next" ? "split" : "checkpoint-and-continue"
|
|
160
|
+
: "continue",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function summarizeToolUtility(input: ToolUtilityInput): ToolUtilityMetrics {
|
|
165
|
+
const findings = input.findings.filter((item) => item.trim().length > 0);
|
|
166
|
+
const uniqueFiles = new Set(input.filesRead);
|
|
167
|
+
const duplicateReads = input.filesRead.length - uniqueFiles.size;
|
|
168
|
+
const qualityScores = input.memoryCandidates.map(memoryQualityScore);
|
|
169
|
+
const memoryCandidatesQuality = qualityScores.length ? round(qualityScores.reduce((sum, value) => sum + value, 0) / qualityScores.length) : 0;
|
|
170
|
+
return {
|
|
171
|
+
findingsPerTool: round(findings.length / Math.max(input.toolCalls, 1)),
|
|
172
|
+
filesReadPerFinding: round(uniqueFiles.size / Math.max(findings.length, 1)),
|
|
173
|
+
duplicateReads,
|
|
174
|
+
toolCallsBeforeFirstSignal: input.firstSignalToolCall ?? (findings.length > 0 ? Math.min(input.toolCalls, 1) : input.toolCalls),
|
|
175
|
+
verificationDone: input.verificationDone,
|
|
176
|
+
memoryCandidatesQuality,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function recordBudgetCheckpoint(store: ArtifactStore, featureId: string, step: RunStepState, reason: string): Promise<ArtifactCheckpoint> {
|
|
181
|
+
await store.initFeature({
|
|
182
|
+
featureId,
|
|
183
|
+
goal: `Continue budget-capped pi-chalin step ${step.agent}`,
|
|
184
|
+
chain: [step.agent],
|
|
185
|
+
currentStep: step.task,
|
|
186
|
+
});
|
|
187
|
+
return store.appendCheckpoint(featureId, {
|
|
188
|
+
agent: step.agent,
|
|
189
|
+
title: `${step.agent} budget-capped`,
|
|
190
|
+
summary: compact([step.output?.handoff, step.output?.text, reason].filter(Boolean).join(" "), 900),
|
|
191
|
+
status: "paused",
|
|
192
|
+
stage: step.id,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function compare(caps: BudgetCapHit[], name: BudgetCapHit["name"], used: number, limit: number): void {
|
|
197
|
+
if (Number.isFinite(limit) && used >= limit) caps.push({ name, used, limit });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function baseCapsForTask(taskKind: BudgetTaskKind, agentName: string): BudgetCaps {
|
|
201
|
+
const baseToolCalls = baseToolCallsFor(taskKind, agentName);
|
|
202
|
+
const isLong = taskKind === "long-autonomous";
|
|
203
|
+
const isWriteHeavy = taskKind === "implementation" || taskKind === "migration";
|
|
204
|
+
const isSynthesis = taskKind === "synthesis" || taskKind === "planning";
|
|
205
|
+
return {
|
|
206
|
+
maxToolCalls: baseToolCalls,
|
|
207
|
+
maxSeconds: isLong ? 7200 : isWriteHeavy ? 1800 : isSynthesis ? 900 : 1200,
|
|
208
|
+
maxUsd: isLong ? 2.5 : isWriteHeavy ? 1.2 : isSynthesis ? 0.45 : 0.8,
|
|
209
|
+
maxTurns: isLong ? 12 : isWriteHeavy ? 8 : isSynthesis ? 4 : 6,
|
|
210
|
+
maxOutputChars: isLong ? 24000 : isWriteHeavy ? 16000 : isSynthesis ? 7000 : 12000,
|
|
211
|
+
maxReadBytes: isLong ? 5_000_000 : isWriteHeavy ? 2_000_000 : isSynthesis ? 350_000 : 1_500_000,
|
|
212
|
+
maxFilesTouched: taskKind === "migration" ? 40 : taskKind === "implementation" ? 20 : 4,
|
|
213
|
+
maxRetriesPerTool: 3,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function baseToolCallsFor(taskKind: BudgetTaskKind, agentName: string): number {
|
|
218
|
+
if (agentName === "context-builder") return 60;
|
|
219
|
+
if (agentName === "scout") return 40;
|
|
220
|
+
if (agentName === "planner") return 25;
|
|
221
|
+
if (agentName === "reviewer") return 50;
|
|
222
|
+
if (agentName === "worker") return 80;
|
|
223
|
+
if (taskKind === "long-autonomous") return 160;
|
|
224
|
+
if (taskKind === "migration") return 120;
|
|
225
|
+
if (taskKind === "implementation") return 80;
|
|
226
|
+
if (taskKind === "review") return 50;
|
|
227
|
+
if (taskKind === "research") return 60;
|
|
228
|
+
if (taskKind === "planning") return 25;
|
|
229
|
+
if (taskKind === "synthesis") return 25;
|
|
230
|
+
return 40;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function scaleCaps(caps: BudgetCaps, profile: ToolBudgetProfile, risk: RouteRisk): BudgetCaps {
|
|
234
|
+
const multiplier = profile === "tight" ? 0.5 : profile === "deep" ? 2 : profile === "extended" ? 4 : 1;
|
|
235
|
+
const riskMultiplier = risk === "critical" ? 0.75 : risk === "high" ? 0.9 : 1;
|
|
236
|
+
const toolCap = profile === "extended" ? 500 : profile === "deep" ? 240 : profile === "tight" ? 60 : 140;
|
|
237
|
+
return {
|
|
238
|
+
maxToolCalls: Math.max(1, Math.min(toolCap, Math.ceil(caps.maxToolCalls * multiplier * riskMultiplier))),
|
|
239
|
+
maxSeconds: Math.max(120, Math.ceil(caps.maxSeconds * multiplier)),
|
|
240
|
+
maxUsd: round(caps.maxUsd * multiplier),
|
|
241
|
+
maxTurns: Math.max(1, Math.ceil(caps.maxTurns * (profile === "tight" ? 0.75 : profile === "deep" ? 1.5 : profile === "extended" ? 2 : 1))),
|
|
242
|
+
maxOutputChars: Math.ceil(caps.maxOutputChars * multiplier),
|
|
243
|
+
maxReadBytes: Math.ceil(caps.maxReadBytes * multiplier),
|
|
244
|
+
maxFilesTouched: Math.max(1, Math.ceil(caps.maxFilesTouched * (profile === "extended" ? 2 : profile === "deep" ? 1.5 : profile === "tight" ? 0.75 : 1))),
|
|
245
|
+
maxRetriesPerTool: caps.maxRetriesPerTool,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function taskKindForAgent(agent: AgentDefinition | undefined, task: string): BudgetTaskKind {
|
|
250
|
+
if (agent?.concern === "implementation") return "implementation";
|
|
251
|
+
if (/\b(long[- ]running|hours?|days?|checkpoint|resume|migration|migrate)\b/i.test(task)) {
|
|
252
|
+
return /\b(long[- ]running|hours?|days?|checkpoint|resume)\b/i.test(task) ? "long-autonomous" : "migration";
|
|
253
|
+
}
|
|
254
|
+
if (/\b(implement|fix|edit|modify|write)\b/i.test(task)) return "implementation";
|
|
255
|
+
if (agent?.concern === "review" || /\b(review|audit|validate)\b/i.test(task)) return "review";
|
|
256
|
+
if (agent?.concern === "research") return "research";
|
|
257
|
+
if (agent?.concern === "planning") return "planning";
|
|
258
|
+
if (agent?.concern === "context-building" && /\b(synthesize|summarize|final)\b/i.test(task)) return "synthesis";
|
|
259
|
+
return "recon";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function inferTaskKind(task: string, steps: BudgetPreflightInput["steps"]): BudgetTaskKind {
|
|
263
|
+
const combined = [task, ...(steps ?? []).flatMap((step) => [step.agent, step.task])].join(" ");
|
|
264
|
+
if (/\b(long[- ]running|hours?|days?|checkpoint|resume|autonomous)\b/i.test(combined)) return "long-autonomous";
|
|
265
|
+
if (/\b(migrate|migration|codemod|vue3|rewrite across|all components)\b/i.test(combined)) return "migration";
|
|
266
|
+
if (/\b(implement|fix|edit|modify|worker)\b/i.test(combined)) return "implementation";
|
|
267
|
+
if (/\b(review|audit|security|validate)\b/i.test(combined)) return "review";
|
|
268
|
+
if (/\b(web|internet|docs?|source)\b/i.test(combined)) return "research";
|
|
269
|
+
if (/\b(plan|roadmap|design)\b/i.test(combined)) return "planning";
|
|
270
|
+
return "recon";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function inferRisk(task: string, steps: BudgetPreflightInput["steps"]): RouteRisk {
|
|
274
|
+
const combined = [task, ...(steps ?? []).map((step) => step.task)].join(" ");
|
|
275
|
+
if (/\b(delete|security|auth|payment|database|migration|production|write|modify|edit)\b/i.test(combined)) return "high";
|
|
276
|
+
if ((steps ?? []).some((step) => step.agent === "worker")) return "medium";
|
|
277
|
+
return "low";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function inferPreflightProfile(task: string, taskKind: BudgetTaskKind, steps: BudgetPreflightInput["steps"], routeKind: RouteKind): ToolBudgetProfile {
|
|
281
|
+
const explicit = steps?.map((step) => step.budget).filter(Boolean).at(-1);
|
|
282
|
+
if (explicit) return explicit;
|
|
283
|
+
if (taskKind === "long-autonomous") return "extended";
|
|
284
|
+
if (taskKind === "migration" || routeKind === "multi-agent-dag") return "deep";
|
|
285
|
+
if (taskKind === "planning" || /\b(simple|quick|small)\b/i.test(task)) return "tight";
|
|
286
|
+
return "normal";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function inferredBudgetProfile(agent: AgentDefinition | undefined, step: Pick<RunStepState, "agent" | "task" | "budget">, routeKind: RouteKind): ToolBudgetProfile {
|
|
290
|
+
if (step.budget) return step.budget;
|
|
291
|
+
if (/\b(long[- ]running|hours?|days?|checkpoint|resume|autonomous)\b/i.test(step.task)) return "extended";
|
|
292
|
+
if (routeKind === "multi-agent-dag" && ["recon", "context-building", "review", "research"].includes(agent?.concern ?? "")) return "deep";
|
|
293
|
+
if (agent?.concern === "planning") return "tight";
|
|
294
|
+
return "normal";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resumeStrategyFor(taskKind: BudgetTaskKind, profile: ToolBudgetProfile): BudgetResumeStrategy {
|
|
298
|
+
if (taskKind === "long-autonomous" || profile === "extended") return "stage-checkpoint-validate-memory-next";
|
|
299
|
+
if (taskKind === "implementation" || taskKind === "migration") return "checkpoint-and-continue";
|
|
300
|
+
if (taskKind === "recon" || taskKind === "review" || taskKind === "research") return "handoff-only";
|
|
301
|
+
return "none";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function recommendationFor(taskKind: BudgetTaskKind, profile: ToolBudgetProfile, artifacts: boolean): string {
|
|
305
|
+
if (taskKind === "long-autonomous") return "Use staged DAG execution with checkpoint → validate → memory → next-stage continuation.";
|
|
306
|
+
if (artifacts || profile === "extended") return "Write checkpoint artifacts at every handoff and split work before budget caps are hit.";
|
|
307
|
+
if (taskKind === "migration") return "Prefer DAG fan-out by module with reviewer synthesis and validation contracts.";
|
|
308
|
+
return "Use the smallest bounded agent workflow and stop after high-signal evidence.";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function memoryQualityScore(candidate: { content: string; category?: string; confidence?: number }): number {
|
|
312
|
+
const content = candidate.content.trim();
|
|
313
|
+
if (!content || /\b(stdout|stderr|traceback|cmd =|subprocess|returncode)\b/i.test(content)) return 0;
|
|
314
|
+
const durableCategory = /^(project-fact|pattern|tooling|testing|workflow|bugfix|validation|artifact|decision|preference|architecture|safety|security|failure)$/i.test(candidate.category ?? "");
|
|
315
|
+
const sentenceScore = content.split(/[.!?]+/).filter((part) => part.trim().length > 15).length >= 1 ? 0.35 : 0.15;
|
|
316
|
+
const lengthScore = content.length >= 80 && content.length <= 600 ? 0.35 : 0.15;
|
|
317
|
+
const categoryScore = durableCategory ? 0.2 : 0.05;
|
|
318
|
+
const confidenceScore = Math.min(0.1, Math.max(0, candidate.confidence ?? 0) / 10);
|
|
319
|
+
return round(sentenceScore + lengthScore + categoryScore + confidenceScore);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function compact(text: string, max: number): string {
|
|
323
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
324
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}…`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function round(value: number): number {
|
|
328
|
+
return Math.round(value * 1000) / 1000;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatNumber(value: number): string {
|
|
332
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
333
|
+
}
|