@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5
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/CHANGELOG.md +1654 -0
- package/LICENSE +21 -0
- package/README.md +1533 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +199 -0
- package/dist/history/ingest.js +468 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +466 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/bridge.js +750 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +88 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +141 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +110 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +350 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom-bridge.md +212 -0
- package/docs/intercom.md +581 -0
- package/docs/performance.md +121 -0
- package/package.json +102 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/intercom-bridge-credentials.mjs +143 -0
- package/scripts/intercom-remote.mjs +103 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +209 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +156 -0
- package/src/history/ingest.ts +349 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +321 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/bridge.ts +668 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +82 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +124 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +100 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +415 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1260 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const RESOLVE_CWD = "pi-change-working-dir:resolve-execution-cwd";
|
|
6
|
+
const SET_CWD = "pi-change-working-dir:set-execution-cwd";
|
|
7
|
+
const intentPath = (sessionFile: string): string => `${sessionFile}.subagent-cwd-init`;
|
|
8
|
+
type CwdIntent = { cwd?: string };
|
|
9
|
+
type CwdRequest = { sessionManager: ExtensionContext["sessionManager"]; path?: string; result?: { cwd: string; error?: string } };
|
|
10
|
+
|
|
11
|
+
/** Call once for a new fork or explicit resume override; undo only if launch fails before starting. */
|
|
12
|
+
export function requestChildExecutionCwd(sessionFile: string, cwd?: string): () => void {
|
|
13
|
+
if (cwd !== undefined && !path.isAbsolute(cwd)) throw new Error("Child execution cwd must be absolute.");
|
|
14
|
+
const previous = readIntent(sessionFile);
|
|
15
|
+
fs.writeFileSync(intentPath(sessionFile), JSON.stringify({ cwd }), { mode: 0o600 });
|
|
16
|
+
return () => {
|
|
17
|
+
if (previous) fs.writeFileSync(intentPath(sessionFile), JSON.stringify(previous), { mode: 0o600 });
|
|
18
|
+
else fs.unlinkSync(intentPath(sessionFile));
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readIntent(sessionFile: string): CwdIntent | undefined {
|
|
23
|
+
let raw: string;
|
|
24
|
+
try {
|
|
25
|
+
raw = fs.readFileSync(intentPath(sessionFile), "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const value: unknown = JSON.parse(raw);
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
32
|
+
|| ("cwd" in value && (typeof value.cwd !== "string" || !path.isAbsolute(value.cwd)))) {
|
|
33
|
+
throw new Error("Invalid child execution cwd initialization request.");
|
|
34
|
+
}
|
|
35
|
+
return value as CwdIntent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Freeze the new fork's requested directory once the actual launch cwd is known. */
|
|
39
|
+
export function prepareChildExecutionCwd(sessionFile: string, cwd?: string): void {
|
|
40
|
+
const intent = readIntent(sessionFile);
|
|
41
|
+
if (intent && intent.cwd === undefined) {
|
|
42
|
+
if (!cwd) throw new Error("New fork execution cwd was not supplied.");
|
|
43
|
+
requestChildExecutionCwd(sessionFile, cwd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasExecutionCwdOwner(pi: Pick<ExtensionAPI, "getAllTools" | "getCommands">): boolean {
|
|
48
|
+
// excludeTools can hide change_dir while the owner's /cwd command remains loaded.
|
|
49
|
+
const fromOwner = ({ sourceInfo }: { sourceInfo: { source: string; path: string } }) =>
|
|
50
|
+
/(?:^|[/\\:@])(?:pi-)?change-working-dir(?:[/\\@:.>]|$)/.test(`${sourceInfo.source}/${sourceInfo.path}`);
|
|
51
|
+
return pi.getAllTools().some((tool) => tool.name === "change_dir" || fromOwner(tool))
|
|
52
|
+
|| pi.getCommands().some((command) => command.source === "extension" && (/^cwd(?::\d+)?$/.test(command.name) || fromOwner(command)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerChildExecutionCwd(pi: ExtensionAPI): void {
|
|
56
|
+
let initialized = false;
|
|
57
|
+
pi.on("input", (_event, ctx) => {
|
|
58
|
+
if (initialized) return;
|
|
59
|
+
try {
|
|
60
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
61
|
+
const intent = sessionFile ? readIntent(sessionFile) : undefined;
|
|
62
|
+
const request: CwdRequest = { sessionManager: ctx.sessionManager };
|
|
63
|
+
pi.events.emit(RESOLVE_CWD, request);
|
|
64
|
+
if (request.result?.error) throw new Error(request.result.error);
|
|
65
|
+
const owned = Boolean(request.result) || hasExecutionCwdOwner(pi);
|
|
66
|
+
if (owned && !request.result?.cwd) throw new Error("The loaded directory extension did not resolve the child directory. Update pi-change-working-dir and ensure session startup has completed.");
|
|
67
|
+
if (!intent) {
|
|
68
|
+
initialized = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!intent.cwd) throw new Error("New fork execution cwd was not prepared before startup.");
|
|
72
|
+
if (owned) {
|
|
73
|
+
const selection: CwdRequest = { sessionManager: ctx.sessionManager, path: intent.cwd };
|
|
74
|
+
pi.events.emit(SET_CWD, selection);
|
|
75
|
+
if (!selection.result) throw new Error("The loaded directory extension did not initialize the child directory. Update pi-change-working-dir and ensure session startup has completed.");
|
|
76
|
+
if (selection.result.error) throw new Error(selection.result.error);
|
|
77
|
+
if (fs.realpathSync(selection.result.cwd) !== fs.realpathSync(intent.cwd)) throw new Error("The directory extension selected a different child directory.");
|
|
78
|
+
} else if (fs.realpathSync(ctx.cwd) !== fs.realpathSync(intent.cwd)) {
|
|
79
|
+
throw new Error("Native child directory does not match the requested execution cwd.");
|
|
80
|
+
}
|
|
81
|
+
// This is a launch intent, not another directory store. Only the owner persists selection.
|
|
82
|
+
fs.unlinkSync(intentPath(sessionFile!));
|
|
83
|
+
initialized = true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`Subagent directory initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
// Native hook exceptions are swallowed. Handled input stops before provider/tool dispatch.
|
|
88
|
+
return { action: "handled" };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { NativeUsageMetadata } from "./native-usage.ts";
|
|
3
|
+
|
|
4
|
+
/** Match each native ID once, keeping FIFO assignment for identical message identities. */
|
|
5
|
+
export function createMessageReferenceIndex<T extends { message?: Message; nativeEntryId?: string }>() {
|
|
6
|
+
type Queue = { items: T[]; offset: number };
|
|
7
|
+
const byId = new Map<string, T>();
|
|
8
|
+
const buckets = new Map<string, Map<number | undefined, { all: Queue; tools: Map<string, Queue> }>>();
|
|
9
|
+
const matches = (item: T, message: NonNullable<NativeUsageMetadata["message"]>) => item.message?.role === message.role
|
|
10
|
+
&& item.message.timestamp === message.timestamp
|
|
11
|
+
&& (!message.toolCallId || item.message.role === "toolResult" && item.message.toolCallId === message.toolCallId);
|
|
12
|
+
return {
|
|
13
|
+
add(item: T): void {
|
|
14
|
+
if (!item.message) return;
|
|
15
|
+
const { role, timestamp } = item.message;
|
|
16
|
+
let timestamps = buckets.get(role);
|
|
17
|
+
if (!timestamps) buckets.set(role, timestamps = new Map());
|
|
18
|
+
let bucket = timestamps.get(timestamp);
|
|
19
|
+
if (!bucket) timestamps.set(timestamp, bucket = { all: { items: [], offset: 0 }, tools: new Map() });
|
|
20
|
+
bucket.all.items.push(item);
|
|
21
|
+
if (item.message.role === "toolResult" && item.message.toolCallId) {
|
|
22
|
+
let queue = bucket.tools.get(item.message.toolCallId);
|
|
23
|
+
if (!queue) bucket.tools.set(item.message.toolCallId, queue = { items: [], offset: 0 });
|
|
24
|
+
queue.items.push(item);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
reference(entry: NativeUsageMetadata): T | undefined {
|
|
28
|
+
if (!entry.message) return;
|
|
29
|
+
const existing = byId.get(entry.id);
|
|
30
|
+
if (existing && matches(existing, entry.message)) return existing;
|
|
31
|
+
const bucket = buckets.get(entry.message.role)?.get(entry.message.timestamp);
|
|
32
|
+
const queue = entry.message.toolCallId ? bucket?.tools.get(entry.message.toolCallId) : bucket?.all;
|
|
33
|
+
if (!queue) return;
|
|
34
|
+
while (queue.offset < queue.items.length && queue.items[queue.offset]!.nativeEntryId) queue.offset++;
|
|
35
|
+
const item = queue.items[queue.offset];
|
|
36
|
+
if (item) { item.nativeEntryId = entry.id; byId.set(entry.id, item); }
|
|
37
|
+
return item;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Streaming equivalent of the existing exit/status diagnostic expression. */
|
|
43
|
+
export class ExitCodeObservation {
|
|
44
|
+
private tail = "";
|
|
45
|
+
private collecting = false;
|
|
46
|
+
private complete = false;
|
|
47
|
+
value?: number;
|
|
48
|
+
write(text: string): void {
|
|
49
|
+
if (this.complete) return;
|
|
50
|
+
if (this.collecting) {
|
|
51
|
+
for (const character of text) {
|
|
52
|
+
if (character < "0" || character > "9") { this.complete = true; return; }
|
|
53
|
+
this.value = this.value! * 10 + Number(character);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const normalized = (this.tail + text.replace(/\s+/g, " ")).replace(/ +/g, " ");
|
|
58
|
+
const match = /exit(?:ed)? *(?:with *)?(?:code|status)? *[: ]? *(\d+)/i.exec(normalized);
|
|
59
|
+
if (match) {
|
|
60
|
+
this.value = Number(match[1]);
|
|
61
|
+
this.collecting = match.index + match[0].length === normalized.length;
|
|
62
|
+
this.complete = !this.collecting;
|
|
63
|
+
this.tail = "";
|
|
64
|
+
} else this.tail = normalized.slice(-32);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function argumentPreview(arguments_: Record<string, unknown>): Record<string, string | boolean | number> {
|
|
69
|
+
const result: Record<string, string | boolean | number> = {};
|
|
70
|
+
for (const [key, value] of Object.entries(arguments_)) {
|
|
71
|
+
if (typeof value === "string") result[key] = value.slice(0, 2048);
|
|
72
|
+
else if (typeof value === "boolean" || typeof value === "number") result[key] = value;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/** Facts consumed by guards/reports; full bodies belong to native history or the audit. */
|
|
77
|
+
export function compactObservedMessage(message: Message): Message {
|
|
78
|
+
const firstText = Array.isArray(message.content) ? message.content.find((part) => part.type === "text") : undefined;
|
|
79
|
+
const observedExitCode = message.role === "toolResult" && firstText?.type === "text"
|
|
80
|
+
? (message as Message & { observedExitCode?: number }).observedExitCode ?? Number(firstText.text.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i)?.[1])
|
|
81
|
+
: undefined;
|
|
82
|
+
const content = Array.isArray(message.content) ? message.content.map((part) => {
|
|
83
|
+
if (part.type === "text") {
|
|
84
|
+
const preview = part.text.slice(0, 4096);
|
|
85
|
+
return { type: "text" as const, text: preview + (!preview.trim() && part.text.trim() ? "…" : "") };
|
|
86
|
+
}
|
|
87
|
+
if (part.type === "toolCall") return { ...part, arguments: part.name === "structured_output" ? part.arguments : argumentPreview(part.arguments ?? {}) };
|
|
88
|
+
if (part.type === "thinking") return { type: "thinking" as const, thinking: "" };
|
|
89
|
+
return { type: "image" as const, data: "", mimeType: part.mimeType };
|
|
90
|
+
}) : typeof message.content === "string" ? message.content.slice(0, 4096) : [];
|
|
91
|
+
if (message.role === "toolResult") {
|
|
92
|
+
const details = message.details as { preview?: unknown; modifiedFiles?: unknown } | undefined;
|
|
93
|
+
return { ...message, ...(observedExitCode !== undefined && !Number.isNaN(observedExitCode) ? { observedExitCode } : {}), content: content as typeof message.content, details: details ? {
|
|
94
|
+
...(typeof details.preview === "boolean" ? { preview: details.preview } : {}),
|
|
95
|
+
...(Array.isArray(details.modifiedFiles) ? { modifiedFiles: details.modifiedFiles.filter((file): file is string => typeof file === "string") } : {}),
|
|
96
|
+
} : undefined };
|
|
97
|
+
}
|
|
98
|
+
return { ...message, content } as Message;
|
|
99
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
6
|
+
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
7
|
+
import { scanJournal } from "../../shared/journal-reader.ts";
|
|
8
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
9
|
+
|
|
10
|
+
const PREFIX = "claude-code/";
|
|
11
|
+
const DEFAULT_CONTEXT = "300k";
|
|
12
|
+
const CONTEXT_TOKENS: Record<"300k" | "1m", string> = {
|
|
13
|
+
"300k": "300000",
|
|
14
|
+
"1m": "1000000",
|
|
15
|
+
};
|
|
16
|
+
const FAMILY_ALIASES = new Set(["fable", "opus", "sonnet", "haiku"]);
|
|
17
|
+
const SESSION_EVENT_TYPE = "claude_code_session";
|
|
18
|
+
|
|
19
|
+
export interface ClaudeCodeModelSpec {
|
|
20
|
+
inputModel: string;
|
|
21
|
+
family: "fable" | "opus" | "sonnet" | "haiku";
|
|
22
|
+
context: "300k" | "1m" | "native";
|
|
23
|
+
thinking?: string;
|
|
24
|
+
cliModel: string;
|
|
25
|
+
autoCompactWindow?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClaudeCodeSessionMetadata {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
model: string;
|
|
31
|
+
cliModel: string;
|
|
32
|
+
family: ClaudeCodeModelSpec["family"];
|
|
33
|
+
context: ClaudeCodeModelSpec["context"];
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClaudeCodeInvocation {
|
|
38
|
+
command: string;
|
|
39
|
+
args: string[];
|
|
40
|
+
env: Record<string, string | undefined>;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
resuming: boolean;
|
|
43
|
+
model: ClaudeCodeModelSpec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ClaudeCodeResultEvent {
|
|
47
|
+
type?: string;
|
|
48
|
+
subtype?: string;
|
|
49
|
+
is_error?: boolean;
|
|
50
|
+
api_error_status?: number | null;
|
|
51
|
+
result?: string;
|
|
52
|
+
stop_reason?: string;
|
|
53
|
+
session_id?: string;
|
|
54
|
+
total_cost_usd?: number;
|
|
55
|
+
usage?: {
|
|
56
|
+
input_tokens?: number;
|
|
57
|
+
output_tokens?: number;
|
|
58
|
+
cache_read_input_tokens?: number;
|
|
59
|
+
cache_creation_input_tokens?: number;
|
|
60
|
+
};
|
|
61
|
+
modelUsage?: Record<string, { contextWindow?: number; maxOutputTokens?: number }>;
|
|
62
|
+
structured_output?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isClaudeCodeModel(model: string | undefined): boolean {
|
|
66
|
+
return Boolean(model?.startsWith(PREFIX));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function splitThinking(input: string): { model: string; thinking?: string } {
|
|
70
|
+
const { baseModel, thinkingSuffix } = splitKnownThinkingSuffix(input);
|
|
71
|
+
return thinkingSuffix ? { model: baseModel, thinking: thinkingSuffix.slice(1) } : { model: input };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseClaudeCodeModel(model: string): ClaudeCodeModelSpec {
|
|
75
|
+
if (!isClaudeCodeModel(model)) throw new Error(`Not a Claude Code model: ${model}`);
|
|
76
|
+
const { model: withoutThinking, thinking } = splitThinking(model);
|
|
77
|
+
const raw = withoutThinking.slice(PREFIX.length).trim().toLowerCase();
|
|
78
|
+
const [familyRaw, contextRaw] = raw.split("@", 2);
|
|
79
|
+
if (!FAMILY_ALIASES.has(familyRaw)) {
|
|
80
|
+
throw new Error(`Unsupported Claude Code model '${model}'. Use claude-code/fable, claude-code/opus, claude-code/sonnet, or claude-code/haiku.`);
|
|
81
|
+
}
|
|
82
|
+
const family = familyRaw as ClaudeCodeModelSpec["family"];
|
|
83
|
+
const context = contextRaw ?? (family === "haiku" ? "native" : DEFAULT_CONTEXT);
|
|
84
|
+
if (context !== "300k" && context !== "1m" && context !== "native") {
|
|
85
|
+
throw new Error(`Unsupported Claude Code context '${context}' in '${model}'. Use @300k or @1m.`);
|
|
86
|
+
}
|
|
87
|
+
if (family === "haiku" && context === "1m") throw new Error("Claude Code haiku does not support @1m context.");
|
|
88
|
+
if (family === "haiku" && context === "300k") throw new Error("Claude Code haiku does not support @300k context.");
|
|
89
|
+
const cliModel = context === "1m" && (family === "opus" || family === "sonnet") ? `${family}[1m]` : family;
|
|
90
|
+
return {
|
|
91
|
+
inputModel: model,
|
|
92
|
+
family,
|
|
93
|
+
context,
|
|
94
|
+
...(thinking && thinking !== "off" ? { thinking } : {}),
|
|
95
|
+
cliModel,
|
|
96
|
+
...(context === "300k" ? { autoCompactWindow: CONTEXT_TOKENS[context] } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readClaudeCodeSessionMetadata(sessionFile: string | undefined): ClaudeCodeSessionMetadata | undefined {
|
|
101
|
+
if (!sessionFile) return;
|
|
102
|
+
const validate = (value: Partial<ClaudeCodeSessionMetadata> | undefined): ClaudeCodeSessionMetadata | undefined => {
|
|
103
|
+
if (!value || typeof value.sessionId !== "string" || !value.sessionId || typeof value.model !== "string" || typeof value.cliModel !== "string"
|
|
104
|
+
|| !["fable", "opus", "sonnet", "haiku"].includes(value.family ?? "") || !["300k", "1m", "native"].includes(value.context ?? "")) return;
|
|
105
|
+
return { ...value, updatedAt: Number(value.updatedAt) || 0 } as ClaudeCodeSessionMetadata;
|
|
106
|
+
};
|
|
107
|
+
try { return validate(JSON.parse(fs.readFileSync(`${sessionFile}.metadata.json`, "utf8"))); }
|
|
108
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
109
|
+
if (!fs.existsSync(sessionFile)) return;
|
|
110
|
+
let metadata: ClaudeCodeSessionMetadata | undefined;
|
|
111
|
+
// Legacy metadata is recovered read-only; new updates never rewrite its audit messages.
|
|
112
|
+
scanJournal(sessionFile, (path) => !path.length || ["type", "claudeCode"].includes(String(path[0])) ? 4096 : false, ({ value }) => {
|
|
113
|
+
if (value?.type === SESSION_EVENT_TYPE) metadata ??= validate(value.claudeCode);
|
|
114
|
+
}, { policy: "inspect" });
|
|
115
|
+
return metadata;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function writeClaudeCodeSessionMetadata(sessionFile: string, metadata: ClaudeCodeSessionMetadata): void {
|
|
119
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
120
|
+
writeAtomicJson(`${sessionFile}.metadata.json`, metadata);
|
|
121
|
+
fs.closeSync(fs.openSync(sessionFile, "a", 0o600));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function appendClaudeCodeMessage(sessionFile: string | undefined, message: Message, observation?: Omit<ClaudeCodeResultEvent, "result" | "structured_output">): void {
|
|
125
|
+
if (!sessionFile) return;
|
|
126
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
127
|
+
fs.appendFileSync(sessionFile, `${JSON.stringify({ type: "message_end", message, ...(observation ? { claudeCodeResult: observation } : {}) })}\n`, { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasNonMetadataContent(sessionFile: string): boolean {
|
|
131
|
+
if (!fs.existsSync(sessionFile)) return false;
|
|
132
|
+
let content = false;
|
|
133
|
+
scanJournal(sessionFile, (path) => !path.length || path.length === 1 && path[0] === "type", ({ value }) => {
|
|
134
|
+
if (value?.type !== SESSION_EVENT_TYPE) content = true;
|
|
135
|
+
}, { policy: "inspect", malformed: () => { content = true; } });
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildClaudeCodeInvocation(input: {
|
|
140
|
+
model: string;
|
|
141
|
+
task: string;
|
|
142
|
+
systemPrompt?: string;
|
|
143
|
+
systemPromptMode?: "append" | "replace";
|
|
144
|
+
sessionFile?: string;
|
|
145
|
+
sessionName?: string;
|
|
146
|
+
tools?: string[];
|
|
147
|
+
mcpDirectTools?: string[];
|
|
148
|
+
allowSubagents?: boolean;
|
|
149
|
+
inheritProjectContext?: boolean;
|
|
150
|
+
inheritSkills?: boolean;
|
|
151
|
+
outputSchema?: JsonSchemaObject;
|
|
152
|
+
}): ClaudeCodeInvocation {
|
|
153
|
+
const parsed = parseClaudeCodeModel(input.model);
|
|
154
|
+
const existing = readClaudeCodeSessionMetadata(input.sessionFile);
|
|
155
|
+
if (input.sessionFile && !existing && hasNonMetadataContent(input.sessionFile)) {
|
|
156
|
+
throw new Error(`Claude Code backend cannot resume non-Claude session file: ${input.sessionFile}. Use fresh context for claude-code/* subagents.`);
|
|
157
|
+
}
|
|
158
|
+
const sessionId = existing?.sessionId ?? randomUUID();
|
|
159
|
+
const args = ["-p", "--dangerously-skip-permissions", "--model", parsed.cliModel, "--output-format", "stream-json", "--verbose"];
|
|
160
|
+
if (parsed.thinking && parsed.thinking !== "minimal") args.push("--effort", parsed.thinking);
|
|
161
|
+
if (input.sessionName) args.push("--name", input.sessionName);
|
|
162
|
+
if (existing) args.push("--resume", sessionId);
|
|
163
|
+
else args.push("--session-id", sessionId);
|
|
164
|
+
if (input.systemPrompt?.trim()) {
|
|
165
|
+
args.push(input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt", input.systemPrompt);
|
|
166
|
+
}
|
|
167
|
+
if (input.allowSubagents) throw new Error("Claude Code backend does not support nested subagent fanout. Use a Pi-backed model for allowSubagents.");
|
|
168
|
+
const inheritProjectContext = input.inheritProjectContext ?? true;
|
|
169
|
+
const inheritSkills = input.inheritSkills ?? true;
|
|
170
|
+
if (!inheritProjectContext && inheritSkills) {
|
|
171
|
+
throw new Error("Claude Code cannot disable project setting sources while preserving their skills. Set inheritSkills: false or use a Pi-backed model.");
|
|
172
|
+
}
|
|
173
|
+
if (!inheritProjectContext) args.push("--setting-sources", "");
|
|
174
|
+
if (!inheritSkills) args.push("--disable-slash-commands");
|
|
175
|
+
args.push("--disallowedTools=Agent");
|
|
176
|
+
const mappedTools = mapClaudeCodeTools(input.tools, input.mcpDirectTools);
|
|
177
|
+
if (input.outputSchema) args.push("--json-schema", JSON.stringify(input.outputSchema));
|
|
178
|
+
args.push(input.task);
|
|
179
|
+
if (mappedTools) args.push("--tools", mappedTools);
|
|
180
|
+
return {
|
|
181
|
+
command: "claude",
|
|
182
|
+
args,
|
|
183
|
+
env: parsed.autoCompactWindow ? { CLAUDE_CODE_AUTO_COMPACT_WINDOW: parsed.autoCompactWindow } : {},
|
|
184
|
+
sessionId,
|
|
185
|
+
resuming: Boolean(existing),
|
|
186
|
+
model: parsed,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mapClaudeCodeTools(tools: string[] | undefined, mcpDirectTools: string[] | undefined): string | undefined {
|
|
191
|
+
if (mcpDirectTools?.length) {
|
|
192
|
+
throw new Error(`Claude Code backend does not support MCP direct tool allowlist entries: ${mcpDirectTools.join(", ")}. Use a Pi-backed model for MCP direct tools.`);
|
|
193
|
+
}
|
|
194
|
+
if (!tools || tools.length === 0) return undefined;
|
|
195
|
+
const map: Record<string, string> = {
|
|
196
|
+
bash: "Bash",
|
|
197
|
+
read: "Read",
|
|
198
|
+
edit: "Edit",
|
|
199
|
+
write: "Write",
|
|
200
|
+
grep: "Grep",
|
|
201
|
+
glob: "Glob",
|
|
202
|
+
web_fetch: "WebFetch",
|
|
203
|
+
webfetch: "WebFetch",
|
|
204
|
+
web_search: "WebSearch",
|
|
205
|
+
websearch: "WebSearch",
|
|
206
|
+
};
|
|
207
|
+
const mapped: string[] = [];
|
|
208
|
+
const unsupported: string[] = [];
|
|
209
|
+
for (const tool of tools) {
|
|
210
|
+
const name = tool.trim();
|
|
211
|
+
if (!name) continue;
|
|
212
|
+
const mappedTool = map[name.toLowerCase()];
|
|
213
|
+
if (mappedTool) mapped.push(mappedTool);
|
|
214
|
+
else unsupported.push(name);
|
|
215
|
+
}
|
|
216
|
+
if (unsupported.length > 0 || mapped.length === 0) {
|
|
217
|
+
throw new Error(`Claude Code backend does not support tool allowlist entr${unsupported.length === 1 ? "y" : "ies"}: ${(unsupported.length ? unsupported : tools).join(", ")}. Supported tools: bash, read, edit, write, grep, glob, web_fetch, web_search.`);
|
|
218
|
+
}
|
|
219
|
+
return [...new Set(mapped)].join(",");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function claudeCodeMessageFromResult(event: ClaudeCodeResultEvent, fallbackModel: string): Message {
|
|
223
|
+
const resultText = event.result ?? "";
|
|
224
|
+
const isError = event.is_error === true || event.subtype === "error" || Boolean(event.api_error_status);
|
|
225
|
+
const counters = { input: event.usage?.input_tokens, output: event.usage?.output_tokens,
|
|
226
|
+
cacheRead: event.usage?.cache_read_input_tokens, cacheWrite: event.usage?.cache_creation_input_tokens };
|
|
227
|
+
const observed = Object.fromEntries(Object.entries(counters).filter(([, value]) => value !== undefined));
|
|
228
|
+
if (Object.values(counters).every((value) => typeof value === "number")) observed.totalTokens = Object.values(counters).reduce<number>((sum, value) => sum + value!, 0);
|
|
229
|
+
return {
|
|
230
|
+
role: "assistant",
|
|
231
|
+
content: [{ type: "text", text: resultText }],
|
|
232
|
+
model: resolveClaudeCodeResultModel(event) ?? fallbackModel,
|
|
233
|
+
stopReason: isError ? "error" : "stop",
|
|
234
|
+
...(isError ? { errorMessage: resultText || `Claude Code failed${event.api_error_status ? ` (${event.api_error_status})` : ""}.` } : {}),
|
|
235
|
+
usage: Object.keys(observed).length || event.total_cost_usd !== undefined
|
|
236
|
+
? { ...observed, ...(event.total_cost_usd !== undefined ? { cost: { total: event.total_cost_usd } } : {}) } : undefined,
|
|
237
|
+
} as Message;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function resolveClaudeCodeResultModel(event: ClaudeCodeResultEvent): string | undefined {
|
|
241
|
+
const keys = Object.keys(event.modelUsage ?? {});
|
|
242
|
+
return keys.find((key) => !key.includes("haiku")) ?? keys[0];
|
|
243
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import { createMutationCompletionTracker } from "./mutating-tool-guard.ts";
|
|
3
|
+
|
|
4
|
+
export type CompletionPolicy = "none" | "mutation-guard" | "acceptance-contract";
|
|
5
|
+
|
|
6
|
+
export function resolveCompletionPolicy(input: {
|
|
7
|
+
completionGuardEnabled: boolean;
|
|
8
|
+
usesAcceptanceContract: boolean;
|
|
9
|
+
}): CompletionPolicy {
|
|
10
|
+
if (input.usesAcceptanceContract) return "acceptance-contract";
|
|
11
|
+
return input.completionGuardEnabled ? "mutation-guard" : "none";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasCompletedMutationToolCall(messages: Message[]): boolean {
|
|
15
|
+
const mutations = createMutationCompletionTracker();
|
|
16
|
+
for (const message of messages) {
|
|
17
|
+
if (message.role === "assistant") {
|
|
18
|
+
for (const part of message.content) {
|
|
19
|
+
if (part.type !== "toolCall") continue;
|
|
20
|
+
const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
|
|
21
|
+
? part.arguments as Record<string, unknown>
|
|
22
|
+
: {};
|
|
23
|
+
mutations.recordToolStart({
|
|
24
|
+
id: typeof part.id === "string" ? part.id : undefined,
|
|
25
|
+
toolName: typeof part.name === "string" ? part.name : undefined,
|
|
26
|
+
args,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (message.role !== "toolResult") continue;
|
|
32
|
+
if (mutations.recordToolResult(message as { toolCallId?: unknown; toolName?: unknown; isError?: unknown })?.completedMutation) return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|