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 CHANGED
@@ -39,10 +39,12 @@ $ mc
39
39
 
40
40
  ## Tools
41
41
 
42
- Two built-in tools, plus a read-only image tool:
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
- - Writes raw NDJSON events to stdout for text deltas, tool activity, final messages, and `done` / `error` / `aborted` outcomes.
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.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.11", "@biomejs/cli-darwin-x64": "2.4.11", "@biomejs/cli-linux-arm64": "2.4.11", "@biomejs/cli-linux-arm64-musl": "2.4.11", "@biomejs/cli-linux-x64": "2.4.11", "@biomejs/cli-linux-x64-musl": "2.4.11", "@biomejs/cli-win32-arm64": "2.4.11", "@biomejs/cli-win32-x64": "2.4.11" }, "bin": { "biome": "bin/biome" } }, "sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA=="],
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.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg=="],
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.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw=="],
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.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA=="],
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.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q=="],
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.11", "", { "os": "linux", "cpu": "x64" }, "sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ=="],
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.11", "", { "os": "linux", "cpu": "x64" }, "sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA=="],
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.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg=="],
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.11", "", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
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.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="],
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.6",
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.11",
27
+ "@biomejs/biome": "^2.4.12",
28
28
  "@types/bun": "^1.3.12",
29
29
  "lefthook": "^2.1.5",
30
- "prettier": "^3.8.2",
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 { ToolExecResult } from "./tools.ts";
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
- if (
494
- assistantMessage.stopReason === "stop" ||
495
- assistantMessage.stopReason === "length"
496
- ) {
497
- onEvent?.({ type: "done", message: assistantMessage });
498
- return { messages, stopReason: assistantMessage.stopReason };
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 null;
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(opts);
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
- turn,
894
+ currentTurn,
622
895
  onEvent,
623
896
  );
624
897
 
625
- const stopResult = resolveLoopStopReason(
898
+ const iterationOutcome = await resolveAgentIteration(
626
899
  assistantMessage,
627
- messages,
628
- onEvent,
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
- toolCall,
646
- result,
647
- turn,
906
+ toolHandlers,
907
+ cwd,
908
+ signal,
648
909
  onEvent,
649
- );
650
-
651
- if (signal?.aborted) {
652
- onEvent?.({
653
- type: "aborted",
654
- message: buildIncompleteAssistantMessage(
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
- * Currently supports only `-p, --prompt <text>` for headless one-shot mode.
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
  // ---------------------------------------------------------------------------