@twinklerg/coden 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/index.js +21269 -0
  4. package/package.json +48 -0
  5. package/src/cli/agent-command.ts +497 -0
  6. package/src/cli/format.ts +42 -0
  7. package/src/cli/index.ts +149 -0
  8. package/src/cli/plugin-command.ts +217 -0
  9. package/src/config/config.ts +96 -0
  10. package/src/config/trust.ts +35 -0
  11. package/src/context/manager.ts +186 -0
  12. package/src/context/truncate.ts +9 -0
  13. package/src/core/events.ts +32 -0
  14. package/src/core/runtime.ts +402 -0
  15. package/src/core/types.ts +97 -0
  16. package/src/index.ts +14 -0
  17. package/src/observability/terminal.ts +201 -0
  18. package/src/observability/trace.ts +30 -0
  19. package/src/permissions/policy.ts +56 -0
  20. package/src/permissions/workspace.ts +139 -0
  21. package/src/plugins/api.ts +68 -0
  22. package/src/plugins/bun-package-manager.ts +35 -0
  23. package/src/plugins/installed-loader.ts +144 -0
  24. package/src/plugins/installer.ts +314 -0
  25. package/src/plugins/manifest.ts +89 -0
  26. package/src/plugins/package-manager.ts +10 -0
  27. package/src/plugins/package-metadata.ts +95 -0
  28. package/src/plugins/paths.ts +43 -0
  29. package/src/plugins/specifier.ts +63 -0
  30. package/src/plugins/transaction.ts +403 -0
  31. package/src/process/runner.ts +134 -0
  32. package/src/providers/anthropic.ts +117 -0
  33. package/src/providers/openai.ts +96 -0
  34. package/src/providers/scripted.ts +28 -0
  35. package/src/sessions/store.ts +278 -0
  36. package/src/tools/builtin/bash.ts +56 -0
  37. package/src/tools/builtin/edit.ts +42 -0
  38. package/src/tools/builtin/index.ts +9 -0
  39. package/src/tools/builtin/read.ts +91 -0
  40. package/src/tools/builtin/write.ts +34 -0
  41. package/src/tools/executor.ts +90 -0
  42. package/src/tools/plugin-loader.ts +122 -0
  43. package/src/tools/registry.ts +97 -0
