@thispointon/kondi-chat 0.1.2

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. package/src/web/manager.ts +311 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Session persistence — save/load sessions under `.kondi-chat/sessions/`.
3
+ *
4
+ * Single SessionStore. Two constants (AUTO_SAVE_MS, MAX_AGE_DAYS). Atomic
5
+ * writes via temp+rename. The ledger files stay flat in storageDir as they
6
+ * are today — see specs/06-session-resume/SPEC.md for the rationale.
7
+ */
8
+
9
+ import {
10
+ existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync,
11
+ renameSync, rmSync, statSync, unlinkSync,
12
+ } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+ import type { Session } from '../types.ts';
15
+
16
+ export const AUTO_SAVE_MS = 30_000;
17
+ export const MAX_AGE_DAYS = 30;
18
+ const MAX_AGE_MS = MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
19
+
20
+ export interface PersistedSession {
21
+ version: 1;
22
+ session: Session;
23
+ activeProfile: string;
24
+ overrideModel?: string;
25
+ lastSavedAt: string;
26
+ workingDirectory: string;
27
+ }
28
+
29
+ export interface SessionIndexEntry {
30
+ id: string;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ workingDirectory: string;
34
+ messageCount: number;
35
+ totalCostUsd: number;
36
+ activeModel?: string;
37
+ profile: string;
38
+ summary: string;
39
+ }
40
+
41
+ function atomicWrite(path: string, data: string): void {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ const tmp = path + '.tmp';
44
+ writeFileSync(tmp, data);
45
+ try { renameSync(tmp, path); } catch { writeFileSync(path, data); }
46
+ }
47
+
48
+ export class SessionStore {
49
+ private sessionsDir: string;
50
+ private indexPath: string;
51
+ private activePath: string;
52
+ private index: SessionIndexEntry[];
53
+
54
+ constructor(storageDir: string) {
55
+ this.sessionsDir = join(storageDir, 'sessions');
56
+ this.indexPath = join(this.sessionsDir, 'index.json');
57
+ this.activePath = join(this.sessionsDir, 'active.json');
58
+ mkdirSync(this.sessionsDir, { recursive: true });
59
+ this.index = this.loadIndex();
60
+ }
61
+
62
+ save(session: Session, profile: string, overrideModel?: string): void {
63
+ const persisted: PersistedSession = {
64
+ version: 1,
65
+ session,
66
+ activeProfile: profile,
67
+ overrideModel,
68
+ lastSavedAt: new Date().toISOString(),
69
+ workingDirectory: session.workingDirectory || '',
70
+ };
71
+ atomicWrite(
72
+ join(this.sessionsDir, `${session.id}.json`),
73
+ JSON.stringify(persisted, null, 2),
74
+ );
75
+ this.updateIndex({
76
+ id: session.id,
77
+ createdAt: session.createdAt,
78
+ updatedAt: new Date().toISOString(),
79
+ workingDirectory: session.workingDirectory || '',
80
+ messageCount: session.messages.length,
81
+ totalCostUsd: session.totalCostUsd,
82
+ activeModel: session.activeModel,
83
+ profile,
84
+ summary: this.computeSummary(session),
85
+ });
86
+ }
87
+
88
+ load(idOrPrefix: string): PersistedSession | null {
89
+ const id = this.resolveId(idOrPrefix);
90
+ if (!id) return null;
91
+ const file = join(this.sessionsDir, `${id}.json`);
92
+ if (!existsSync(file)) return null;
93
+ try {
94
+ return JSON.parse(readFileSync(file, 'utf-8')) as PersistedSession;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ loadLatest(workingDirectory?: string): PersistedSession | null {
101
+ const entries = this.list(workingDirectory);
102
+ if (entries.length === 0) return null;
103
+ return this.load(entries[0].id);
104
+ }
105
+
106
+ list(workingDirectory?: string): SessionIndexEntry[] {
107
+ const entries = workingDirectory
108
+ ? this.index.filter(e => e.workingDirectory === workingDirectory)
109
+ : [...this.index];
110
+ return entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
111
+ }
112
+
113
+ delete(id: string): void {
114
+ const resolved = this.resolveId(id) || id;
115
+ const file = join(this.sessionsDir, `${resolved}.json`);
116
+ if (existsSync(file)) { try { unlinkSync(file); } catch { /* ignore */ } }
117
+ this.index = this.index.filter(e => e.id !== resolved);
118
+ this.saveIndex();
119
+ }
120
+
121
+ /** Delete sessions older than MAX_AGE_DAYS. Returns count removed. */
122
+ cleanup(): { deleted: number } {
123
+ const cutoff = Date.now() - MAX_AGE_MS;
124
+ let deleted = 0;
125
+ for (const entry of [...this.index]) {
126
+ const updated = new Date(entry.updatedAt).getTime();
127
+ if (!isFinite(updated) || updated < cutoff) {
128
+ this.delete(entry.id);
129
+ deleted++;
130
+ }
131
+ }
132
+ return { deleted };
133
+ }
134
+
135
+ setActive(id: string): void {
136
+ atomicWrite(this.activePath, JSON.stringify({ id }));
137
+ }
138
+
139
+ getActive(): string | null {
140
+ if (!existsSync(this.activePath)) return null;
141
+ try { return JSON.parse(readFileSync(this.activePath, 'utf-8')).id || null; }
142
+ catch { return null; }
143
+ }
144
+
145
+ format(workingDirectory?: string): string {
146
+ const entries = this.list(workingDirectory);
147
+ if (entries.length === 0) return 'No sessions yet.';
148
+ const lines = ['Sessions (newest first):'];
149
+ for (const e of entries.slice(0, 20)) {
150
+ lines.push(
151
+ ` ${e.id.slice(0, 8)} ${e.updatedAt.slice(0, 19)} ${e.messageCount}msg $${e.totalCostUsd.toFixed(4)}`
152
+ + (e.summary ? `\n ${e.summary}` : ''),
153
+ );
154
+ }
155
+ if (entries.length > 20) lines.push(` ... and ${entries.length - 20} more`);
156
+ return lines.join('\n');
157
+ }
158
+
159
+ /** Match by exact id, or ≥8-char prefix when unambiguous. */
160
+ private resolveId(idOrPrefix: string): string | null {
161
+ if (this.index.some(e => e.id === idOrPrefix)) return idOrPrefix;
162
+ if (idOrPrefix.length < 8) return null;
163
+ const matches = this.index.filter(e => e.id.startsWith(idOrPrefix));
164
+ return matches.length === 1 ? matches[0].id : null;
165
+ }
166
+
167
+ private updateIndex(entry: SessionIndexEntry): void {
168
+ const existing = this.index.findIndex(e => e.id === entry.id);
169
+ if (existing >= 0) this.index[existing] = entry;
170
+ else this.index.push(entry);
171
+ this.saveIndex();
172
+ }
173
+
174
+ private loadIndex(): SessionIndexEntry[] {
175
+ if (!existsSync(this.indexPath)) return [];
176
+ try {
177
+ const raw = JSON.parse(readFileSync(this.indexPath, 'utf-8'));
178
+ return Array.isArray(raw?.sessions) ? raw.sessions : [];
179
+ } catch { return []; }
180
+ }
181
+
182
+ private saveIndex(): void {
183
+ atomicWrite(this.indexPath, JSON.stringify({ sessions: this.index }, null, 2));
184
+ }
185
+
186
+ /** Spec 13 — save in-progress assistant content so a crash leaves a recoverable trail. */
187
+ savePartialMessage(sessionId: string, content: string): void {
188
+ const dir = join(this.sessionsDir, '..', 'recovery');
189
+ const path = join(dir, `${sessionId}-partial.json`);
190
+ atomicWrite(path, JSON.stringify({ sessionId, content, savedAt: new Date().toISOString() }));
191
+ }
192
+
193
+ /** Spec 13 — read any partial from a prior run; null if none. */
194
+ checkForRecovery(sessionId: string): { content: string; savedAt: string } | null {
195
+ const path = join(this.sessionsDir, '..', 'recovery', `${sessionId}-partial.json`);
196
+ if (!existsSync(path)) return null;
197
+ try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
198
+ }
199
+
200
+ /** Delete the partial file once the session has integrated it. */
201
+ clearRecovery(sessionId: string): void {
202
+ const path = join(this.sessionsDir, '..', 'recovery', `${sessionId}-partial.json`);
203
+ if (existsSync(path)) { try { unlinkSync(path); } catch { /* ignore */ } }
204
+ }
205
+
206
+ private computeSummary(session: Session): string {
207
+ if (session.state.goal) return session.state.goal.slice(0, 120);
208
+ const firstUser = session.messages.find(m => m.role === 'user');
209
+ return firstUser ? firstUser.content.slice(0, 120) : '';
210
+ }
211
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Minimal Mock LLM for tests.
3
+ *
4
+ * Pass a pre-canned queue of LLMResponses; each call dequeues the next.
5
+ * Tests that exercise multi-turn tool loops can enqueue the full sequence.
6
+ */
7
+
8
+ import type { LLMRequest, LLMResponse } from '../types.ts';
9
+
10
+ export interface MockLLMOptions {
11
+ responses: Array<Partial<LLMResponse>>;
12
+ }
13
+
14
+ export interface MockLLM {
15
+ call: (req: LLMRequest) => Promise<LLMResponse>;
16
+ calls: LLMRequest[];
17
+ }
18
+
19
+ export function createMockLLM(opts: MockLLMOptions): MockLLM {
20
+ const queue = [...opts.responses];
21
+ const calls: LLMRequest[] = [];
22
+ return {
23
+ calls,
24
+ async call(req: LLMRequest): Promise<LLMResponse> {
25
+ calls.push(req);
26
+ const next = queue.shift();
27
+ if (!next) throw new Error('Mock LLM: no more responses queued');
28
+ return {
29
+ content: next.content ?? '',
30
+ model: next.model ?? req.model ?? 'mock',
31
+ provider: next.provider ?? req.provider ?? 'anthropic',
32
+ inputTokens: next.inputTokens ?? 10,
33
+ outputTokens: next.outputTokens ?? 20,
34
+ latencyMs: next.latencyMs ?? 1,
35
+ toolCalls: next.toolCalls,
36
+ };
37
+ },
38
+ };
39
+ }
package/src/types.ts ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Core types for kondi-chat
3
+ */
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Provider & Model
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export type ProviderId =
10
+ | 'anthropic'
11
+ | 'openai'
12
+ | 'deepseek'
13
+ | 'google'
14
+ | 'xai'
15
+ | 'zai'
16
+ | 'ollama'
17
+ | 'nvidia-router';
18
+
19
+ export interface ProviderConfig {
20
+ id: ProviderId;
21
+ apiKey?: string;
22
+ baseUrl?: string;
23
+ defaultModel?: string;
24
+ }
25
+
26
+ export interface ModelInfo {
27
+ id: string;
28
+ provider: ProviderId;
29
+ contextWindow: number;
30
+ maxOutputTokens: number;
31
+ inputCostPer1M: number;
32
+ outputCostPer1M: number;
33
+ supportsCaching?: boolean;
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Messages
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export interface Message {
41
+ role: 'user' | 'assistant' | 'system';
42
+ content: string;
43
+ timestamp: string;
44
+ model?: string;
45
+ provider?: ProviderId;
46
+ tokenCount?: number;
47
+ inputTokens?: number;
48
+ outputTokens?: number;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Repo Map — structured codebase summary, created once per repo
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface RepoMap {
56
+ stack: string[];
57
+ entrypoints: string[];
58
+ subsystems: Array<{ name: string; paths: string[]; purpose: string }>;
59
+ commands: { build?: string; test?: string; lint?: string; typecheck?: string };
60
+ conventions: string[];
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Session State — durable conversation memory
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export interface SessionState {
68
+ goal: string;
69
+ decisions: string[];
70
+ constraints: string[];
71
+ currentPlan: string[];
72
+ activeTaskId?: string;
73
+ recentFailures: string[];
74
+ lastUpdatedAtTurn: number;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Task Card — bounded work packet for execution
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Task kinds are open-ended. Defaults: implementation, fix, refactor, test, analysis.
83
+ * Users/plugins can add domain-specific kinds: robot-control, image-generation, etc.
84
+ * The router learns to route new kinds through training data.
85
+ */
86
+ export type TaskKind = string;
87
+
88
+ export interface TaskCard {
89
+ id: string;
90
+ kind: TaskKind;
91
+ goal: string;
92
+ relevantFiles: string[];
93
+ constraints: string[];
94
+ acceptanceCriteria: string[];
95
+ outputMode: 'diff' | 'file_replacements' | 'text';
96
+ failures: number;
97
+ createdAt: string;
98
+ completedAt?: string;
99
+ status: 'pending' | 'executing' | 'verifying' | 'passed' | 'failed' | 'promoted';
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Creative Generation
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export interface CreativeGenerationRequest {
107
+ description: string;
108
+ images?: string[]; // Base64 encoded images
109
+ style?: string; // e.g., "technical", "narrative", "visual", etc.
110
+ constraints?: string[]; // Any specific requirements
111
+ }
112
+
113
+ export interface CreativeGenerationResponse {
114
+ content: string; // The generated creative content
115
+ type: 'text' | 'code' | 'structured' | 'mixed';
116
+ metadata?: {
117
+ model: string;
118
+ tokens?: number;
119
+ confidence?: number;
120
+ suggestions?: string[]; // Additional ideas or variations
121
+ };
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Verification
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export interface VerificationResult {
129
+ passed: boolean;
130
+ testOutput?: string;
131
+ lintOutput?: string;
132
+ typecheckOutput?: string;
133
+ error?: string;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Audit Ledger — every LLM call recorded
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export type LedgerPhase =
141
+ | 'consult' // consultant expert — domain-specialized persona
142
+ | 'discuss' // frontier: user conversation
143
+ | 'commit' // system: state update
144
+ | 'dispatch' // frontier: task card creation
145
+ | 'execute' // worker: code generation
146
+ | 'verify' // local: tests/lint/typecheck
147
+ | 'reflect' // frontier: summarize results
148
+ | 'compress' // cheap: context compression
149
+ | 'state_update'; // cheap: working state update
150
+
151
+ export interface LedgerEntry {
152
+ id: string;
153
+ timestamp: string;
154
+ phase: LedgerPhase;
155
+ provider: ProviderId;
156
+ model: string;
157
+ inputTokens: number;
158
+ outputTokens: number;
159
+ latencyMs: number;
160
+ costUsd: number;
161
+ cached: boolean;
162
+ /** Input tokens served from the provider's prompt cache (included in inputTokens). */
163
+ cachedInputTokens?: number;
164
+
165
+ /** What was sent (system + user prompt) */
166
+ promptSummary: string;
167
+ /** What came back */
168
+ responseSummary: string;
169
+
170
+ /** Associated task card ID, if any */
171
+ taskId?: string;
172
+ /** Was this a promotion (retry with better model)? */
173
+ promoted?: boolean;
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Session
178
+ // ---------------------------------------------------------------------------
179
+
180
+ export interface Session {
181
+ id: string;
182
+ createdAt: string;
183
+ workingDirectory?: string;
184
+
185
+ /** Full message history — never truncated, used for export/replay */
186
+ messages: Message[];
187
+
188
+ /** Durable session state */
189
+ state: SessionState;
190
+
191
+ /** Repo map — structured codebase summary */
192
+ repoMap?: RepoMap;
193
+
194
+ /** Raw grounding context for prompt assembly */
195
+ groundingContext?: string;
196
+
197
+ /** All task cards created during this session */
198
+ tasks: TaskCard[];
199
+
200
+ /** Active provider/model (can change mid-session) */
201
+ activeProvider: ProviderId;
202
+ activeModel?: string;
203
+
204
+ /** Cumulative cost tracking */
205
+ totalInputTokens: number;
206
+ totalOutputTokens: number;
207
+ totalCostUsd: number;
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Context Budget
212
+ // ---------------------------------------------------------------------------
213
+
214
+ export interface ContextSection {
215
+ key: string;
216
+ content: string;
217
+ priority: number;
218
+ compressible: boolean;
219
+ tokenEstimate: number;
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Tool Use
224
+ // ---------------------------------------------------------------------------
225
+
226
+ export interface ToolDefinition {
227
+ name: string;
228
+ description: string;
229
+ parameters: Record<string, unknown>; // JSON Schema
230
+ }
231
+
232
+ export interface ToolCall {
233
+ id: string;
234
+ name: string;
235
+ arguments: Record<string, unknown>;
236
+ }
237
+
238
+ export interface ToolResult {
239
+ toolCallId: string;
240
+ content: string;
241
+ isError?: boolean;
242
+ /** Spec 03 — unified diff, when the tool mutated a file. */
243
+ diff?: string;
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // LLM Call — multi-turn message for agent loops
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /** Spec 09 — multimodal content part. Text remains the default. */
251
+ export type ContentPart =
252
+ | { type: 'text'; text: string }
253
+ | { type: 'image'; mimeType: string; base64: string };
254
+
255
+ export interface LLMMessage {
256
+ role: 'user' | 'assistant' | 'tool';
257
+ content?: string;
258
+ /** Hidden chain-of-thought from reasoning models. Must be passed back
259
+ * to some providers (DeepSeek) in multi-turn conversations. */
260
+ reasoningContent?: string;
261
+ /** Spec 09 — interleaved text/image parts; providers that support it use this. */
262
+ parts?: ContentPart[];
263
+ toolCalls?: ToolCall[]; // assistant messages with tool use
264
+ toolResults?: ToolResult[]; // tool-result messages
265
+ }
266
+
267
+ /** Spec 09 — image attachment descriptor (used by submit command + pipeline). */
268
+ export interface ImageAttachment {
269
+ mimeType: string;
270
+ base64: string;
271
+ originalPath?: string;
272
+ sizeBytes: number;
273
+ }
274
+
275
+ export interface LLMRequest {
276
+ provider: ProviderId;
277
+ model?: string;
278
+ systemPrompt: string;
279
+ /** Simple single-turn: set userMessage */
280
+ userMessage?: string;
281
+ /** Multi-turn agent loop: set messages instead */
282
+ messages?: LLMMessage[];
283
+ tools?: ToolDefinition[];
284
+ maxOutputTokens?: number;
285
+ temperature?: number;
286
+ cacheablePrefix?: string;
287
+ /** Stream the response token by token */
288
+ stream?: boolean;
289
+ /** Callback for each streamed token chunk */
290
+ onToken?: (token: string) => void;
291
+ }
292
+
293
+ export interface LLMResponse {
294
+ content: string;
295
+ model: string;
296
+ provider: ProviderId;
297
+ inputTokens: number;
298
+ outputTokens: number;
299
+ latencyMs: number;
300
+ cached?: boolean;
301
+ toolCalls?: ToolCall[];
302
+ /** True if this response came from a fallback model, not the originally requested one */
303
+ wasFallback?: boolean;
304
+ /** The originally requested model (if different from the responding model) */
305
+ requestedModel?: string;
306
+ /** Spec 14 — raw response headers for rate-limit parsing. */
307
+ responseHeaders?: Record<string, string>;
308
+ /**
309
+ * Hidden chain-of-thought emitted by reasoning models (GLM-5.x, OpenAI o-series,
310
+ * DeepSeek-R1, Anthropic extended thinking). Billed as output tokens but not
311
+ * shown inline; the TUI exposes it via Ctrl+R.
312
+ */
313
+ reasoningContent?: string;
314
+ /**
315
+ * Portion of inputTokens that the provider served from its prompt cache
316
+ * (OpenAI `prompt_tokens_details.cached_tokens`, Anthropic
317
+ * `cache_read_input_tokens`). Billed at reduced rate — our cost estimate
318
+ * subtracts 50% of the standard input price on the cached portion, which
319
+ * matches the published discount on both OpenAI and Z.AI endpoints.
320
+ */
321
+ cachedInputTokens?: number;
322
+ }