indusagi-coding-agent 0.2.8 → 0.2.9

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 (219) hide show
  1. package/package.json +2 -1
  2. package/src/_decls/entry.ts +18 -0
  3. package/src/_decls/guardrails.ts +35 -0
  4. package/src/_decls/index.ts +26 -0
  5. package/src/addons/contract.ts +236 -0
  6. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  7. package/src/addons/dispatch/index.ts +25 -0
  8. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  9. package/src/addons/host.ts +225 -0
  10. package/src/addons/index.ts +112 -0
  11. package/src/addons/manifest.ts +158 -0
  12. package/src/addons/sandbox.ts +170 -0
  13. package/src/addons/surface.ts +78 -0
  14. package/src/boot/auth-vault.ts +195 -0
  15. package/src/boot/boot.ts +138 -0
  16. package/src/boot/contract.ts +238 -0
  17. package/src/boot/heap.ts +59 -0
  18. package/src/boot/index.ts +28 -0
  19. package/src/boot/invocation.ts +93 -0
  20. package/src/boot/runners/addon-wiring.ts +153 -0
  21. package/src/boot/runners/checkpoint.ts +169 -0
  22. package/src/boot/runners/delegate-runner.ts +294 -0
  23. package/src/boot/runners/index.ts +13 -0
  24. package/src/boot/runners/link-runner.ts +45 -0
  25. package/src/boot/runners/memdir.ts +168 -0
  26. package/src/boot/runners/oneshot-runner.ts +58 -0
  27. package/src/boot/runners/read-state.ts +90 -0
  28. package/src/boot/runners/registry.ts +42 -0
  29. package/src/boot/runners/repl-runner.ts +143 -0
  30. package/src/boot/runners/server-mode.ts +121 -0
  31. package/src/boot/runners/session.ts +641 -0
  32. package/src/boot/server-token.ts +148 -0
  33. package/src/boot/stages.ts +167 -0
  34. package/src/boot/upgrade/apply.ts +94 -0
  35. package/src/boot/upgrade/index.ts +13 -0
  36. package/src/boot/upgrade/upgrades.ts +289 -0
  37. package/src/briefing/compose.ts +150 -0
  38. package/src/briefing/context-docs.ts +19 -0
  39. package/src/briefing/contract.ts +717 -0
  40. package/src/briefing/index.ts +31 -0
  41. package/src/briefing/macros.ts +97 -0
  42. package/src/briefing/skills.ts +47 -0
  43. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  44. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  45. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  46. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  47. package/src/capability-deck/builtin-bridge.ts +312 -0
  48. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  49. package/src/capability-deck/cards/index.ts +115 -0
  50. package/src/capability-deck/cards/memory-card.ts +146 -0
  51. package/src/capability-deck/cards/plan-file.ts +97 -0
  52. package/src/capability-deck/cards/plan-tools.ts +185 -0
  53. package/src/capability-deck/cards/saas-card.ts +183 -0
  54. package/src/capability-deck/cards/task-card.ts +207 -0
  55. package/src/capability-deck/cards/todo-card.ts +168 -0
  56. package/src/capability-deck/cards/workflow-card.ts +247 -0
  57. package/src/capability-deck/contract.ts +388 -0
  58. package/src/capability-deck/index.ts +48 -0
  59. package/src/capability-deck/manifest.ts +109 -0
  60. package/src/capability-deck/provision.ts +169 -0
  61. package/src/channels/contract.ts +191 -0
  62. package/src/channels/framer.ts +50 -0
  63. package/src/channels/index.ts +101 -0
  64. package/src/channels/link/dialog.ts +129 -0
  65. package/src/channels/link/driver.ts +190 -0
  66. package/src/channels/link/index.ts +34 -0
  67. package/src/channels/link/server.ts +134 -0
  68. package/src/channels/oneshot.ts +90 -0
  69. package/src/channels/ops.ts +65 -0
  70. package/src/channels/session-ops.ts +81 -0
  71. package/src/conductor/bash-guard.ts +599 -0
  72. package/src/conductor/catalog/catalog.ts +116 -0
  73. package/src/conductor/catalog/index.ts +8 -0
  74. package/src/conductor/catalog/matcher.ts +134 -0
  75. package/src/conductor/conductor.ts +234 -0
  76. package/src/conductor/contract.ts +842 -0
  77. package/src/conductor/diagnostics.ts +227 -0
  78. package/src/conductor/index.ts +33 -0
  79. package/src/conductor/permissions.ts +588 -0
  80. package/src/conductor/quota-error.ts +49 -0
  81. package/src/conductor/signal-hub/hub.ts +46 -0
  82. package/src/conductor/signal-hub/index.ts +2 -0
  83. package/src/conductor/signal-hub/translate.test.ts +81 -0
  84. package/src/conductor/signal-hub/translate.ts +74 -0
  85. package/src/conductor/skill-parse/index.ts +2 -0
  86. package/src/conductor/skill-parse/parse.ts +108 -0
  87. package/src/conductor/transcript-store/index.ts +22 -0
  88. package/src/conductor/transcript-store/serialize.ts +116 -0
  89. package/src/conductor/transcript-store/store.ts +205 -0
  90. package/src/console/auth-status.ts +56 -0
  91. package/src/console/components/AgentsView.ts +165 -0
  92. package/src/console/components/BackgroundAgents.ts +155 -0
  93. package/src/console/components/Banner.ts +334 -0
  94. package/src/console/components/Composer.ts +94 -0
  95. package/src/console/components/StatusBar.ts +49 -0
  96. package/src/console/components/TerminalConsole.ts +1090 -0
  97. package/src/console/components/WorkingIndicator.ts +98 -0
  98. package/src/console/components/banner-sweep.ts +24 -0
  99. package/src/console/components/welcome.ts +74 -0
  100. package/src/console/contract.ts +630 -0
  101. package/src/console/index.ts +34 -0
  102. package/src/console/input/complete.ts +127 -0
  103. package/src/console/input/dir-reader.ts +34 -0
  104. package/src/console/input/index.ts +23 -0
  105. package/src/console/input/keymap.ts +159 -0
  106. package/src/console/input/paste.ts +104 -0
  107. package/src/console/mount.ts +56 -0
  108. package/src/console/overlays/approval-queue.ts +57 -0
  109. package/src/console/overlays/approval.ts +130 -0
  110. package/src/console/overlays/auth.ts +342 -0
  111. package/src/console/overlays/boards.ts +308 -0
  112. package/src/console/overlays/host.ts +36 -0
  113. package/src/console/overlays/index.ts +26 -0
  114. package/src/console/overlays/pickers.ts +258 -0
  115. package/src/console/overlays/sessions.ts +190 -0
  116. package/src/console/reducer.ts +182 -0
  117. package/src/console/slash/builtins.ts +81 -0
  118. package/src/console/slash/commands/dynamic.ts +83 -0
  119. package/src/console/slash/commands/integrations.ts +695 -0
  120. package/src/console/slash/commands/shared.ts +75 -0
  121. package/src/console/slash/commands/transcript.ts +263 -0
  122. package/src/console/slash/commands/workbench.ts +246 -0
  123. package/src/console/slash/index.ts +15 -0
  124. package/src/console/slash/registry.ts +70 -0
  125. package/src/console/slash/resolve.ts +63 -0
  126. package/src/console/startup.ts +209 -0
  127. package/src/console/theme/adapter.ts +45 -0
  128. package/src/console/theme/index.ts +7 -0
  129. package/src/console/theme/palette.ts +68 -0
  130. package/src/console/theme/resolve.ts +39 -0
  131. package/src/console/theme/tokens.ts +71 -0
  132. package/src/entry.ts +55 -0
  133. package/src/guardrails.ts +37 -0
  134. package/src/index.ts +18 -0
  135. package/src/insight/channel.ts +88 -0
  136. package/src/insight/contract.ts +185 -0
  137. package/src/insight/index.ts +110 -0
  138. package/src/insight/recorder.ts +213 -0
  139. package/src/insight/redaction.ts +157 -0
  140. package/src/insight/replay.ts +158 -0
  141. package/src/insight/sampling.ts +70 -0
  142. package/src/insight/serialize.ts +50 -0
  143. package/src/insight/sinks/console.ts +64 -0
  144. package/src/insight/sinks/file.ts +40 -0
  145. package/src/insight/sinks/index.ts +24 -0
  146. package/src/insight/sinks/stream.ts +54 -0
  147. package/src/integrations/sarvam/attach.ts +239 -0
  148. package/src/integrations/sarvam/config.ts +156 -0
  149. package/src/integrations/sarvam/index.ts +25 -0
  150. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  151. package/src/integrations/sarvam/types.ts +27 -0
  152. package/src/integrations/zoho/attach.ts +342 -0
  153. package/src/integrations/zoho/config.ts +125 -0
  154. package/src/integrations/zoho/index.ts +27 -0
  155. package/src/integrations/zoho/types.ts +21 -0
  156. package/src/integrations/zoho/zoho.test.ts +50 -0
  157. package/src/kit/clipboard-image.ts +107 -0
  158. package/src/kit/external-editor.ts +48 -0
  159. package/src/kit/image.ts +59 -0
  160. package/src/kit/index.ts +51 -0
  161. package/src/kit/shell.ts +19 -0
  162. package/src/kit/tool-fetch.ts +85 -0
  163. package/src/launch/catalog.ts +148 -0
  164. package/src/launch/contract.ts +187 -0
  165. package/src/launch/credentials.ts +625 -0
  166. package/src/launch/index.ts +98 -0
  167. package/src/launch/invocation/attachments.ts +179 -0
  168. package/src/launch/invocation/flags.ts +196 -0
  169. package/src/launch/invocation/index.ts +25 -0
  170. package/src/launch/invocation/read.ts +260 -0
  171. package/src/launch/invocation/usage.ts +67 -0
  172. package/src/launch/login.ts +324 -0
  173. package/src/launch/oauth.test.ts +18 -0
  174. package/src/launch/oauth.ts +203 -0
  175. package/src/launch/packages.ts +194 -0
  176. package/src/launch/pickers.ts +189 -0
  177. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  178. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  179. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  180. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  181. package/src/runtime-bridge/bridges/index.ts +33 -0
  182. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  183. package/src/runtime-bridge/broker.ts +227 -0
  184. package/src/runtime-bridge/contract.ts +122 -0
  185. package/src/runtime-bridge/index.ts +79 -0
  186. package/src/runtime-bridge/sink.ts +180 -0
  187. package/src/sessions/contract.ts +81 -0
  188. package/src/sessions/index.ts +13 -0
  189. package/src/sessions/library.ts +229 -0
  190. package/src/settings/contract.ts +114 -0
  191. package/src/settings/index.ts +32 -0
  192. package/src/settings/manager.ts +117 -0
  193. package/src/transcript-export/index.ts +45 -0
  194. package/src/transcript-export/publish.ts +260 -0
  195. package/src/transcript-export/sgr.ts +315 -0
  196. package/src/transcript-export/template.ts +272 -0
  197. package/src/transcript-export/theme-bridge.ts +150 -0
  198. package/src/window-budget/budget/estimate.ts +135 -0
  199. package/src/window-budget/budget/gate.ts +33 -0
  200. package/src/window-budget/budget/index.ts +16 -0
  201. package/src/window-budget/budget/slice.ts +56 -0
  202. package/src/window-budget/condenser.ts +58 -0
  203. package/src/window-budget/contract.ts +184 -0
  204. package/src/window-budget/index.ts +19 -0
  205. package/src/window-budget/microcompact.ts +95 -0
  206. package/src/window-budget/rehydrate.ts +136 -0
  207. package/src/window-budget/summarize/condense.ts +103 -0
  208. package/src/window-budget/summarize/index.ts +14 -0
  209. package/src/window-budget/summarize/prompt.ts +149 -0
  210. package/src/workflow-engine/agent-runner.ts +181 -0
  211. package/src/workflow-engine/display.ts +224 -0
  212. package/src/workflow-engine/engine.ts +294 -0
  213. package/src/workflow-engine/index.ts +23 -0
  214. package/src/workflow-engine/parse.ts +172 -0
  215. package/src/workflow-engine/structured-output.ts +35 -0
  216. package/src/workspace/brand.ts +29 -0
  217. package/src/workspace/index.ts +18 -0
  218. package/src/workspace/locator.ts +103 -0
  219. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { getModels, getProviders } from "indusagi/ai";
