@pi-agents/orchid 0.1.0-beta.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
@@ -0,0 +1,40 @@
1
+ interface TimerApi {
2
+ setTimeout(handler: () => void, delayMs: number): unknown;
3
+ clearTimeout(handle: unknown): void;
4
+ }
5
+
6
+ interface FileCoalescer {
7
+ schedule(file: string, delayMs?: number): boolean;
8
+ clear(): void;
9
+ }
10
+
11
+ const defaultTimerApi: TimerApi = {
12
+ setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
13
+ clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
14
+ };
15
+
16
+ export function createFileCoalescer(
17
+ handler: (file: string) => void,
18
+ defaultDelayMs: number,
19
+ timerApi: TimerApi = defaultTimerApi,
20
+ ): FileCoalescer {
21
+ const pending = new Map<string, unknown>();
22
+
23
+ return {
24
+ schedule(file: string, delayMs = defaultDelayMs): boolean {
25
+ if (pending.has(file)) return false;
26
+ const timer = timerApi.setTimeout(() => {
27
+ pending.delete(file);
28
+ handler(file);
29
+ }, delayMs);
30
+ pending.set(file, timer);
31
+ return true;
32
+ },
33
+ clear(): void {
34
+ for (const timer of pending.values()) {
35
+ timerApi.clearTimeout(timer);
36
+ }
37
+ pending.clear();
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,76 @@
1
+ import * as fs from "node:fs";
2
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
3
+
4
+ type SubagentExecutionContext = "fresh" | "fork";
5
+
6
+ interface ForkableSessionManager {
7
+ getSessionFile(): string | undefined;
8
+ getLeafId(): string | null;
9
+ getSessionDir?(): string;
10
+ openSession?: (path: string, sessionDir?: string) => { createBranchedSession(leafId: string): string | undefined };
11
+ }
12
+
13
+ interface ForkContextResolverOptions {
14
+ openSession?: (path: string, sessionDir?: string) => { createBranchedSession(leafId: string): string | undefined };
15
+ }
16
+
17
+ interface ForkContextResolver {
18
+ sessionFileForIndex(index?: number): string | undefined;
19
+ }
20
+
21
+ export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
22
+ return value === "fork" ? "fork" : "fresh";
23
+ }
24
+
25
+ export function createForkContextResolver(
26
+ sessionManager: ForkableSessionManager,
27
+ requestedContext: unknown,
28
+ options: ForkContextResolverOptions = {},
29
+ ): ForkContextResolver {
30
+ if (resolveSubagentContext(requestedContext) !== "fork") {
31
+ return {
32
+ sessionFileForIndex: () => undefined,
33
+ };
34
+ }
35
+
36
+ const parentSessionFile = sessionManager.getSessionFile();
37
+ if (!parentSessionFile) {
38
+ throw new Error("Forked subagent context requires a persisted parent session.");
39
+ }
40
+
41
+ const leafId = sessionManager.getLeafId();
42
+ if (!leafId) {
43
+ throw new Error("Forked subagent context requires a current leaf to fork from.");
44
+ }
45
+
46
+ const openSession = options.openSession
47
+ ?? sessionManager.openSession
48
+ ?? ((file: string, dir?: string) => SessionManager.open(file, dir));
49
+ const sessionDir = sessionManager.getSessionDir?.();
50
+ const cachedSessionFiles = new Map<number, string>();
51
+
52
+ return {
53
+ sessionFileForIndex(index = 0): string | undefined {
54
+ const cached = cachedSessionFiles.get(index);
55
+ if (cached) return cached;
56
+ try {
57
+ if (!fs.existsSync(parentSessionFile)) {
58
+ throw new Error(`Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`);
59
+ }
60
+ const sourceManager = openSession(parentSessionFile, sessionDir);
61
+ const sessionFile = sourceManager.createBranchedSession(leafId);
62
+ if (!sessionFile) {
63
+ throw new Error("Session manager did not return a forked session file.");
64
+ }
65
+ if (!fs.existsSync(sessionFile)) {
66
+ throw new Error(`Session manager returned a forked session file that does not exist: ${sessionFile}`);
67
+ }
68
+ cachedSessionFiles.set(index, sessionFile);
69
+ return sessionFile;
70
+ } catch (error) {
71
+ const cause = error instanceof Error ? error : new Error(String(error));
72
+ throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
73
+ }
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Formatting utilities for display output
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import type { Usage, SingleResult } from "./types.ts";
8
+ import type { ChainStep } from "./settings.ts";
9
+ import { isParallelStep } from "./settings.ts";
10
+ import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
11
+
12
+ /**
13
+ * Format token count with k suffix for large numbers
14
+ */
15
+ export function formatTokens(n: number): string {
16
+ return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
17
+ }
18
+
19
+ export function formatModelThinking(model?: string, thinking?: string): string {
20
+ const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
21
+ let displayModel = parsed?.baseModel ?? model;
22
+ const explicitThinking = THINKING_LEVELS.find((level) => level === thinking?.trim());
23
+ const displayThinking = parsed?.thinkingSuffix ? parsed.thinkingSuffix.slice(1) : explicitThinking;
24
+ if (displayModel) {
25
+ const slashIdx = displayModel.lastIndexOf("/");
26
+ if (slashIdx !== -1) displayModel = displayModel.slice(slashIdx + 1);
27
+ }
28
+ return [displayModel, displayThinking ? `thinking ${displayThinking}` : undefined].filter(Boolean).join(" · ");
29
+ }
30
+
31
+ /**
32
+ * Format usage statistics into a compact string
33
+ */
34
+ export function formatUsage(u: Usage, model?: string): string {
35
+ const parts: string[] = [];
36
+ if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
37
+ if (u.input) parts.push(`in:${formatTokens(u.input)}`);
38
+ if (u.output) parts.push(`out:${formatTokens(u.output)}`);
39
+ if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
40
+ if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
41
+ if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
42
+ if (model) parts.push(model);
43
+ return parts.join(" ");
44
+ }
45
+
46
+ /**
47
+ * Format duration in human-readable form
48
+ */
49
+ export function formatDuration(ms: number): string {
50
+ if (ms < 1000) return `${ms}ms`;
51
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
52
+ return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
53
+ }
54
+
55
+ /**
56
+ * Build a summary string for a completed/failed chain
57
+ */
58
+ export function buildChainSummary(
59
+ steps: ChainStep[],
60
+ results: SingleResult[],
61
+ chainDir: string,
62
+ status: "completed" | "failed",
63
+ failedStep?: { index: number; error: string },
64
+ ): string {
65
+ const stepNames = steps
66
+ .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : step.agent))
67
+ .join(" → ");
68
+
69
+ const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
70
+ const durationStr = formatDuration(totalDuration);
71
+
72
+ const progressPath = path.join(chainDir, "progress.md");
73
+ const hasProgress = fs.existsSync(progressPath);
74
+ const allSkills = new Set<string>();
75
+ for (const r of results) {
76
+ if (r.skills) r.skills.forEach((s) => allSkills.add(s));
77
+ }
78
+ const skillsLine = allSkills.size > 0 ? `🔧 Skills: ${[...allSkills].join(", ")}` : "";
79
+
80
+ if (status === "completed") {
81
+ const stepWord = results.length === 1 ? "step" : "steps";
82
+ return `✅ Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})${skillsLine ? `\n${skillsLine}` : ""}
83
+
84
+ 📋 Progress: ${hasProgress ? progressPath : "(none)"}
85
+ 📁 Artifacts: ${chainDir}`;
86
+ } else {
87
+ const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
88
+ const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
89
+ return `❌ Chain failed${stepInfo}${errorInfo}${skillsLine ? `\n${skillsLine}` : ""}
90
+
91
+ 📋 Progress: ${hasProgress ? progressPath : "(none)"}
92
+ 📁 Artifacts: ${chainDir}`;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Format a tool call for display
98
+ */
99
+ export function formatToolCall(name: string, args: Record<string, unknown>, expanded = false): string {
100
+ switch (name) {
101
+ case "bash": {
102
+ const command = typeof args.command === "string" ? args.command : "";
103
+ const maxLength = expanded ? 240 : 60;
104
+ return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
105
+ }
106
+ case "read":
107
+ case "write":
108
+ case "edit": {
109
+ const target = typeof args.path === "string"
110
+ ? args.path
111
+ : typeof args.file_path === "string"
112
+ ? args.file_path
113
+ : "";
114
+ return `${name} ${shortenPath(target)}`;
115
+ }
116
+ default: {
117
+ const s = JSON.stringify(args);
118
+ const maxLength = expanded ? 160 : 40;
119
+ return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Shorten a path by replacing home directory with ~
126
+ */
127
+ export function shortenPath(p: string): string {
128
+ const home = process.env.HOME;
129
+ if (home && p.startsWith(home)) {
130
+ return `~${p.slice(home.length)}`;
131
+ }
132
+ return p;
133
+ }
@@ -0,0 +1,81 @@
1
+ import * as fs from "node:fs";
2
+
3
+ export interface DrainableSource {
4
+ pause(): void;
5
+ resume(): void;
6
+ }
7
+
8
+ export interface JsonlWriteStream {
9
+ write(chunk: string): boolean;
10
+ once(event: "drain", listener: () => void): JsonlWriteStream;
11
+ end(callback?: () => void): void;
12
+ }
13
+
14
+ const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
15
+
16
+ interface JsonlWriterDeps {
17
+ createWriteStream?: (filePath: string) => JsonlWriteStream;
18
+ maxBytes?: number;
19
+ }
20
+
21
+ interface JsonlWriter {
22
+ writeLine(line: string): void;
23
+ close(): Promise<void>;
24
+ }
25
+
26
+ export function createJsonlWriter(
27
+ filePath: string | undefined,
28
+ source: DrainableSource,
29
+ deps: JsonlWriterDeps = {},
30
+ ): JsonlWriter {
31
+ if (!filePath) {
32
+ return {
33
+ writeLine() {},
34
+ async close() {},
35
+ };
36
+ }
37
+
38
+ const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
39
+ let stream: JsonlWriteStream | undefined;
40
+ try {
41
+ stream = createWriteStream(filePath);
42
+ } catch {
43
+ return {
44
+ writeLine() {},
45
+ async close() {},
46
+ };
47
+ }
48
+
49
+ let backpressured = false;
50
+ let closed = false;
51
+ let bytesWritten = 0;
52
+ const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
53
+
54
+ return {
55
+ writeLine(line: string) {
56
+ if (!stream || closed || !line.trim()) return;
57
+ const chunk = `${line}\n`;
58
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
59
+ if (bytesWritten + chunkBytes > maxBytes) return;
60
+ try {
61
+ const ok = stream.write(chunk);
62
+ bytesWritten += chunkBytes;
63
+ if (!ok && !backpressured) {
64
+ backpressured = true;
65
+ source.pause();
66
+ stream.once("drain", () => {
67
+ backpressured = false;
68
+ if (!closed) source.resume();
69
+ });
70
+ }
71
+ } catch {}
72
+ },
73
+ async close() {
74
+ if (!stream || closed) return;
75
+ closed = true;
76
+ const current = stream;
77
+ stream = undefined;
78
+ await new Promise<void>((resolve) => current.end(() => resolve()));
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,78 @@
1
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
2
+ export type ThinkingLevel = typeof THINKING_LEVELS[number];
3
+ export type ThinkingLevelMap = Partial<Record<ThinkingLevel, string | null>>;
4
+
5
+ export interface ModelInfo {
6
+ provider: string;
7
+ id: string;
8
+ fullId: string;
9
+ reasoning?: boolean;
10
+ thinkingLevelMap?: ThinkingLevelMap;
11
+ }
12
+
13
+ interface RegistryModelLike {
14
+ provider: string;
15
+ id: string;
16
+ reasoning?: boolean;
17
+ thinkingLevelMap?: ThinkingLevelMap;
18
+ }
19
+
20
+ export function toModelInfo(model: RegistryModelLike): ModelInfo {
21
+ return {
22
+ provider: model.provider,
23
+ id: model.id,
24
+ fullId: `${model.provider}/${model.id}`,
25
+ reasoning: model.reasoning,
26
+ thinkingLevelMap: model.thinkingLevelMap,
27
+ };
28
+ }
29
+
30
+ /** Resolve the effective thinking level from a model string (which may contain a known suffix like `:high`)
31
+ * and an explicit thinking config value. Returns `undefined` when no thinking is applicable
32
+ * (e.g. no model was specified, or the model has no suffix and no config was provided). */
33
+ export function resolveEffectiveThinking(model: string | undefined, configThinking: string | undefined): string | undefined {
34
+ if (!model) return undefined;
35
+ const { thinkingSuffix } = splitKnownThinkingSuffix(model);
36
+ if (thinkingSuffix) return thinkingSuffix.slice(1);
37
+ return THINKING_LEVELS.find((level) => level === configThinking);
38
+ }
39
+
40
+ export function splitKnownThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
41
+ const colonIdx = model.lastIndexOf(":");
42
+ if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
43
+ const suffix = THINKING_LEVELS.find((level) => level === model.substring(colonIdx + 1));
44
+ if (!suffix) return { baseModel: model, thinkingSuffix: "" };
45
+ return {
46
+ baseModel: model.substring(0, colonIdx),
47
+ thinkingSuffix: `:${suffix}`,
48
+ };
49
+ }
50
+
51
+ export function findModelInfo(model: string | undefined, availableModels: ModelInfo[] | undefined, preferredProvider?: string): ModelInfo | undefined {
52
+ if (!model || !availableModels || availableModels.length === 0) return undefined;
53
+ const { baseModel } = splitKnownThinkingSuffix(model);
54
+ const exact = availableModels.find((entry) => entry.fullId === baseModel);
55
+ if (exact) return exact;
56
+
57
+ const matches = availableModels.filter((entry) => entry.id === baseModel);
58
+ if (preferredProvider) {
59
+ const preferred = matches.find((entry) => entry.provider === preferredProvider);
60
+ if (preferred) return preferred;
61
+ }
62
+ return matches.length === 1 ? matches[0] : undefined;
63
+ }
64
+
65
+ export function getSupportedThinkingLevels(model: ModelInfo | undefined): ThinkingLevel[] {
66
+ if (!model) return [...THINKING_LEVELS];
67
+ if (model.reasoning === false) return ["off"];
68
+
69
+ if (!model.thinkingLevelMap) return [...THINKING_LEVELS];
70
+
71
+ const levels = THINKING_LEVELS.filter((level) => {
72
+ const mapped = model.thinkingLevelMap?.[level];
73
+ if (mapped === null) return false;
74
+ if (level === "xhigh") return mapped !== undefined;
75
+ return true;
76
+ });
77
+ return levels;
78
+ }
@@ -0,0 +1,85 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+
3
+ interface PostExitStdioGuardOptions {
4
+ idleMs: number;
5
+ hardMs: number;
6
+ }
7
+
8
+ interface ChildWithPipedStdio {
9
+ stdout: ChildProcess["stdout"];
10
+ stderr: ChildProcess["stderr"];
11
+ on: ChildProcess["on"];
12
+ }
13
+
14
+ interface ChildWithKill {
15
+ kill(signal?: NodeJS.Signals | number): boolean;
16
+ }
17
+
18
+ export function trySignalChild(child: ChildWithKill, signal: NodeJS.Signals): boolean {
19
+ try {
20
+ return child.kill(signal);
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ export function attachPostExitStdioGuard(
27
+ child: ChildWithPipedStdio,
28
+ options: PostExitStdioGuardOptions,
29
+ ): () => void {
30
+ const { idleMs, hardMs } = options;
31
+ let exited = false;
32
+ let stdoutEnded = false;
33
+ let stderrEnded = false;
34
+ let idleTimer: NodeJS.Timeout | undefined;
35
+ let hardTimer: NodeJS.Timeout | undefined;
36
+
37
+ const destroyUnendedStdio = () => {
38
+ if (!stdoutEnded) {
39
+ try { child.stdout?.destroy(); } catch {}
40
+ }
41
+ if (!stderrEnded) {
42
+ try { child.stderr?.destroy(); } catch {}
43
+ }
44
+ };
45
+
46
+ const clearTimers = () => {
47
+ if (idleTimer) {
48
+ clearTimeout(idleTimer);
49
+ idleTimer = undefined;
50
+ }
51
+ if (hardTimer) {
52
+ clearTimeout(hardTimer);
53
+ hardTimer = undefined;
54
+ }
55
+ };
56
+
57
+ const armIdleTimer = () => {
58
+ if (!exited) return;
59
+ if (idleTimer) clearTimeout(idleTimer);
60
+ idleTimer = setTimeout(destroyUnendedStdio, idleMs);
61
+ idleTimer.unref?.();
62
+ };
63
+
64
+ child.stdout?.on("data", armIdleTimer);
65
+ child.stderr?.on("data", armIdleTimer);
66
+ child.stdout?.on("end", () => {
67
+ stdoutEnded = true;
68
+ if (stdoutEnded && stderrEnded) clearTimers();
69
+ });
70
+ child.stderr?.on("end", () => {
71
+ stderrEnded = true;
72
+ if (stdoutEnded && stderrEnded) clearTimers();
73
+ });
74
+ child.on("exit", () => {
75
+ exited = true;
76
+ armIdleTimer();
77
+ if (hardTimer) return;
78
+ hardTimer = setTimeout(destroyUnendedStdio, hardMs);
79
+ hardTimer.unref?.();
80
+ });
81
+ child.on("close", clearTimers);
82
+ child.on("error", clearTimers);
83
+
84
+ return clearTimers;
85
+ }
@@ -0,0 +1,10 @@
1
+ interface SessionIdentityManager {
2
+ getSessionFile(): string | null | undefined;
3
+ getSessionId(): string | null | undefined;
4
+ }
5
+
6
+ export function resolveCurrentSessionId(sessionManager: SessionIdentityManager): string {
7
+ const sessionId = sessionManager.getSessionFile() ?? sessionManager.getSessionId();
8
+ if (!sessionId) throw new Error("Current session identity is unavailable.");
9
+ return sessionId;
10
+ }
@@ -0,0 +1,44 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TokenUsage } from "./types.ts";
4
+
5
+ function findLatestSessionFile(sessionDir: string): string | null {
6
+ try {
7
+ const files = fs.readdirSync(sessionDir)
8
+ .filter((f) => f.endsWith(".jsonl"))
9
+ .map((f) => path.join(sessionDir, f));
10
+ if (files.length === 0) return null;
11
+ files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
12
+ return files[0] ?? null;
13
+ } catch {
14
+ // Session token lookup is optional metadata.
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function parseSessionTokens(sessionDir: string): TokenUsage | null {
20
+ const sessionFile = findLatestSessionFile(sessionDir);
21
+ if (!sessionFile) return null;
22
+ try {
23
+ const content = fs.readFileSync(sessionFile, "utf-8");
24
+ let input = 0;
25
+ let output = 0;
26
+ for (const line of content.split("\n")) {
27
+ if (!line.trim()) continue;
28
+ try {
29
+ const entry = JSON.parse(line);
30
+ const usage = entry.usage ?? entry.message?.usage;
31
+ if (usage) {
32
+ input += usage.inputTokens ?? usage.input ?? 0;
33
+ output += usage.outputTokens ?? usage.output ?? 0;
34
+ }
35
+ } catch {
36
+ // Ignore malformed lines while scanning usage entries.
37
+ }
38
+ }
39
+ return { input, output, total: input + output };
40
+ } catch {
41
+ // Usage extraction should not fail the run.
42
+ return null;
43
+ }
44
+ }