mini-coder 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/bun.lock +13 -13
- package/package.json +3 -3
- package/src/agent.ts +299 -41
- package/src/cli.ts +11 -2
- package/src/headless.ts +185 -47
- package/src/index.ts +105 -12
- package/src/input.ts +1 -0
- package/src/prompt.ts +86 -78
- package/src/session.ts +56 -8
- package/src/submit.ts +53 -11
- package/src/tools.ts +401 -44
- package/src/ui/agent.ts +16 -2
- package/src/ui/commands.test.ts +72 -0
- package/src/ui/commands.ts +13 -0
- package/src/ui/conversation.test.ts +91 -0
- package/src/ui/conversation.ts +122 -3
- package/src/ui/help.test.ts +1 -0
- package/src/ui/help.ts +1 -0
- package/src/ui/input.test.ts +29 -1
- package/src/ui/input.ts +45 -51
- package/src/ui/render-performance.test.ts +1 -0
- package/src/ui/status.test.ts +1 -0
- package/src/ui.ts +85 -18
package/README.md
CHANGED
|
@@ -39,10 +39,12 @@ $ mc
|
|
|
39
39
|
|
|
40
40
|
## Tools
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Four built-in tools, plus a read-only image tool:
|
|
43
43
|
|
|
44
44
|
- **`shell`** — runs commands in the user's shell. Returns stdout, stderr, and exit code. Large output is truncated to protect model context.
|
|
45
45
|
- **`edit`** — exact-text replacement in a single file. Fails deterministically if the target is missing or ambiguous. Creates new files when old text is empty.
|
|
46
|
+
- **`todoWrite`** — creates or updates the session todo list incrementally and returns the full current snapshot.
|
|
47
|
+
- **`todoRead`** — returns the full current session todo list snapshot.
|
|
46
48
|
- **`readImage`** — reads PNG, JPEG, GIF, and WebP files as model input. Only registered when the active model supports images.
|
|
47
49
|
|
|
48
50
|
Plugins can add more tools, but the core stays intentionally small.
|
|
@@ -68,6 +70,7 @@ Plugins can add more tools, but the core stays intentionally small.
|
|
|
68
70
|
| `/undo` | Remove the last conversational turn without touching filesystem changes. |
|
|
69
71
|
| `/reasoning` | Show or hide model thinking. The setting is saved and restored on launch. |
|
|
70
72
|
| `/verbose` | Expand shell output plus edit previews and edit errors in the conversation log. |
|
|
73
|
+
| `/todo` | Show the current session todo list in the conversation log as a UI-only checklist block. |
|
|
71
74
|
| `/login` | Sign in with a supported OAuth provider. |
|
|
72
75
|
| `/logout` | Remove saved OAuth credentials for a logged-in provider. |
|
|
73
76
|
| `/effort` | Choose low, medium, high, or xhigh reasoning effort. |
|
|
@@ -100,7 +103,7 @@ $ printf '%s\n' 'fix the failing tests' | mc
|
|
|
100
103
|
- Starts when `-p/--prompt` is provided or when stdin or stdout is not a TTY.
|
|
101
104
|
- If stdout is redirected but stdin is still interactive, pass `-p`; headless mode will not fall back to an interactive prompt.
|
|
102
105
|
- Uses the same parser as the TUI for plain text, `/skill:name`, and standalone image paths.
|
|
103
|
-
-
|
|
106
|
+
- With `--json`, writes NDJSON events for completed assistant/tool-result messages plus `done` / `error` / `aborted` outcomes; streaming deltas are omitted.
|
|
104
107
|
- Headless runs still persist like normal sessions and show up in `/session` history for that working directory.
|
|
105
108
|
- Interactive slash commands such as `/model`, `/session`, and `/help` are not available in headless mode.
|
|
106
109
|
|
package/bun.lock
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "mini-coder",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cel-tui/components": "0.7.2",
|
|
9
|
-
"@cel-tui/core": "0.7.2",
|
|
10
|
-
"@cel-tui/types": "0.7.2",
|
|
8
|
+
"@cel-tui/components": "^0.7.2",
|
|
9
|
+
"@cel-tui/core": "^0.7.2",
|
|
10
|
+
"@cel-tui/types": "^0.7.2",
|
|
11
11
|
"@mariozechner/pi-ai": "^0.66.1",
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
@@ -90,23 +90,23 @@
|
|
|
90
90
|
|
|
91
91
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
|
92
92
|
|
|
93
|
-
"@biomejs/biome": ["@biomejs/biome@2.4.
|
|
93
|
+
"@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="],
|
|
94
94
|
|
|
95
|
-
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.
|
|
95
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="],
|
|
96
96
|
|
|
97
|
-
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.
|
|
97
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="],
|
|
98
98
|
|
|
99
|
-
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.
|
|
99
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="],
|
|
100
100
|
|
|
101
|
-
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.
|
|
101
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="],
|
|
102
102
|
|
|
103
|
-
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.
|
|
103
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="],
|
|
104
104
|
|
|
105
|
-
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.
|
|
105
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="],
|
|
106
106
|
|
|
107
|
-
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.
|
|
107
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="],
|
|
108
108
|
|
|
109
|
-
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.
|
|
109
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="],
|
|
110
110
|
|
|
111
111
|
"@cel-tui/components": ["@cel-tui/components@0.7.2", "", { "dependencies": { "@cel-tui/core": "0.7.2", "@cel-tui/types": "0.7.2", "lextide": "^0.2.1", "yoctomarkdown": "^0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-RHq7G/lS6A733l534cSStQp+cJECkxZZWrLghbPHwGBsSeDdJ2MkrQm+02roh6EH/N40ZAYjQqplq8acRrnFFQ=="],
|
|
112
112
|
|
|
@@ -368,7 +368,7 @@
|
|
|
368
368
|
|
|
369
369
|
"path-expression-matcher": ["path-expression-matcher@1.2.1", "", {}, "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw=="],
|
|
370
370
|
|
|
371
|
-
"prettier": ["prettier@3.8.
|
|
371
|
+
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
|
372
372
|
|
|
373
373
|
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
|
374
374
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "A small, fast CLI coding agent",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"@mariozechner/pi-ai": "^0.66.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@biomejs/biome": "^2.4.
|
|
27
|
+
"@biomejs/biome": "^2.4.12",
|
|
28
28
|
"@types/bun": "^1.3.12",
|
|
29
29
|
"lefthook": "^2.1.5",
|
|
30
|
-
"prettier": "^3.8.
|
|
30
|
+
"prettier": "^3.8.3",
|
|
31
31
|
"typescript": "^6.0.2"
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT"
|
package/src/agent.ts
CHANGED
|
@@ -18,10 +18,11 @@ import type {
|
|
|
18
18
|
Tool,
|
|
19
19
|
ToolCall,
|
|
20
20
|
ToolResultMessage,
|
|
21
|
+
UserMessage,
|
|
21
22
|
} from "@mariozechner/pi-ai";
|
|
22
23
|
import { streamSimple } from "@mariozechner/pi-ai";
|
|
23
24
|
import { appendMessage } from "./session.ts";
|
|
24
|
-
import type
|
|
25
|
+
import { getTodoItems, type ToolExecResult } from "./tools.ts";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Types
|
|
@@ -79,6 +80,7 @@ export type AgentEvent =
|
|
|
79
80
|
args: Record<string, unknown>;
|
|
80
81
|
content: AssistantMessage["content"];
|
|
81
82
|
}
|
|
83
|
+
| { type: "user_message"; message: UserMessage }
|
|
82
84
|
| { type: "assistant_message"; message: AssistantMessage }
|
|
83
85
|
| {
|
|
84
86
|
type: "tool_start";
|
|
@@ -131,6 +133,8 @@ interface RunAgentOpts {
|
|
|
131
133
|
signal?: AbortSignal;
|
|
132
134
|
/** Callback for UI events. */
|
|
133
135
|
onEvent?: (event: AgentEvent) => void;
|
|
136
|
+
/** Dequeue the next queued steering message, if one is waiting. */
|
|
137
|
+
takeQueuedUserMessage?: () => UserMessage | null;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
/** Result of the agent loop. */
|
|
@@ -462,6 +466,19 @@ async function streamAssistantMessage(
|
|
|
462
466
|
return mergeAssistantMessage(partialAssistantMessage, finalAssistantMessage);
|
|
463
467
|
}
|
|
464
468
|
|
|
469
|
+
function appendUserMessage(
|
|
470
|
+
db: Database,
|
|
471
|
+
sessionId: string,
|
|
472
|
+
messages: Message[],
|
|
473
|
+
userMessage: UserMessage,
|
|
474
|
+
onEvent: RunAgentOpts["onEvent"],
|
|
475
|
+
): number {
|
|
476
|
+
messages.push(userMessage);
|
|
477
|
+
const turn = appendMessage(db, sessionId, userMessage);
|
|
478
|
+
onEvent?.({ type: "user_message", message: userMessage });
|
|
479
|
+
return turn;
|
|
480
|
+
}
|
|
481
|
+
|
|
465
482
|
function appendAssistantMessage(
|
|
466
483
|
db: Database,
|
|
467
484
|
sessionId: string,
|
|
@@ -490,15 +507,22 @@ function resolveLoopStopReason(
|
|
|
490
507
|
return { messages, stopReason: assistantMessage.stopReason };
|
|
491
508
|
}
|
|
492
509
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function consumeQueuedUserMessage(
|
|
514
|
+
db: Database,
|
|
515
|
+
sessionId: string,
|
|
516
|
+
messages: Message[],
|
|
517
|
+
takeQueuedUserMessage: RunAgentOpts["takeQueuedUserMessage"],
|
|
518
|
+
onEvent: RunAgentOpts["onEvent"],
|
|
519
|
+
): number | null {
|
|
520
|
+
const queuedUserMessage = takeQueuedUserMessage?.();
|
|
521
|
+
if (!queuedUserMessage) {
|
|
522
|
+
return null;
|
|
499
523
|
}
|
|
500
524
|
|
|
501
|
-
return
|
|
525
|
+
return appendUserMessage(db, sessionId, messages, queuedUserMessage, onEvent);
|
|
502
526
|
}
|
|
503
527
|
|
|
504
528
|
function getAssistantToolCalls(message: AssistantMessage): ToolCall[] {
|
|
@@ -581,6 +605,244 @@ function appendToolResultMessage(
|
|
|
581
605
|
onEvent?.({ type: "tool_result", message: toolResultMessage });
|
|
582
606
|
}
|
|
583
607
|
|
|
608
|
+
function getIncompleteTodos(messages: readonly Message[]) {
|
|
609
|
+
return getTodoItems(messages).filter((todo) => todo.status !== "completed");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function getTodoReminderSignature(
|
|
613
|
+
todos: ReturnType<typeof getIncompleteTodos>,
|
|
614
|
+
): string {
|
|
615
|
+
return JSON.stringify(
|
|
616
|
+
[...todos].sort((left, right) => {
|
|
617
|
+
const contentOrder = left.content.localeCompare(right.content);
|
|
618
|
+
if (contentOrder !== 0) {
|
|
619
|
+
return contentOrder;
|
|
620
|
+
}
|
|
621
|
+
return left.status.localeCompare(right.status);
|
|
622
|
+
}),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function createTodoReminderMessage(
|
|
627
|
+
messages: readonly Message[],
|
|
628
|
+
remindedTodoSignatures: Set<string>,
|
|
629
|
+
): UserMessage | null {
|
|
630
|
+
const incompleteTodos = getIncompleteTodos(messages);
|
|
631
|
+
if (incompleteTodos.length === 0) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const signature = getTodoReminderSignature(incompleteTodos);
|
|
636
|
+
if (remindedTodoSignatures.has(signature)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
remindedTodoSignatures.add(signature);
|
|
640
|
+
|
|
641
|
+
const lines = [
|
|
642
|
+
"You have pending todo items that must be completed before finishing the task:",
|
|
643
|
+
"",
|
|
644
|
+
...incompleteTodos.map((todo) => {
|
|
645
|
+
const status = todo.status === "in_progress" ? "IN_PROGRESS" : "PENDING";
|
|
646
|
+
return `- [${status}] ${todo.content}`;
|
|
647
|
+
}),
|
|
648
|
+
"",
|
|
649
|
+
"Please complete all pending items before finishing.",
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
role: "user",
|
|
654
|
+
content: lines.join("\n"),
|
|
655
|
+
timestamp: Date.now(),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
interface StoppedAssistantResolution {
|
|
660
|
+
/** Next turn number when a queued steering message was consumed. */
|
|
661
|
+
nextTurn: number | null;
|
|
662
|
+
/** Ephemeral context messages to include on the next model request. */
|
|
663
|
+
pendingContextMessages: Message[];
|
|
664
|
+
/** Final loop result when the turn should stop immediately. */
|
|
665
|
+
finalResult: AgentLoopResult | null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function resolveStoppedAssistantMessage(
|
|
669
|
+
assistantMessage: AssistantMessage,
|
|
670
|
+
stopReason: "stop" | "length",
|
|
671
|
+
opts: Pick<
|
|
672
|
+
RunAgentOpts,
|
|
673
|
+
"db" | "sessionId" | "messages" | "takeQueuedUserMessage" | "onEvent"
|
|
674
|
+
>,
|
|
675
|
+
remindedTodoSignatures: Set<string>,
|
|
676
|
+
): StoppedAssistantResolution {
|
|
677
|
+
const queuedTurn = consumeQueuedUserMessage(
|
|
678
|
+
opts.db,
|
|
679
|
+
opts.sessionId,
|
|
680
|
+
opts.messages,
|
|
681
|
+
opts.takeQueuedUserMessage,
|
|
682
|
+
opts.onEvent,
|
|
683
|
+
);
|
|
684
|
+
if (queuedTurn !== null) {
|
|
685
|
+
return {
|
|
686
|
+
nextTurn: queuedTurn,
|
|
687
|
+
pendingContextMessages: [],
|
|
688
|
+
finalResult: null,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const todoReminder = createTodoReminderMessage(
|
|
693
|
+
opts.messages,
|
|
694
|
+
remindedTodoSignatures,
|
|
695
|
+
);
|
|
696
|
+
if (todoReminder) {
|
|
697
|
+
return {
|
|
698
|
+
nextTurn: null,
|
|
699
|
+
pendingContextMessages: [todoReminder],
|
|
700
|
+
finalResult: null,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
opts.onEvent?.({ type: "done", message: assistantMessage });
|
|
705
|
+
return {
|
|
706
|
+
nextTurn: null,
|
|
707
|
+
pendingContextMessages: [],
|
|
708
|
+
finalResult: {
|
|
709
|
+
messages: opts.messages,
|
|
710
|
+
stopReason,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function executeAssistantToolCalls(
|
|
716
|
+
assistantMessage: AssistantMessage,
|
|
717
|
+
opts: Pick<
|
|
718
|
+
RunAgentOpts,
|
|
719
|
+
| "db"
|
|
720
|
+
| "sessionId"
|
|
721
|
+
| "messages"
|
|
722
|
+
| "toolHandlers"
|
|
723
|
+
| "cwd"
|
|
724
|
+
| "signal"
|
|
725
|
+
| "onEvent"
|
|
726
|
+
| "model"
|
|
727
|
+
>,
|
|
728
|
+
turn: number,
|
|
729
|
+
): Promise<AgentLoopResult | null> {
|
|
730
|
+
for (const toolCall of getAssistantToolCalls(assistantMessage)) {
|
|
731
|
+
const result = await executeToolCall(toolCall, opts);
|
|
732
|
+
appendToolResultMessage(
|
|
733
|
+
opts.db,
|
|
734
|
+
opts.sessionId,
|
|
735
|
+
opts.messages,
|
|
736
|
+
toolCall,
|
|
737
|
+
result,
|
|
738
|
+
turn,
|
|
739
|
+
opts.onEvent,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
if (opts.signal?.aborted) {
|
|
743
|
+
opts.onEvent?.({
|
|
744
|
+
type: "aborted",
|
|
745
|
+
message: buildIncompleteAssistantMessage(
|
|
746
|
+
{ model: opts.model, signal: opts.signal },
|
|
747
|
+
assistantMessage,
|
|
748
|
+
),
|
|
749
|
+
});
|
|
750
|
+
return { messages: opts.messages, stopReason: "aborted" };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
interface AgentIterationOutcome {
|
|
758
|
+
/** Final loop result when the run should stop immediately. */
|
|
759
|
+
finalResult: AgentLoopResult | null;
|
|
760
|
+
/** Next turn number when a queued steering message starts a new turn. */
|
|
761
|
+
nextTurn: number;
|
|
762
|
+
/** Ephemeral context messages for the next model request. */
|
|
763
|
+
pendingContextMessages: Message[];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function resolveAgentIteration(
|
|
767
|
+
assistantMessage: AssistantMessage,
|
|
768
|
+
currentTurn: number,
|
|
769
|
+
remindedTodoSignatures: Set<string>,
|
|
770
|
+
opts: Pick<
|
|
771
|
+
RunAgentOpts,
|
|
772
|
+
| "db"
|
|
773
|
+
| "sessionId"
|
|
774
|
+
| "messages"
|
|
775
|
+
| "toolHandlers"
|
|
776
|
+
| "cwd"
|
|
777
|
+
| "signal"
|
|
778
|
+
| "onEvent"
|
|
779
|
+
| "model"
|
|
780
|
+
| "takeQueuedUserMessage"
|
|
781
|
+
>,
|
|
782
|
+
): Promise<AgentIterationOutcome> {
|
|
783
|
+
const stopResult = resolveLoopStopReason(
|
|
784
|
+
assistantMessage,
|
|
785
|
+
opts.messages,
|
|
786
|
+
opts.onEvent,
|
|
787
|
+
);
|
|
788
|
+
if (stopResult) {
|
|
789
|
+
return {
|
|
790
|
+
finalResult: stopResult,
|
|
791
|
+
nextTurn: currentTurn,
|
|
792
|
+
pendingContextMessages: [],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (
|
|
797
|
+
assistantMessage.stopReason === "stop" ||
|
|
798
|
+
assistantMessage.stopReason === "length"
|
|
799
|
+
) {
|
|
800
|
+
const stopResolution = resolveStoppedAssistantMessage(
|
|
801
|
+
assistantMessage,
|
|
802
|
+
assistantMessage.stopReason,
|
|
803
|
+
{
|
|
804
|
+
db: opts.db,
|
|
805
|
+
sessionId: opts.sessionId,
|
|
806
|
+
messages: opts.messages,
|
|
807
|
+
takeQueuedUserMessage: opts.takeQueuedUserMessage,
|
|
808
|
+
onEvent: opts.onEvent,
|
|
809
|
+
},
|
|
810
|
+
remindedTodoSignatures,
|
|
811
|
+
);
|
|
812
|
+
return {
|
|
813
|
+
finalResult: stopResolution.finalResult,
|
|
814
|
+
nextTurn: stopResolution.nextTurn ?? currentTurn,
|
|
815
|
+
pendingContextMessages: stopResolution.pendingContextMessages,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const toolStopResult = await executeAssistantToolCalls(
|
|
820
|
+
assistantMessage,
|
|
821
|
+
opts,
|
|
822
|
+
currentTurn,
|
|
823
|
+
);
|
|
824
|
+
if (toolStopResult) {
|
|
825
|
+
return {
|
|
826
|
+
finalResult: toolStopResult,
|
|
827
|
+
nextTurn: currentTurn,
|
|
828
|
+
pendingContextMessages: [],
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const queuedTurn = consumeQueuedUserMessage(
|
|
833
|
+
opts.db,
|
|
834
|
+
opts.sessionId,
|
|
835
|
+
opts.messages,
|
|
836
|
+
opts.takeQueuedUserMessage,
|
|
837
|
+
opts.onEvent,
|
|
838
|
+
);
|
|
839
|
+
return {
|
|
840
|
+
finalResult: null,
|
|
841
|
+
nextTurn: queuedTurn ?? currentTurn,
|
|
842
|
+
pendingContextMessages: [],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
584
846
|
// ---------------------------------------------------------------------------
|
|
585
847
|
// Agent loop
|
|
586
848
|
// ---------------------------------------------------------------------------
|
|
@@ -609,55 +871,51 @@ export async function runAgentLoop(
|
|
|
609
871
|
onEvent,
|
|
610
872
|
toolHandlers,
|
|
611
873
|
cwd,
|
|
874
|
+
takeQueuedUserMessage,
|
|
612
875
|
} = opts;
|
|
876
|
+
let currentTurn = turn;
|
|
877
|
+
let pendingContextMessages: Message[] = [];
|
|
878
|
+
const remindedTodoSignatures = new Set<string>();
|
|
613
879
|
|
|
614
880
|
while (true) {
|
|
615
|
-
const assistantMessage = await streamAssistantMessage(
|
|
881
|
+
const assistantMessage = await streamAssistantMessage({
|
|
882
|
+
...opts,
|
|
883
|
+
messages:
|
|
884
|
+
pendingContextMessages.length > 0
|
|
885
|
+
? [...messages, ...pendingContextMessages]
|
|
886
|
+
: messages,
|
|
887
|
+
});
|
|
888
|
+
pendingContextMessages = [];
|
|
616
889
|
appendAssistantMessage(
|
|
617
890
|
db,
|
|
618
891
|
sessionId,
|
|
619
892
|
messages,
|
|
620
893
|
assistantMessage,
|
|
621
|
-
|
|
894
|
+
currentTurn,
|
|
622
895
|
onEvent,
|
|
623
896
|
);
|
|
624
897
|
|
|
625
|
-
const
|
|
898
|
+
const iterationOutcome = await resolveAgentIteration(
|
|
626
899
|
assistantMessage,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (stopResult) {
|
|
631
|
-
return stopResult;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
for (const toolCall of getAssistantToolCalls(assistantMessage)) {
|
|
635
|
-
const result = await executeToolCall(toolCall, {
|
|
636
|
-
toolHandlers,
|
|
637
|
-
cwd,
|
|
638
|
-
signal,
|
|
639
|
-
onEvent,
|
|
640
|
-
});
|
|
641
|
-
appendToolResultMessage(
|
|
900
|
+
currentTurn,
|
|
901
|
+
remindedTodoSignatures,
|
|
902
|
+
{
|
|
642
903
|
db,
|
|
643
904
|
sessionId,
|
|
644
905
|
messages,
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
906
|
+
toolHandlers,
|
|
907
|
+
cwd,
|
|
908
|
+
signal,
|
|
648
909
|
onEvent,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
{ model, signal },
|
|
656
|
-
assistantMessage,
|
|
657
|
-
),
|
|
658
|
-
});
|
|
659
|
-
return { messages, stopReason: "aborted" };
|
|
660
|
-
}
|
|
910
|
+
model,
|
|
911
|
+
takeQueuedUserMessage,
|
|
912
|
+
},
|
|
913
|
+
);
|
|
914
|
+
if (iterationOutcome.finalResult) {
|
|
915
|
+
return iterationOutcome.finalResult;
|
|
661
916
|
}
|
|
917
|
+
|
|
918
|
+
currentTurn = iterationOutcome.nextTurn;
|
|
919
|
+
pendingContextMessages = iterationOutcome.pendingContextMessages;
|
|
662
920
|
}
|
|
663
921
|
}
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export interface CliOptions {
|
|
13
13
|
/** One-shot prompt text, or `null` when not provided. */
|
|
14
14
|
prompt: string | null;
|
|
15
|
+
/** Whether to stream headless output as NDJSON instead of final text. */
|
|
16
|
+
json: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** TTY availability for stdin/stdout. */
|
|
@@ -29,7 +31,8 @@ export interface TtyState {
|
|
|
29
31
|
/**
|
|
30
32
|
* Parse supported CLI arguments.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
34
|
+
* Supports `-p, --prompt <text>` for headless one-shot mode and `--json`
|
|
35
|
+
* to stream NDJSON events instead of the default final-text output.
|
|
33
36
|
* Unknown flags and positional arguments fail eagerly.
|
|
34
37
|
*
|
|
35
38
|
* @param argv - Process arguments excluding the Bun executable and script path.
|
|
@@ -37,6 +40,7 @@ export interface TtyState {
|
|
|
37
40
|
*/
|
|
38
41
|
export function parseCliArgs(argv: readonly string[]): CliOptions {
|
|
39
42
|
let prompt: string | null = null;
|
|
43
|
+
let json = false;
|
|
40
44
|
|
|
41
45
|
for (let index = 0; index < argv.length; index += 1) {
|
|
42
46
|
const arg = argv[index];
|
|
@@ -59,6 +63,11 @@ export function parseCliArgs(argv: readonly string[]): CliOptions {
|
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
if (arg === "--json") {
|
|
67
|
+
json = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
if (arg.startsWith("-")) {
|
|
63
72
|
throw new Error(`Unknown argument: ${arg}`);
|
|
64
73
|
}
|
|
@@ -66,7 +75,7 @@ export function parseCliArgs(argv: readonly string[]): CliOptions {
|
|
|
66
75
|
throw new Error(`Unexpected positional argument: ${arg}`);
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
return { prompt };
|
|
78
|
+
return { prompt, json };
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
// ---------------------------------------------------------------------------
|