@@ -0,0 +1,32 @@
1
+ import type { Usage } from "./types.js";
2
+
3
+ export type RuntimeEvent = {
4
+ type: string;
5
+ timestamp: string;
6
+ turnId?: string;
7
+ data?: Record<string, unknown>;
8
+ };
9
+
10
+ export type EventListener = (event: RuntimeEvent) => void | Promise<void>;
11
+
12
+ export class EventBus {
13
+ readonly #listeners = new Set<EventListener>();
14
+ on(listener: EventListener): () => void {
15
+ this.#listeners.add(listener);
16
+ return () => this.#listeners.delete(listener);
17
+ }
18
+ async emit(type: string, data?: Record<string, unknown>, turnId?: string): Promise<RuntimeEvent> {
19
+ const event: RuntimeEvent = { type, timestamp: new Date().toISOString() };
20
+ if (turnId !== undefined) event.turnId = turnId;
21
+ if (data !== undefined) event.data = data;
22
+ await Promise.all([...this.#listeners].map((listener) => listener(event)));
23
+ return event;
24
+ }
25
+ }
26
+
27
+ export interface TurnMetrics {
28
+ tools: number;
29
+ durationMs: number;
30
+ usage: Usage;
31
+ contextTokens: number;
32
+ }
@@ -0,0 +1,402 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ContextManager } from "../context/manager.js";
3
+ import { toModelRequest } from "../context/manager.js";
4
+ import type { SessionStore } from "../sessions/store.js";
5
+ import type { ToolExecutor } from "../tools/executor.js";
6
+ import type { ToolRegistry } from "../tools/registry.js";
7
+ import type { EventBus } from "./events.js";
8
+ import {
9
+ type AgentMessage,
10
+ type AssistantMessage,
11
+ CodeNError,
12
+ type ModelEvent,
13
+ type ModelProvider,
14
+ type ToolCall,
15
+ type Usage,
16
+ } from "./types.js";
17
+
18
+ export interface RuntimeOptions {
19
+ model: string;
20
+ maxSteps?: number;
21
+ retries?: number;
22
+ retryBaseMs?: number;
23
+ systemPrompt?: string;
24
+ }
25
+ export interface TurnResult {
26
+ answer: string;
27
+ messages: AgentMessage[];
28
+ toolsExecuted: number;
29
+ usage: Usage;
30
+ }
31
+
32
+ export class AgentRuntime {
33
+ readonly messages: AgentMessage[];
34
+ private readonly maxSteps: number;
35
+ private readonly retries: number;
36
+ private readonly retryBaseMs: number;
37
+ private systemPersisted: boolean;
38
+ constructor(
39
+ private readonly provider: ModelProvider,
40
+ private readonly registry: ToolRegistry,
41
+ private readonly executor: ToolExecutor,
42
+ private readonly context: ContextManager,
43
+ private readonly sessions: SessionStore,
44
+ private readonly events: EventBus,
45
+ private readonly options: RuntimeOptions,
46
+ initialMessages?: AgentMessage[],
47
+ ) {
48
+ this.maxSteps = options.maxSteps ?? 20;
49
+ this.retries = options.retries ?? 3;
50
+ this.retryBaseMs = options.retryBaseMs ?? 250;
51
+ this.systemPersisted = Boolean(initialMessages?.length);
52
+ this.messages = initialMessages?.length
53
+ ? [...initialMessages]
54
+ : [
55
+ {
56
+ role: "system",
57
+ content:
58
+ options.systemPrompt ??
59
+ "You are CodeN, a concise coding agent. Inspect before editing, use tools carefully, and verify changes.",
60
+ },
61
+ ];
62
+ }
63
+
64
+ async reset(): Promise<void> {
65
+ const system = this.messages.find((message) => message.role === "system") ?? {
66
+ role: "system" as const,
67
+ content: "You are CodeN.",
68
+ };
69
+ await this.sessions.append("session.reset", {});
70
+ this.messages.splice(0, this.messages.length, system);
71
+ this.context.clearSummary();
72
+ this.systemPersisted = false;
73
+ }
74
+
75
+ async compact(): Promise<void> {
76
+ const prepared = this.context.forceCompact(this.messages, this.registry.list());
77
+ const summary = this.context.getSummary();
78
+ if (summary) await this.sessions.appendCompaction(summary, this.context.getCompactionRange());
79
+ await this.events.emit("context.compacted", {
80
+ manual: true,
81
+ estimatedTokens: prepared.estimatedTokens,
82
+ });
83
+ }
84
+
85
+ async run(userText: string, signal = new AbortController().signal): Promise<TurnResult> {
86
+ const turnId = randomUUID();
87
+ const start = Date.now();
88
+ let toolsExecuted = 0;
89
+ const usage: Usage = { inputTokens: 0, outputTokens: 0 };
90
+ await this.events.emit("turn.started", { input: userText }, turnId);
91
+ try {
92
+ if (!this.systemPersisted) {
93
+ const system = this.messages[0];
94
+ if (system?.role === "system") await this.sessions.appendMessage(system);
95
+ this.systemPersisted = true;
96
+ }
97
+ const hasPriorUser = this.messages.some((message) => message.role === "user");
98
+ const user: AgentMessage = { role: "user", content: userText };
99
+ this.messages.push(user);
100
+ await this.sessions.appendMessage(user);
101
+ if (!hasPriorUser) await this.sessions.setTitle(userText);
102
+ for (let step = 0; step < this.maxSteps; step++) {
103
+ let prepared = this.context.prepare(this.messages, this.registry.list());
104
+ if (prepared.compacted) {
105
+ let summary = this.context.getSummary();
106
+ if (summary) {
107
+ const refined = await this.refineSummary(summary, signal, turnId);
108
+ if (refined) {
109
+ const previous = summary;
110
+ summary = refined;
111
+ this.context.setSummary(refined, this.context.getCompactionRange()?.end ?? 0);
112
+ prepared.messages = prepared.messages.map((message) =>
113
+ message.role === "system" && message.content === previous
114
+ ? { role: "system", content: refined }
115
+ : message,
116
+ );
117
+ }
118
+ await this.sessions.appendCompaction(summary, this.context.getCompactionRange());
119
+ }
120
+ await this.events.emit(
121
+ "context.compacted",
122
+ { estimatedTokens: prepared.estimatedTokens },
123
+ turnId,
124
+ );
125
+ }
126
+ await this.events.emit(
127
+ "context.prepared",
128
+ { estimatedTokens: prepared.estimatedTokens, budget: this.context.inputBudget() },
129
+ turnId,
130
+ );
131
+ const accumulated = await this.requestWithRetry(
132
+ toModelRequest(
133
+ this.options.model,
134
+ prepared,
135
+ this.registry.list(),
136
+ this.context.budget,
137
+ signal,
138
+ ),
139
+ turnId,
140
+ async () => {
141
+ prepared = this.context.forceCompact(this.messages, this.registry.list());
142
+ const summary = this.context.getSummary();
143
+ if (summary)
144
+ await this.sessions.appendCompaction(summary, this.context.getCompactionRange());
145
+ return toModelRequest(
146
+ this.options.model,
147
+ prepared,
148
+ this.registry.list(),
149
+ this.context.budget,
150
+ signal,
151
+ );
152
+ },
153
+ );
154
+ usage.inputTokens += accumulated.usage.inputTokens;
155
+ usage.outputTokens += accumulated.usage.outputTokens;
156
+ const assistant: AssistantMessage = {
157
+ role: "assistant",
158
+ content: accumulated.text,
159
+ toolCalls: accumulated.toolCalls,
160
+ model: this.options.model,
161
+ usage: accumulated.usage,
162
+ };
163
+ this.messages.push(assistant);
164
+ await this.sessions.appendMessage(assistant);
165
+ if (assistant.toolCalls.length === 0) {
166
+ await this.events.emit(
167
+ "turn.completed",
168
+ {
169
+ tools: toolsExecuted,
170
+ durationMs: Date.now() - start,
171
+ inputTokens: usage.inputTokens,
172
+ outputTokens: usage.outputTokens,
173
+ contextTokens: prepared.estimatedTokens,
174
+ },
175
+ turnId,
176
+ );
177
+ return { answer: assistant.content, messages: this.messages, toolsExecuted, usage };
178
+ }
179
+ for (const call of assistant.toolCalls) {
180
+ const result = await this.executor.execute(call, signal, turnId);
181
+ toolsExecuted++;
182
+ const message: AgentMessage = {
183
+ role: "tool",
184
+ callId: call.callId,
185
+ name: call.name,
186
+ content: result.content,
187
+ isError: result.isError ?? false,
188
+ };
189
+ this.messages.push(message);
190
+ await this.sessions.appendMessage(message);
191
+ }
192
+ }
193
+ throw new CodeNError(
194
+ "runtime",
195
+ "runtime.step_limit",
196
+ `Maximum model steps (${this.maxSteps}) reached`,
197
+ );
198
+ } catch (error) {
199
+ await this.events.emit(
200
+ "turn.failed",
201
+ {
202
+ code: error instanceof CodeNError ? error.code : "runtime.unknown",
203
+ message: error instanceof Error ? error.message : String(error),
204
+ },
205
+ turnId,
206
+ );
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ private async refineSummary(
212
+ deterministicSummary: string,
213
+ signal: AbortSignal,
214
+ turnId: string,
215
+ ): Promise<string | undefined> {
216
+ try {
217
+ await this.events.emit("context.compaction_started", { model: this.options.model }, turnId);
218
+ const result = await accumulateStream(
219
+ this.provider.stream({
220
+ model: this.options.model,
221
+ messages: [
222
+ {
223
+ role: "system",
224
+ content:
225
+ "Rewrite the supplied coding-session summary concisely. Preserve goals, constraints, decisions, changed files, tool/test results, unresolved errors, and next steps. Return only the summary.",
226
+ },
227
+ { role: "user", content: deterministicSummary },
228
+ ],
229
+ tools: [],
230
+ maxOutputTokens: Math.min(2048, this.context.budget.reservedOutputTokens),
231
+ signal,
232
+ }),
233
+ );
234
+ if (result.toolCalls.length > 0 || !result.text.trim()) return undefined;
235
+ return `Compacted conversation summary:\n${result.text.trim()}`;
236
+ } catch (error) {
237
+ await this.events.emit(
238
+ "context.compaction_failed",
239
+ {
240
+ message: error instanceof Error ? error.message : String(error),
241
+ fallback: "deterministic",
242
+ },
243
+ turnId,
244
+ );
245
+ return undefined;
246
+ }
247
+ }
248
+
249
+ private async requestWithRetry(
250
+ initialRequest: ReturnType<typeof toModelRequest>,
251
+ turnId: string,
252
+ emergency: () => Promise<ReturnType<typeof toModelRequest>>,
253
+ ): Promise<{ text: string; toolCalls: ToolCall[]; usage: Usage }> {
254
+ let request = initialRequest;
255
+ let emergencyUsed = false;
256
+ for (let attempt = 0; ; attempt++) {
257
+ try {
258
+ await this.events.emit("provider.started", { attempt }, turnId);
259
+ const result = await accumulateStream(
260
+ this.provider.stream(request),
261
+ async (text) => {
262
+ await this.events.emit("provider.delta", { text }, turnId);
263
+ },
264
+ async (text) => {
265
+ await this.events.emit("provider.reasoning_delta", { text }, turnId);
266
+ },
267
+ );
268
+ await this.events.emit("provider.completed", { usage: result.usage }, turnId);
269
+ return result;
270
+ } catch (error) {
271
+ if (request.signal?.aborted) throw error;
272
+ if (isContextError(error)) {
273
+ if (!emergencyUsed) {
274
+ emergencyUsed = true;
275
+ request = await emergency();
276
+ await this.events.emit("context.compacted", { emergency: true }, turnId);
277
+ continue;
278
+ }
279
+ throw new CodeNError(
280
+ "context",
281
+ "context.exhausted",
282
+ "Context is still over the provider limit after emergency compaction",
283
+ false,
284
+ undefined,
285
+ { cause: error },
286
+ );
287
+ }
288
+ if (attempt >= this.retries || !isRetryable(error)) throw error;
289
+ await this.events.emit(
290
+ "provider.retry",
291
+ { attempt: attempt + 1, message: error instanceof Error ? error.message : String(error) },
292
+ turnId,
293
+ );
294
+ const retryAfter = retryAfterMs(error);
295
+ const backoff = this.retryBaseMs * 2 ** attempt * (0.8 + Math.random() * 0.4);
296
+ await delay(retryAfter ?? Math.min(30_000, backoff), request.signal);
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ export async function accumulateStream(
303
+ stream: AsyncIterable<ModelEvent>,
304
+ onText?: (text: string) => void | Promise<void>,
305
+ onReasoning?: (text: string) => void | Promise<void>,
306
+ ): Promise<{ text: string; toolCalls: ToolCall[]; usage: Usage }> {
307
+ let text = "";
308
+ let usage: Usage = { inputTokens: 0, outputTokens: 0 };
309
+ const builders = new Map<
310
+ number,
311
+ { callId: string; name: string; json: string; ended: boolean }
312
+ >();
313
+ for await (const event of stream) {
314
+ if (event.type === "reasoning_delta") {
315
+ await onReasoning?.(event.text);
316
+ } else if (event.type === "text_delta") {
317
+ text += event.text;
318
+ await onText?.(event.text);
319
+ } else if (event.type === "tool_call_start")
320
+ builders.set(event.index, { callId: event.callId, name: event.name, json: "", ended: false });
321
+ else if (event.type === "tool_call_delta") {
322
+ const builder = builders.get(event.index);
323
+ if (!builder)
324
+ throw new CodeNError(
325
+ "provider",
326
+ "provider.invalid_stream",
327
+ "Tool arguments arrived before tool start",
328
+ );
329
+ builder.json += event.argumentsDelta;
330
+ } else if (event.type === "tool_call_end") {
331
+ const builder = builders.get(event.index);
332
+ if (builder) builder.ended = true;
333
+ } else if (event.type === "usage")
334
+ usage = {
335
+ inputTokens: Math.max(usage.inputTokens, event.usage.inputTokens),
336
+ outputTokens: Math.max(usage.outputTokens, event.usage.outputTokens),
337
+ };
338
+ }
339
+ const toolCalls = [...builders.entries()]
340
+ .sort(([a], [b]) => a - b)
341
+ .map(([, builder]) => {
342
+ if (!builder.ended)
343
+ throw new CodeNError(
344
+ "provider",
345
+ "provider.incomplete_tool_call",
346
+ `Incomplete tool call: ${builder.name}`,
347
+ );
348
+ try {
349
+ return {
350
+ callId: builder.callId,
351
+ name: builder.name,
352
+ input: JSON.parse(builder.json || "{}"),
353
+ };
354
+ } catch (cause) {
355
+ throw new CodeNError(
356
+ "provider",
357
+ "provider.invalid_tool_json",
358
+ `Invalid JSON for tool ${builder.name}`,
359
+ false,
360
+ undefined,
361
+ { cause },
362
+ );
363
+ }
364
+ });
365
+ return { text, toolCalls, usage };
366
+ }
367
+ function isRetryable(error: unknown): boolean {
368
+ if (error instanceof CodeNError) return error.retryable;
369
+ const status = (error as { status?: unknown })?.status;
370
+ return typeof status !== "number" || status === 429 || status >= 500;
371
+ }
372
+ function retryAfterMs(error: unknown): number | undefined {
373
+ const headers = (error as { headers?: { get?: (name: string) => string | null } })?.headers;
374
+ const raw = headers?.get?.("retry-after");
375
+ if (!raw) return undefined;
376
+ const seconds = Number(raw);
377
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
378
+ const date = Date.parse(raw);
379
+ return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
380
+ }
381
+ function isContextError(error: unknown): boolean {
382
+ const status = (error as { status?: unknown })?.status;
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ return status === 413 || /context.{0,20}(?:length|window|limit|too long)/i.test(message);
385
+ }
386
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
387
+ return new Promise((resolve, reject) => {
388
+ if (signal?.aborted) {
389
+ reject(signal.reason);
390
+ return;
391
+ }
392
+ const timer = setTimeout(resolve, ms);
393
+ signal?.addEventListener(
394
+ "abort",
395
+ () => {
396
+ clearTimeout(timer);
397
+ reject(signal.reason);
398
+ },
399
+ { once: true },
400
+ );
401
+ });
402
+ }
@@ -0,0 +1,97 @@
1
+ export type JsonSchema = Record<string, unknown>;
2
+ export type ToolRisk = "read" | "modify" | "dangerous";
3
+
4
+ export interface Usage {
5
+ inputTokens: number;
6
+ outputTokens: number;
7
+ }
8
+
9
+ export interface ToolCall {
10
+ callId: string;
11
+ name: string;
12
+ input: unknown;
13
+ }
14
+
15
+ export interface SystemMessage {
16
+ role: "system";
17
+ content: string;
18
+ }
19
+ export interface UserMessage {
20
+ role: "user";
21
+ content: string;
22
+ }
23
+ export interface AssistantMessage {
24
+ role: "assistant";
25
+ content: string;
26
+ toolCalls: ToolCall[];
27
+ model?: string;
28
+ usage?: Usage;
29
+ }
30
+ export interface ToolResultMessage {
31
+ role: "tool";
32
+ callId: string;
33
+ name: string;
34
+ content: string;
35
+ isError: boolean;
36
+ }
37
+ export type AgentMessage = SystemMessage | UserMessage | AssistantMessage | ToolResultMessage;
38
+
39
+ export interface ToolResult {
40
+ content: string;
41
+ isError?: boolean;
42
+ metadata?: Record<string, unknown>;
43
+ }
44
+ export interface ToolContext {
45
+ workspace: string;
46
+ signal: AbortSignal;
47
+ }
48
+ export interface ToolDefinition {
49
+ name: string;
50
+ description: string;
51
+ inputSchema: JsonSchema;
52
+ risk: ToolRisk;
53
+ execute(input: unknown, context: ToolContext): Promise<ToolResult>;
54
+ }
55
+
56
+ export interface ModelRequest {
57
+ model: string;
58
+ messages: AgentMessage[];
59
+ tools: ToolDefinition[];
60
+ maxOutputTokens: number;
61
+ signal?: AbortSignal;
62
+ }
63
+
64
+ export type ModelEvent =
65
+ | { type: "reasoning_delta"; text: string }
66
+ | { type: "text_delta"; text: string }
67
+ | { type: "tool_call_start"; index: number; callId: string; name: string }
68
+ | { type: "tool_call_delta"; index: number; argumentsDelta: string }
69
+ | { type: "tool_call_end"; index: number }
70
+ | { type: "usage"; usage: Usage }
71
+ | { type: "done" };
72
+
73
+ export interface ModelProvider {
74
+ stream(request: ModelRequest): AsyncIterable<ModelEvent>;
75
+ }
76
+
77
+ export type ErrorCategory =
78
+ | "provider"
79
+ | "context"
80
+ | "tool"
81
+ | "permission"
82
+ | "plugin"
83
+ | "session"
84
+ | "runtime";
85
+ export class CodeNError extends Error {
86
+ constructor(
87
+ readonly category: ErrorCategory,
88
+ readonly code: string,
89
+ message: string,
90
+ readonly retryable = false,
91
+ readonly details?: Record<string, unknown>,
92
+ options?: ErrorOptions,
93
+ ) {
94
+ super(message, options);
95
+ this.name = "CodeNError";
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./context/manager.js";
2
+ export * from "./core/events.js";
3
+ export * from "./core/runtime.js";
4
+ export * from "./core/types.js";
5
+ export * from "./plugins/api.js";
6
+ export * from "./plugins/installed-loader.js";
7
+ export * from "./plugins/installer.js";
8
+ export * from "./plugins/manifest.js";
9
+ export * from "./plugins/paths.js";
10
+ export * from "./plugins/specifier.js";
11
+ export * from "./providers/scripted.js";
12
+ export * from "./tools/builtin/index.js";
13
+ export * from "./tools/plugin-loader.js";
14
+ export * from "./tools/registry.js";