3
+ import type { KnownProvider, Model } from "indusagi/ai";
4
+ import type { ModelCardRef } from "../contract.js";
5
+
6
+ export interface CatalogCard {
7
+ readonly id: string;
8
+ readonly provider: KnownProvider | string;
9
+ readonly modelId: string;
10
+ readonly name: string;
11
+ readonly reasoning: boolean;
12
+ readonly acceptsImages: boolean;
13
+ readonly contextTokens: number;
14
+ readonly model: Model<Model<string>["api"]>;
15
+ }
16
+
17
+ export interface CatalogSource {
18
+ readonly providers: () => readonly (KnownProvider | string)[];
19
+ readonly models: (provider: string) => readonly Model<string>[];
20
+ }
21
+
22
+ const rawModelGate = z.object({
23
+ id: z.string().min(1),
24
+ name: z.string().min(1),
25
+ provider: z.string().min(1),
26
+ reasoning: z.boolean().optional(),
27
+ input: z.array(z.string()).optional(),
28
+ contextWindow: z.number().optional(),
29
+ });
30
+
31
+ const EXCLUDED_PROVIDERS = new Set(["mock"]);
32
+ const frameworkSource: CatalogSource = {
33
+ providers: () => getProviders().filter((provider) => !EXCLUDED_PROVIDERS.has(String(provider))),
34
+ models: (provider) => getModels(provider as KnownProvider),
35
+ };
36
+
37
+ export function toCardRef(card: CatalogCard): ModelCardRef {
38
+ return {
39
+ id: card.id,
40
+ provider: card.provider,
41
+ modelId: card.modelId,
42
+ name: card.name,
43
+ reasoning: card.reasoning,
44
+ };
45
+ }
46
+
47
+ export function canonicalId(provider: string, modelId: string): string {
48
+ return `${provider}/${modelId}`;
49
+ }
50
+
51
+ function normalizeCard(raw: Model<string>): CatalogCard {
52
+ const provider = String(raw.provider);
53
+ const modelId = raw.id;
54
+ const inputs = Array.isArray(raw.input) ? raw.input : [];
55
+ return {
56
+ id: canonicalId(provider, modelId),
57
+ provider,
58
+ modelId,
59
+ name: raw.name,
60
+ reasoning: raw.reasoning === true,
61
+ acceptsImages: inputs.includes("image"),
62
+ contextTokens: typeof raw.contextWindow === "number" ? raw.contextWindow : 0,
63
+ model: raw as CatalogCard["model"],
64
+ };
65
+ }
66
+
67
+ function buildIndex(providers: readonly (KnownProvider | string)[], pull: CatalogSource["models"]): Map<string, CatalogCard> {
68
+ const index = new Map<string, CatalogCard>();
69
+ for (const provider of providers) {
70
+ let raws: readonly Model<string>[];
71
+ try {
72
+ raws = pull(String(provider));
73
+ } catch {
74
+ continue;
75
+ }
76
+ for (const raw of raws) {
77
+ if (!rawModelGate.safeParse(raw).success) continue;
78
+ const card = normalizeCard(raw);
79
+ if (!index.has(card.id)) index.set(card.id, card);
80
+ }
81
+ }
82
+ return index;
83
+ }
84
+
85
+ export class ModelCatalog {
86
+ readonly #index: Map<string, CatalogCard>;
87
+ readonly #byProvider: Map<string, CatalogCard[]>;
88
+
89
+ constructor(source: CatalogSource = frameworkSource) {
90
+ this.#index = buildIndex(source.providers(), source.models);
91
+ this.#byProvider = new Map();
92
+ for (const card of this.#index.values()) {
93
+ const bucket = this.#byProvider.get(card.provider);
94
+ if (bucket) bucket.push(card);
95
+ else this.#byProvider.set(card.provider, [card]);
96
+ }
97
+ }
98
+
99
+ get size(): number { return this.#index.size; }
100
+ all(): readonly CatalogCard[] { return [...this.#index.values()]; }
101
+ byProvider(provider: KnownProvider | string): readonly CatalogCard[] { return this.#byProvider.get(String(provider)) ?? []; }
102
+ providers(): readonly string[] { return [...this.#byProvider.keys()]; }
103
+
104
+ get(id: string): CatalogCard | undefined {
105
+ const direct = this.#index.get(id);
106
+ if (direct) return direct;
107
+ if (!id.includes("/")) {
108
+ const matches = [...this.#index.values()].filter((card) => card.modelId === id);
109
+ if (matches.length === 1) return matches[0];
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ has(id: string): boolean { return this.get(id) !== undefined; }
115
+ refreshed(source: CatalogSource = frameworkSource): ModelCatalog { return new ModelCatalog(source); }
116
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ ModelCatalog,
3
+ canonicalId,
4
+ toCardRef,
5
+ type CatalogCard,
6
+ type CatalogSource,
7
+ } from "./catalog.js";
8
+ export { ModelMatcher, type ResolveInput } from "./matcher.js";
@@ -0,0 +1,134 @@
1
+ import type { MatchQuery, ModelCardRef } from "../contract.js";
2
+ import { toCardRef } from "./catalog.js";
3
+ import type { CatalogCard, ModelCatalog } from "./catalog.js";
4
+
5
+ export type ResolveInput = string | MatchQuery;
6
+
7
+ type Scored = { card: CatalogCard; score: number; bonus: number };
8
+ const SCALE = {
9
+ exactCanonical: 900,
10
+ exactModelId: 800,
11
+ providerScoped: 600,
12
+ globPattern: 400,
13
+ aliasSubstring: 200,
14
+ };
15
+ const GLOB_CACHE = new Map<string, RegExp>();
16
+
17
+ function liftQuery(input: ResolveInput): MatchQuery {
18
+ return typeof input === "string" ? { pattern: input } : input;
19
+ }
20
+
21
+ function clearsConstraints(card: CatalogCard, query: MatchQuery): boolean {
22
+ if (query.provider !== undefined && card.provider !== String(query.provider)) return false;
23
+ if (query.modelId !== undefined && card.modelId !== query.modelId) return false;
24
+ if (query.reasoning === true && !card.reasoning) return false;
25
+ if (query.supportsImageInput === true && !card.acceptsImages) return false;
26
+ return true;
27
+ }
28
+
29
+ function looksGlobby(pattern: string): boolean {
30
+ return /[*?[\]]/.test(pattern);
31
+ }
32
+
33
+ function compileGlob(pattern: string): RegExp | undefined {
34
+ const cached = GLOB_CACHE.get(pattern);
35
+ if (cached) return cached;
36
+ let body = "";
37
+ for (let i = 0; i < pattern.length; i++) {
38
+ const ch = pattern[i];
39
+ if (ch === "*") body += "[^/]*";
40
+ else if (ch === "?") body += "[^/]";
41
+ else if (ch === "[") {
42
+ const close = pattern.indexOf("]", i + 1);
43
+ if (close === -1) body += "\\[";
44
+ else {
45
+ body += `[${pattern.slice(i + 1, close).replace(/\\/g, "\\\\")}]`;
46
+ i = close;
47
+ }
48
+ } else body += ch.replace(/[.+^${}()|\\]/g, "\\$&");
49
+ }
50
+ try {
51
+ const regexp = new RegExp(`^${body}$`, "i");
52
+ GLOB_CACHE.set(pattern, regexp);
53
+ return regexp;
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ function probePattern(card: CatalogCard, pattern: string): number {
60
+ const needle = pattern.trim();
61
+ if (needle === "") return 0;
62
+ const folded = needle.toLowerCase();
63
+ if (card.id === needle) return SCALE.exactCanonical;
64
+ if (card.modelId === needle) return SCALE.exactModelId;
65
+ const providerPrefix = `${card.provider}/`;
66
+ if (needle === providerPrefix || folded === card.provider.toLowerCase()) return SCALE.providerScoped;
67
+ if (looksGlobby(needle)) {
68
+ const regexp = compileGlob(needle);
69
+ if (regexp && (regexp.test(card.id) || regexp.test(card.modelId))) return SCALE.globPattern;
70
+ }
71
+ if (card.id.toLowerCase().includes(folded) || card.name.toLowerCase().includes(folded)) return SCALE.aliasSubstring;
72
+ return 0;
73
+ }
74
+
75
+ function tieBonus(card: CatalogCard, pattern: string): number {
76
+ const needle = pattern.trim().toLowerCase();
77
+ let bonus = 0;
78
+ if (needle.length > 0) {
79
+ const slack = Math.abs(card.id.length - needle.length);
80
+ bonus += Math.max(0, 40 - Math.min(slack, 40));
81
+ }
82
+ if (card.reasoning) bonus += 3;
83
+ return bonus;
84
+ }
85
+
86
+ function rankCandidates(a: Scored, b: Scored): number {
87
+ if (a.score !== b.score) return b.score - a.score;
88
+ if (a.bonus !== b.bonus) return b.bonus - a.bonus;
89
+ return a.card.id.localeCompare(b.card.id);
90
+ }
91
+
92
+ export class ModelMatcher {
93
+ readonly #catalog: ModelCatalog;
94
+
95
+ constructor(catalog: ModelCatalog) { this.#catalog = catalog; }
96
+
97
+ resolve(input: ResolveInput): ModelCardRef | undefined {
98
+ const card = this.resolveCard(input);
99
+ return card ? toCardRef(card) : undefined;
100
+ }
101
+
102
+ resolveCard(input: ResolveInput): CatalogCard | undefined {
103
+ const query = liftQuery(input);
104
+ if (query.provider !== undefined && query.modelId !== undefined) {
105
+ const pinned = this.#catalog.get(`${String(query.provider)}/${query.modelId}`);
106
+ if (pinned && clearsConstraints(pinned, query)) return pinned;
107
+ }
108
+ const field = this.#catalog.all().filter((card) => clearsConstraints(card, query));
109
+ if (field.length === 0) return undefined;
110
+ const pattern = query.pattern?.trim();
111
+ if (!pattern) return field.find((card) => card.reasoning) ?? field[0];
112
+ const scored: Scored[] = [];
113
+ for (const card of field) {
114
+ const score = probePattern(card, pattern);
115
+ if (score > 0) scored.push({ card, score, bonus: tieBonus(card, pattern) });
116
+ }
117
+ scored.sort(rankCandidates);
118
+ return scored[0]?.card;
119
+ }
120
+
121
+ resolveAll(input: ResolveInput): readonly ModelCardRef[] {
122
+ const query = liftQuery(input);
123
+ const field = this.#catalog.all().filter((card) => clearsConstraints(card, query));
124
+ const pattern = query.pattern?.trim();
125
+ if (!pattern) return field.map(toCardRef);
126
+ const scored: Scored[] = [];
127
+ for (const card of field) {
128
+ const score = probePattern(card, pattern);
129
+ if (score > 0) scored.push({ card, score, bonus: tieBonus(card, pattern) });
130
+ }
131
+ scored.sort(rankCandidates);
132
+ return scored.map(({ card }) => toCardRef(card));
133
+ }
134
+ }
@@ -0,0 +1,234 @@
1
+ import { Agent } from "indusagi/agent";
2
+ import type { AgentEvent, AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel } from "indusagi/agent";
3
+ import { createZeroUsage } from "indusagi/ai";
4
+ import type { Model, Usage } from "indusagi/ai";
5
+ import * as path from "node:path";
6
+ import { conductorFault } from "./contract.js";
7
+ import type { ApprovalResolver, ConductorFault, ConductorPhase, ConductorState, PermissionMode, SessionConductor, SessionConductorOptions, SessionHead } from "./contract.js";
8
+ import { ModelCatalog, ModelMatcher } from "./catalog/index.js";
9
+ import type { CatalogCard } from "./catalog/index.js";
10
+ import { SignalHub, translateAgentEvent } from "./signal-hub/index.js";
11
+ import { fsBackend, TranscriptStore } from "./transcript-store/index.js";
12
+ import { parseSkillInvocation } from "./skill-parse/index.js";
13
+ import { isQuotaFault, quotaErrorMessage } from "./quota-error.js";
14
+ import { writePlan } from "../capability-deck/cards/plan-file.js";
15
+ import { AUTO_CONDENSE_POLICY, isOverBudget } from "../window-budget/index.js";
16
+ import { DiagnosticsEngine, formatDiagnosticsSummary, hasEslintConfig, hasTsConfig, isTypeScriptOrJs, runEslintDiagnostics, runTscDiagnostics } from "./diagnostics.js";
17
+ import type { DiagnosticsConfig } from "./diagnostics.js";
18
+
19
+ export interface AgentLike {
20
+ subscribe(fn: (event: AgentEvent) => void): () => void;
21
+ prompt(input: string): Promise<void>;
22
+ abort(): void;
23
+ readonly state: { readonly messages: AgentMessage[]; readonly model: Model<any>; readonly isStreaming: boolean };
24
+ replaceMessages(messages: AgentMessage[]): void;
25
+ setModel(model: Model<any>): void;
26
+ setSystemPrompt(prompt: string): void;
27
+ setTools(tools: AgentTool[]): void;
28
+ setThinkingLevel?(level: ThinkingLevel): void;
29
+ }
30
+ export interface CondenseOpts { readonly force?: boolean; readonly model?: Model<any>; readonly contextTokens?: number; readonly signal?: AbortSignal; readonly apiKey?: string }
31
+ export type CondenseFn = (messages: AgentMessage[], opts?: CondenseOpts) => AgentMessage[] | Promise<AgentMessage[]>;
32
+ export const noopCondense: CondenseFn = (messages) => messages;
33
+ export interface RetryPolicy { readonly maxAttempts: number; readonly baseDelayMs: number }
34
+ export interface ConductorDeps {
35
+ readonly agent?: AgentLike;
36
+ readonly store?: TranscriptStore;
37
+ readonly hub?: SignalHub;
38
+ readonly matcher?: ModelMatcher;
39
+ readonly condense?: CondenseFn;
40
+ readonly retry?: RetryPolicy;
41
+ readonly compactAt?: number;
42
+ readonly sleep?: (ms: number) => Promise<void>;
43
+ readonly diagnostics?: DiagnosticsEngine | DiagnosticsConfig;
44
+ }
45
+ type StateAction =
46
+ | { readonly type: "phase"; readonly phase: ConductorPhase }
47
+ | { readonly type: "head"; readonly head: SessionHead }
48
+ | { readonly type: "usage"; readonly usage: Usage; readonly contextTokens: number }
49
+ | { readonly type: "model"; readonly modelId: string }
50
+ | { readonly type: "fault"; readonly fault: ConductorFault }
51
+ | { readonly type: "settled" };
52
+
53
+ export function reduceState(prev: ConductorState, action: StateAction): ConductorState {
54
+ switch (action.type) {
55
+ case "phase": return { ...prev, phase: action.phase };
56
+ case "head": return { ...prev, head: action.head };
57
+ case "usage": return { ...prev, usage: action.usage, contextTokens: action.contextTokens };
58
+ case "model": return { ...prev, modelId: action.modelId };
59
+ case "fault": return { ...prev, phase: "faulted", fault: action.fault };
60
+ case "settled": { const { fault: _fault, ...rest } = prev; return { ...rest, phase: "idle" }; }
61
+ }
62
+ }
63
+
64
+ const DEFAULT_RETRY = { maxAttempts: 2, baseDelayMs: 250 };
65
+ const THINKING_LADDER: ThinkingLevel[] = ["off", "low", "medium", "high"];
66
+ const PERMISSION_CYCLE: PermissionMode[] = ["default", "acceptEdits", "plan", "bypass"];
67
+
68
+ function newSessionId(): string { return `s_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; }
69
+ function contextTokensOf(usage: Usage): number { return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite; }
70
+ function accumulateUsage(a: Usage, b: Usage): Usage {
71
+ return { input: a.input + b.input, output: a.output + b.output, cacheRead: a.cacheRead + b.cacheRead, cacheWrite: a.cacheWrite + b.cacheWrite, totalTokens: a.totalTokens + b.totalTokens, cost: { input: a.cost.input + b.cost.input, output: a.cost.output + b.cost.output, cacheRead: a.cost.cacheRead + b.cost.cacheRead, cacheWrite: a.cost.cacheWrite + b.cost.cacheWrite, total: a.cost.total + b.cost.total } };
72
+ }
73
+ function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); }
74
+ function isTransient(error: unknown): boolean { const text = errorText(error).toLowerCase(); return ["429", "rate limit", "overloaded", "timeout", "timed out", "econnreset", "etimedout", "503", "502", "500", "unavailable", "temporarily"].some((part) => text.includes(part)); }
75
+ function isOverloadFault(error: unknown): boolean { const text = errorText(error).toLowerCase(); return text.includes("529") || text.includes("overload"); }
76
+
77
+ export function withGatewayBaseUrl<M extends { baseUrl: string; provider: string }>(model: M, gatewayBaseUrls: Record<string, string> | undefined): M {
78
+ const url = gatewayBaseUrls?.[model.provider];
79
+ return url ? { ...model, baseUrl: url } : model;
80
+ }
81
+
82
+ function makeAgent(model: Model<any>, options: SessionConductorOptions, gate?: CanUseToolFn): AgentLike {
83
+ return new Agent({ initialState: { model, ...(options.system !== undefined ? { systemPrompt: options.system } : {}), ...(options.tools ? { tools: options.tools } : {}), ...(options.thinking ? { thinkingLevel: options.thinking } : {}) }, ...(options.getApiKey ? { getApiKey: options.getApiKey } : {}), ...(gate ? { canUseTool: gate } : {}) }) as unknown as AgentLike;
84
+ }
85
+
86
+ class LazyAgent implements AgentLike {
87
+ #real?: AgentLike;
88
+ #model: Model<any>;
89
+ constructor(model: Model<any>, private readonly options: SessionConductorOptions, private readonly gate?: CanUseToolFn) {
90
+ this.#model = model;
91
+ }
92
+ #ensure(): AgentLike { return this.#real ??= makeAgent(this.#model, this.options, this.gate); }
93
+ subscribe(fn: (event: AgentEvent) => void) { return this.#ensure().subscribe(fn); }
94
+ prompt(input: string) { return this.#ensure().prompt(input); }
95
+ abort() { this.#ensure().abort(); }
96
+ get state() { return this.#ensure().state; }
97
+ replaceMessages(messages: AgentMessage[]) { this.#ensure().replaceMessages(messages); }
98
+ setModel(model: Model<any>) { this.#model = model; this.#ensure().setModel(model); }
99
+ setSystemPrompt(prompt: string) { this.#ensure().setSystemPrompt(prompt); }
100
+ setTools(tools: AgentTool[]) { this.#ensure().setTools(tools); }
101
+ setThinkingLevel(level: ThinkingLevel) { this.#ensure().setThinkingLevel?.(level); }
102
+ }
103
+
104
+ function resolveDiagnosticsEngine(workspace: string, dep: ConductorDeps["diagnostics"], liveDefault: boolean): DiagnosticsEngine | undefined {
105
+ if (dep instanceof DiagnosticsEngine) return dep.enabled ? dep : undefined;
106
+ if (dep?.enabled === false || (!dep && (!liveDefault || !hasTsConfig(workspace)))) return undefined;
107
+ const runners = dep?.runners ?? (hasEslintConfig(workspace) ? [runTscDiagnostics, (cwd: string, signal?: AbortSignal) => runEslintDiagnostics(["."], cwd, signal)] : [runTscDiagnostics]);
108
+ return new DiagnosticsEngine(workspace, { ...dep, runners });
109
+ }
110
+
111
+ export function createSessionConductor(options: SessionConductorOptions, deps: ConductorDeps = {}): SessionConductor {
112
+ const store = deps.store ?? new TranscriptStore(newSessionId(), options.sessionsDir ? { backend: fsBackend(options.sessionsDir) } : {});
113
+ const matcher = deps.matcher ?? (deps.agent ? undefined : new ModelMatcher(new ModelCatalog()));
114
+ const modeHolder = { mode: options.permissionMode ?? "default" as PermissionMode };
115
+ const approvalHolder: { resolver?: ApprovalResolver } = {};
116
+ const delegate: ApprovalResolver = async (...args) => approvalHolder.resolver ? approvalHolder.resolver(...args) : "unavailable";
117
+ const gate = options.canUseTool?.(() => modeHolder.mode, delegate);
118
+ let agent = deps.agent;
119
+ if (!agent) {
120
+ const card = matcher?.resolveCard(options.modelId);
121
+ if (!card) throw new Error(`createSessionConductor: no model matches "${options.modelId}"`);
122
+ agent = new LazyAgent(withGatewayBaseUrl(card.model, options.gatewayBaseUrls), options, gate);
123
+ }
124
+ return new SessionConductorImpl(options, { ...deps, matcher, diagnostics: resolveDiagnosticsEngine(options.workspace ?? process.cwd(), deps.diagnostics, !deps.agent) }, agent, store, modeHolder, approvalHolder);
125
+ }
126
+
127
+ class SessionConductorImpl implements SessionConductor {
128
+ #hub: SignalHub;
129
+ #matcher?: ModelMatcher;
130
+ #state: ConductorState;
131
+ #busy = false;
132
+ #draining = false;
133
+ #queue: { mode: "steer" | "followUp"; text: string }[] = [];
134
+ #boundModel?: Model<any>;
135
+ #thinking: ThinkingLevel;
136
+ #prePlanMode?: PermissionMode;
137
+ #sessionName?: string;
138
+ #gatewayBaseUrls?: Record<string, string>;
139
+ #condenseAbort?: AbortController;
140
+ #diagnostics?: DiagnosticsEngine;
141
+ #edited = new Set<string>();
142
+ #deckTools: AgentTool[];
143
+ #workspace: string;
144
+
145
+ constructor(private readonly options: SessionConductorOptions, private readonly deps: ConductorDeps, private readonly agent: AgentLike, private readonly store: TranscriptStore, private readonly modeHolder: { mode: PermissionMode }, private readonly approvalHolder: { resolver?: ApprovalResolver }) {
146
+ this.#hub = deps.hub ?? new SignalHub(); this.#matcher = deps.matcher; this.#thinking = options.thinking ?? "off"; this.#gatewayBaseUrls = options.gatewayBaseUrls; this.#workspace = options.workspace ?? process.cwd(); this.#deckTools = options.tools ?? []; this.#diagnostics = deps.diagnostics instanceof DiagnosticsEngine ? deps.diagnostics : undefined;
147
+ this.#boundModel = withGatewayBaseUrl(this.#matcher?.resolveCard(options.modelId)?.model ?? agent.state.model, this.#gatewayBaseUrls);
148
+ this.#state = { phase: "idle", head: store.head, usage: createZeroUsage(), contextTokens: 0, modelId: options.modelId };
149
+ }
150
+ subscribe(handler: any) { return this.#hub.subscribe(handler); }
151
+ snapshot() { return this.#state; }
152
+ messages() { return this.agent.state.messages; }
153
+ model() { return this.#boundModel; }
154
+ isBusy() { return this.#busy; }
155
+ availableModels() { return this.#matcher ? [...this.#matcher.resolveAll("")] : []; }
156
+ pendingCount() { return this.#queue.length; }
157
+ pendingInputs() { return [...this.#queue]; }
158
+ enqueue(text: string, mode: "steer" | "followUp" = "followUp") { this.#queue.push({ mode, text }); this.#hub.emit({ kind: "queue", count: this.#queue.length }); }
159
+ clearQueue() { this.#queue.length = 0; this.#hub.emit({ kind: "queue", count: 0 }); }
160
+ dequeueLast() { const item = this.#queue.pop(); if (item) this.#hub.emit({ kind: "queue", count: this.#queue.length }); return item?.text; }
161
+ steerNow() { const item = this.#queue.pop(); if (!item) return false; this.#queue.unshift({ mode: "steer", text: item.text }); this.agent.abort(); return true; }
162
+ abort() { this.clearQueue(); this.#condenseAbort?.abort(); this.agent.abort(); const fault = conductorFault("aborted", "turn aborted by caller"); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); this.#hub.emit({ kind: "idle" }); }
163
+
164
+ async submit(input: string) {
165
+ if (this.#busy) { this.enqueue(input); return this.#state; }
166
+ this.#busy = true;
167
+ try { await this.#runTurn(input); await this.#drainQueue(); return this.#state; } finally { this.#busy = false; }
168
+ }
169
+ async #drainQueue() { if (this.#draining) return; this.#draining = true; try { while (this.#queue.length) { const index = this.#queue.findIndex((item) => item.mode === "steer"); const item = index >= 0 ? this.#queue.splice(index, 1)[0] : this.#queue.shift()!; this.#hub.emit({ kind: "queue", count: this.#queue.length }); await this.#runTurn(item.text); } } finally { this.#draining = false; } }
170
+ async #runTurn(input: string) {
171
+ this.#dispatch({ type: "phase", phase: "streaming" });
172
+ const baseline = this.agent.state.messages.length;
173
+ const prompt = parseSkillInvocation(input)?.body ?? input;
174
+ let success = false;
175
+ for (let attempt = 0; attempt <= (this.deps.retry ?? DEFAULT_RETRY).maxAttempts; attempt++) {
176
+ const unsubscribe = this.agent.subscribe((event) => this.#onEvent(event));
177
+ try { await this.agent.prompt(prompt); success = true; unsubscribe(); break; } catch (error) {
178
+ unsubscribe();
179
+ if (isQuotaFault(error)) { const fault = conductorFault("model", quotaErrorMessage(error)!, error); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); break; }
180
+ if (attempt < (this.deps.retry ?? DEFAULT_RETRY).maxAttempts && isTransient(error)) { this.#hub.emit({ kind: "fault", fault: conductorFault("model", `transient model fault; retry ${attempt + 1}`, error), transient: true }); await (this.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))((this.deps.retry ?? DEFAULT_RETRY).baseDelayMs * 2 ** attempt); continue; }
181
+ if (isOverloadFault(error) && this.options.fallbackModelId) this.selectModel(this.options.fallbackModelId);
182
+ const fault = conductorFault("model", "model turn failed", error); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); break;
183
+ }
184
+ }
185
+ if (!success) return;
186
+ for (const message of this.agent.state.messages.slice(baseline)) { try { const entry = await this.store.append(message); this.#hub.emit({ kind: "persisted", entryId: entry.id }); } catch (error) { const fault = conductorFault("persistence", "failed to persist transcript node", error); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); } }
187
+ await this.store.persistUsage(this.#state.usage).catch(() => {});
188
+ if ((this.options.autoCompact ?? true) && this.#boundModel && this.store.size > 1 && isOverBudget(this.agent.state.messages, this.#boundModel, AUTO_CONDENSE_POLICY)) await this.#runCondense(false);
189
+ if (this.#state.phase !== "faulted") this.#dispatch({ type: "settled" });
190
+ this.#dispatch({ type: "head", head: this.store.head }); this.#hub.emit({ kind: "idle" });
191
+ }
192
+ #onEvent(event: AgentEvent) {
193
+ if (event.type === "tool_execution_start") { this.#dispatch({ type: "phase", phase: "tooling" }); const args = event.args as any; if ((event.toolName === "edit" || event.toolName === "write") && typeof args?.path === "string" && isTypeScriptOrJs(args.path)) this.#edited.add(path.resolve(this.#workspace, args.path)); }
194
+ if (event.type === "tool_execution_end") this.#dispatch({ type: "phase", phase: "streaming" });
195
+ for (const signal of translateAgentEvent(event)) { if (signal.kind === "turn_end") this.#dispatch({ type: "usage", usage: accumulateUsage(this.#state.usage, signal.usage), contextTokens: contextTokensOf(signal.usage) }); else if (signal.kind === "fault") this.#dispatch({ type: "fault", fault: signal.fault }); this.#hub.emit(signal); }
196
+ }
197
+
198
+ selectModel(id: string) { const card = this.#matcher?.resolveCard(id); if (!card) { const fault = conductorFault("model", `no model matches "${id}"`); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); return; } this.#applyModel(card); }
199
+ cycleModel(id: string) { this.selectModel(id); }
200
+ #applyModel(card: CatalogCard) { const model = withGatewayBaseUrl(card.model, this.#gatewayBaseUrls); this.agent.setModel(model); this.#boundModel = model; this.#dispatch({ type: "model", modelId: card.id }); }
201
+ updateGatewayBaseUrls(map: Record<string, string>) { this.#gatewayBaseUrls = map; this.selectModel(this.#state.modelId); }
202
+ registerTools(tools: AgentTool[]) { this.agent.setTools([...this.#deckTools, ...tools]); }
203
+
204
+ async condense() { if (this.#busy) return "busy" as const; this.#busy = true; this.#condenseAbort = new AbortController(); try { const result = await this.#runCondense(true, this.#condenseAbort.signal); await this.#drainQueue(); return result; } finally { this.#busy = false; this.#condenseAbort = undefined; } }
205
+ cancelCondense() { this.#condenseAbort?.abort(); }
206
+ async #runCondense(force: boolean, signal?: AbortSignal) {
207
+ const before = [...this.agent.state.messages]; this.#dispatch({ type: "phase", phase: "condensing" });
208
+ try { const after = await (this.deps.condense ?? noopCondense)(before, { force, model: this.#boundModel, contextTokens: this.#state.contextTokens, signal }); if (signal?.aborted) return "cancelled" as const; if (after.length >= before.length) { this.#dispatch({ type: "settled" }); return "nothing" as const; } this.agent.replaceMessages(after); await this.store.reset(); for (let i = 0; i < after.length; i++) await this.store.append(after[i], i === 0 ? "condense" : undefined); this.#dispatch({ type: "head", head: this.store.head }); this.#hub.emit({ kind: "compacted", manual: force }); this.#dispatch({ type: "settled" }); return "condensed" as const; } catch (error) { const fault = conductorFault("overflow", "auto-condense failed", error); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); return "failed" as const; }
209
+ }
210
+
211
+ async fork(id: string) { await this.#move(id, `cannot fork from "${id}"`); }
212
+ async navigateTree(id: string) { await this.#move(id, `cannot navigate to "${id}"`); }
213
+ async #move(id: string, message: string) { try { await this.store.branchAt(id); this.agent.replaceMessages(this.store.messages()); this.#dispatch({ type: "head", head: this.store.head }); this.#dispatch({ type: "settled" }); this.#hub.emit({ kind: "idle" }); } catch (error) { const fault = conductorFault("persistence", message, error); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); } }
214
+ async restoreCode(id: string) { return this.options.checkpoint?.restore(id) ?? []; }
215
+ async codeRestoreInfo(id: string) { return { canRestore: this.options.checkpoint?.hasSnapshot(id) ?? false }; }
216
+ async executeBash(command: string, opts: { excludeFromContext?: boolean } = {}) { const { spawn } = await import("node:child_process"); const outcome = await new Promise<{ output: string; exitCode: number }>((resolve) => { const child = spawn(command, { cwd: this.#workspace, shell: true }); let output = ""; child.stdout?.on("data", (chunk) => output += String(chunk)); child.stderr?.on("data", (chunk) => output += String(chunk)); child.on("error", (error) => resolve({ output: output + errorText(error), exitCode: 1 })); child.on("close", (code) => resolve({ output, exitCode: code ?? 1 })); }); if (!opts.excludeFromContext) await this.store.append({ role: "user", content: `$ ${command}\n${outcome.output}`.trimEnd(), timestamp: Date.now() } as AgentMessage, "note"); return outcome; }
217
+
218
+ stats() { const messages = this.agent.state.messages; let userMessages = 0, assistantMessages = 0, toolCalls = 0, toolResults = 0; for (const message of messages) { if (message.role === "user") userMessages++; else if (message.role === "assistant") { assistantMessages++; for (const part of message.content ?? []) if (typeof part === "object" && part && part.type === "toolCall") toolCalls++; } else if (message.role === "toolResult") toolResults++; } const usage = this.#state.usage; return { sessionId: this.#state.head.sessionId, userMessages, assistantMessages, toolCalls, toolResults, totalMessages: messages.length, tokens: { input: usage.input, output: usage.output, cacheRead: usage.cacheRead, cacheWrite: usage.cacheWrite, total: usage.totalTokens }, cost: usage.cost.total }; }
219
+ costReport() { const stats = this.stats(); return [`Total cost: $${stats.cost.toFixed(4)}`, `Total tokens: ${stats.tokens.total}`, ` Input: ${stats.tokens.input}`, ` Output: ${stats.tokens.output}`, ` Cache read: ${stats.tokens.cacheRead}`, ` Cache write: ${stats.tokens.cacheWrite}`].join("\n"); }
220
+ thinkingLevel() { return this.#thinking; }
221
+ setThinkingLevel(level: ThinkingLevel) { this.#thinking = level; this.agent.setThinkingLevel?.(level); }
222
+ cycleThinkingLevel() { const next = THINKING_LADDER[(THINKING_LADDER.indexOf(this.#thinking) + 1) % THINKING_LADDER.length]; this.setThinkingLevel(next); return next; }
223
+ permissionMode() { return this.modeHolder.mode; }
224
+ setPermissionMode(mode: PermissionMode) { this.modeHolder.mode = mode; }
225
+ setApprovalResolver(resolver: ApprovalResolver | undefined) { this.approvalHolder.resolver = resolver; }
226
+ prePlanMode() { return this.#prePlanMode; }
227
+ togglePlanMode(on: boolean) { if (on) { if (this.modeHolder.mode !== "plan") { this.#prePlanMode = this.modeHolder.mode; this.modeHolder.mode = "plan"; } } else { this.modeHolder.mode = this.#prePlanMode ?? "default"; this.#prePlanMode = undefined; } return this.modeHolder.mode; }
228
+ cyclePermissionMode() { const current = this.modeHolder.mode === "bypassPermissions" ? "bypass" : this.modeHolder.mode; const next = PERMISSION_CYCLE[(PERMISSION_CYCLE.indexOf(current) + 1) % PERMISSION_CYCLE.length]; this.setPermissionMode(next); return next; }
229
+ sessionName() { return this.#sessionName; }
230
+ setSessionName(name: string) { this.#sessionName = name; }
231
+ async resume(id: string) { if (!await this.store.load(id)) { const fault = conductorFault("persistence", `no session "${id}" to resume`); this.#dispatch({ type: "fault", fault }); this.#hub.emit({ kind: "fault", fault }); return; } this.agent.replaceMessages(this.store.messages()); this.#state = { ...this.#state, head: this.store.head, usage: this.store.usage ?? createZeroUsage(), phase: "idle" }; this.#hub.emit({ kind: "idle" }); }
232
+ async newSession() { this.agent.abort(); this.agent.replaceMessages([]); this.store.startNewSession(newSessionId()); this.#queue.length = 0; this.#sessionName = undefined; this.#state = { phase: "idle", head: this.store.head, usage: createZeroUsage(), contextTokens: 0, modelId: this.#state.modelId }; this.#hub.emit({ kind: "idle" }); }
233
+ #dispatch(action: StateAction) { this.#state = reduceState(this.#state, action); }
234
+ }