@duetso/agent 0.1.30 → 0.1.32

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 (50) hide show
  1. package/README.md +79 -19
  2. package/dist/package.json +1 -1
  3. package/dist/src/cli.d.ts +22 -1
  4. package/dist/src/cli.d.ts.map +1 -1
  5. package/dist/src/cli.js +47 -199
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/core/structured-output.js +1 -1
  8. package/dist/src/core/structured-output.js.map +1 -1
  9. package/dist/src/lib/sync-skills.d.ts.map +1 -1
  10. package/dist/src/lib/sync-skills.js +7 -1
  11. package/dist/src/lib/sync-skills.js.map +1 -1
  12. package/dist/src/memory/observational-prompts.d.ts.map +1 -1
  13. package/dist/src/memory/observational-prompts.js +6 -0
  14. package/dist/src/memory/observational-prompts.js.map +1 -1
  15. package/dist/src/memory/observational.d.ts +16 -4
  16. package/dist/src/memory/observational.d.ts.map +1 -1
  17. package/dist/src/memory/observational.js +123 -61
  18. package/dist/src/memory/observational.js.map +1 -1
  19. package/dist/src/memory/storage.d.ts +5 -1
  20. package/dist/src/memory/storage.d.ts.map +1 -1
  21. package/dist/src/memory/storage.js +8 -3
  22. package/dist/src/memory/storage.js.map +1 -1
  23. package/dist/src/model-resolution/catalog.d.ts +28 -0
  24. package/dist/src/model-resolution/catalog.d.ts.map +1 -0
  25. package/dist/src/model-resolution/catalog.js +113 -0
  26. package/dist/src/model-resolution/catalog.js.map +1 -0
  27. package/dist/src/model-resolution/duet-gateway.d.ts +3 -2
  28. package/dist/src/model-resolution/duet-gateway.d.ts.map +1 -1
  29. package/dist/src/model-resolution/duet-gateway.js +4 -2
  30. package/dist/src/model-resolution/duet-gateway.js.map +1 -1
  31. package/dist/src/model-resolution/{index.d.ts → resolver.d.ts} +3 -3
  32. package/dist/src/model-resolution/resolver.d.ts.map +1 -0
  33. package/dist/src/model-resolution/resolver.js +107 -0
  34. package/dist/src/model-resolution/resolver.js.map +1 -0
  35. package/dist/src/tui/app.d.ts +2 -2
  36. package/dist/src/tui/app.d.ts.map +1 -1
  37. package/dist/src/tui/app.js +12 -0
  38. package/dist/src/tui/app.js.map +1 -1
  39. package/dist/src/turn-runner/turn-runner.d.ts +17 -6
  40. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  41. package/dist/src/turn-runner/turn-runner.js +40 -15
  42. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  43. package/dist/src/types/memory.d.ts +6 -4
  44. package/dist/src/types/memory.d.ts.map +1 -1
  45. package/dist/src/types/protocol.d.ts +2 -2
  46. package/dist/src/types/protocol.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/src/model-resolution/index.d.ts.map +0 -1
  49. package/dist/src/model-resolution/index.js +0 -129
  50. package/dist/src/model-resolution/index.js.map +0 -1
package/README.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  An opinionated, full-stack agent turn runner. Native multimodal memory. Native interrupts. Multi-agent by default. Serverless-friendly: every turn rehydrates from on-disk state, so a session can pause for minutes or months and resume in a fresh sandbox.
4
4
 
5
+ ## Get started with one login
6
+
7
+ ```bash
8
+ bun add --global @duetso/agent
9
+ duet login
10
+ duet "build a REST API with Express"
11
+ ```
12
+
13
+ `duet login` opens your browser, signs you in, and writes a single `DUET_API_KEY` to `~/.duet/.env`. That one key unlocks:
14
+
15
+ - **Frontier language models** — Claude Opus / Sonnet / Haiku, GPT-5, Gemini, and friends routed through the Duet AI Gateway. No separate Anthropic, OpenAI, or Google billing to set up.
16
+ - **Image and video generation** — GPT Image 2, Seedance, and the other media models the gateway exposes.
17
+ - **Web scraping and research** — Firecrawl-powered scraping, search, and browser automation come bundled as default skills.
18
+ - **The latest Duet skills** — auto-synced into `~/.duet/skills` on first login and refreshed in the background on every subsequent run.
19
+
20
+ One login, every frontier model, every default skill, kept fresh. If you would rather wire your own provider keys, see [CLI Env Setup](#cli-env-setup).
21
+
5
22
  ## Why another agent framework?
6
23
 
7
24
  Existing agent turn runners treat tools and memories as pluggable modules. This makes them flexible but fundamentally disconnected — memory is an afterthought.
@@ -198,14 +215,14 @@ The pre-commit hook runs `format`, `check-types`, and `lint`.
198
215
 
199
216
  ## CLI Quick Start
200
217
 
201
- The quickest way to get started is `duet login`, which signs in via your browser, writes `DUET_API_KEY` to `~/.duet/.env`, and syncs the default skills:
218
+ The recommended path is `duet login`. One sign-in writes `DUET_API_KEY` to `~/.duet/.env`, syncs the default skills, and gives you access to every frontier language, image, and video model on the Duet AI Gateway plus the bundled web-scraping skills — no other API keys required.
202
219
 
203
220
  ```bash
204
221
  duet login
205
222
  duet "build a REST API with Express"
206
223
  ```
207
224
 
208
- If you would rather manage provider API keys yourself, use `duet env` (see [CLI Env Setup](#cli-env-setup) below) or set a provider API key in the environment, `<workdir>/.env`, or `~/.duet/.env`. When `--model` is omitted, the CLI infers a default from the configured provider: Anthropic, AI Gateway, and OpenRouter use Opus 4.7; OpenAI uses GPT-5.5.
225
+ If you would rather manage provider API keys yourself, use `duet env` (see [CLI Env Setup](#cli-env-setup) below) or set a provider API key in the environment, `<workdir>/.env`, or `~/.duet/.env`. When `--model` is omitted, the CLI infers a default from the configured provider: Duet, Anthropic, AI Gateway, and OpenRouter use Opus 4.7; OpenAI uses GPT-5.5.
209
226
 
210
227
  ```bash
211
228
  export ANTHROPIC_API_KEY=sk-...
@@ -217,10 +234,10 @@ duet "build a REST API with Express"
217
234
  duet
218
235
 
219
236
  # With options
220
- duet -m anthropic:claude-opus-4-7 --workdir ./my-project "refactor the auth module"
237
+ duet -m opus-4.7 --workdir ./my-project "refactor the auth module"
221
238
 
222
239
  # With a custom observational memory model
223
- duet --memory-model anthropic:claude-sonnet-4-6 "summarize this repo"
240
+ duet --memory-model sonnet-4.6 "summarize this repo"
224
241
 
225
242
  # With additional system instructions
226
243
  duet --system-prompt "Prefer concise answers." "review this repo"
@@ -239,13 +256,20 @@ duet skills
239
256
 
240
257
  # Through Vercel AI Gateway
241
258
  export AI_GATEWAY_API_KEY=...
242
- duet -m vercel-ai-gateway:anthropic/claude-opus-4.7 "review this repo"
259
+ duet -m opus-4.7 "review this repo"
243
260
  ```
244
261
 
262
+ Model names can use full `provider:modelId` syntax or shorthand names such as
263
+ `opus-4.7`, `sonnet-4.6`, `haiku-4.5`, and `gpt-5.5`. Shorthands resolve to the
264
+ first configured supported provider; use full `provider:modelId` syntax to pin a
265
+ specific provider.
266
+
245
267
  ### CLI Login
246
268
 
247
269
  `duet login` is the recommended setup path. It opens a browser to sign in, writes `DUET_API_KEY` for the selected org to `~/.duet/.env`, and syncs the latest default skills into `~/.duet/skills`.
248
270
 
271
+ That single `DUET_API_KEY` is your access token to the Duet AI Gateway: frontier language models (Claude, GPT, Gemini), image generation (GPT Image 2), video generation (Seedance), and the Firecrawl-powered web scraping and search skills, all behind one key.
272
+
249
273
  Once you have synced default skills at least once, every subsequent `duet` invocation refreshes them in the background using a conditional GET against the saved hash. Logging in with `--skip-skill-sync` leaves no hash on disk, so this auto-refresh stays a no-op until you explicitly opt in by syncing once.
250
274
 
251
275
  ```bash
@@ -291,7 +315,7 @@ bun run cli -- "build a REST API with Express"
291
315
  import { TurnRunner } from "@duetso/agent";
292
316
 
293
317
  const turnRunner = new TurnRunner({
294
- model: "anthropic:claude-opus-4-7",
318
+ model: "opus-4.7",
295
319
  cwd: process.cwd(),
296
320
  mode: "auto",
297
321
  });
@@ -306,28 +330,64 @@ const terminal = await turnRunner.turn({
306
330
  });
307
331
  ```
308
332
 
309
- `TurnRunner.turn()` is the concurrency boundary. Callers may call it repeatedly
310
- while work is active; the runner folds active `prompt` and `answer` commands
311
- back into the active pi agent as `steer` or `follow_up`, queues wakes and other
312
- work it cannot absorb immediately, and emits one terminal event when the whole
313
- active work chain is done. The parent runner transcript stays linear: state
314
- machine continuations, script results, poll results, and user follow-ups rejoin
315
- the parent agent rather than creating separate conversation branches.
333
+ ## TurnRunner Lifecycle
334
+
335
+ `start()` prepares a session, but it does not run agent work. It hydrates durable memory, loads skills and agent files, connects MCP servers, creates or resumes `TurnState`, initializes the parent pi agent, and emits `turn_started`. After that, every `turn()` call accepts one command: `prompt`, `answer`, or `wake`.
336
+
337
+ ```mermaid
338
+ flowchart TD
339
+ Start["start(command)"] --> Setup["hydrate memory\nload skills + agent files\nconnect MCP servers"]
340
+ Setup --> State["create or resume TurnState"]
341
+ State --> Parent["initialize parent pi agent"]
342
+ Parent --> Started["emit turn_started"]
343
+
344
+ Started --> Command["turn(prompt | answer | wake)"]
345
+ Command --> Active{"active turn\nalready running?"}
346
+ Active -- yes --> Fold["fold prompt/answer into parent agent\nor queue work behind active chain"]
347
+ Fold --> ActiveTerminal["return activeTurnPromise"]
348
+
349
+ Active -- no --> Chain["run turn chain"]
350
+ Chain --> Dispatch{"command type"}
351
+ Dispatch -- prompt/answer --> Mode{"state.mode"}
352
+ Dispatch -- wake --> Wake["resume scheduled poll/timer state"]
353
+
354
+ Mode -- agent --> ParentRun["run parent pi agent"]
355
+ Mode -- auto or explicit state machine --> Router["parent agent selects next state"]
356
+ Router --> StateRun["run agent, script, poll, timer,\nor terminal state"]
357
+ StateRun --> StateResult{"state result"}
358
+ StateResult -- completed --> Router
359
+ StateResult -- ask --> Terminal
360
+ StateResult -- sleep --> Terminal
361
+ StateResult -- terminal/interrupted --> Terminal
362
+ Wake --> StateRun
363
+
364
+ ParentRun --> Observe["observe transcript suffix\nreflect large observation logs\nflush durable memory"]
365
+ Router --> Observe
366
+ Observe --> Queue{"queued commands?"}
367
+ Queue -- yes --> Dispatch
368
+ Queue -- no --> Terminal["emit one terminal event\ncomplete | ask | sleep | interrupted"]
369
+ Terminal --> Snapshot["store latest TurnState in runner"]
370
+ Snapshot --> Next["caller persists snapshot\nand may call turn() again later"]
371
+ ```
372
+
373
+ `TurnRunner.turn()` is the concurrency boundary. Callers may invoke it repeatedly while work is active; the runner folds active `prompt` and `answer` commands back into the active pi agent as `steer` or `follow_up`, queues wakes and work it cannot absorb immediately, and emits one terminal event when the whole active work chain is done.
374
+
375
+ The parent runner transcript stays linear across the lifecycle. State-machine continuations, script results, poll results, and user follow-ups all rejoin the parent agent instead of creating separate conversation branches. Terminal events carry the next `TurnState`; callers that need process-level durability persist that snapshot and pass it back to a later `start({ state })`.
316
376
 
317
377
  ## Memory And Persistence
318
378
 
319
- `TurnRunner` owns memory at runtime. It holds a `MemoryStore` in process, hydrates durable observations from PGlite before the first turn, and subscribes to memory-store events to write future observation changes. Raw conversation messages stay in `TurnState.agent.messages`; memory persistence stores only derived observations/reflections.
379
+ `TurnRunner` owns memory at runtime. It holds a `MemoryStore` in process, hydrates durable observations from PGlite before the first turn, and subscribes to memory-store events to write future observation changes. After each pi-agent run, the runner observes the latest unobserved transcript suffix, reflects oversized observation logs, emits memory events with generated observation payloads, and waits for durable writes before continuing. Raw conversation messages stay in `TurnState.agent.messages`; memory persistence stores only derived observations/reflections.
320
380
 
321
381
  ```typescript
322
382
  import { TurnRunner } from "@duetso/agent";
323
383
 
324
384
  const turnRunner = new TurnRunner({
325
- model: "anthropic:claude-opus-4-7",
385
+ model: "opus-4.7",
326
386
  memoryDbPath: false, // Keep observational memory in process only.
327
387
  });
328
388
  ```
329
389
 
330
- By default, the CLI stores durable observations in `~/.duet/memory.db`; run it with `--no-memory` to keep observational memory in process only. Programmatic callers can pass `memoryDbPath: false` or provide a custom `memoryDbPath`. The CLI's `SessionManager` is a convenience layer that stores session snapshots under `~/.duet/sessions`, but the runner owns memory hydration, compaction, and observation persistence.
390
+ By default, the CLI stores durable observations in `~/.duet/memory.db`; run it with `--no-memory` to keep observational memory in process only. Programmatic callers can pass `memoryDbPath: false` or provide a custom `memoryDbPath`. The CLI's `SessionManager` is a convenience layer that stores session snapshots under `~/.duet/sessions`, but the runner owns memory hydration, pi-turn observation/reflection, compaction, and observation persistence.
331
391
 
332
392
  You can also resume directly from saved state. The runner owns state
333
393
  internally after `start`, so resumed state is handed in through the start
@@ -347,9 +407,9 @@ Resume continues turn runner session state, not an in-flight model/tool call. An
347
407
 
348
408
  Observational memory is enabled by default with thresholds tuned for modern 200k-token model windows:
349
409
 
350
- - Raw messages are observed around `150_000` tokens so exact transcript context and prompt caching are used before compaction.
351
- - Observation logs are reflected around `90_000` tokens, targeting about `65_000` tokens after reflection.
352
- - Raw-tail retention keeps about `40_000` exact message tokens after observation activation.
410
+ - Raw messages are observed after each pi-agent run; the `100_000` token threshold controls when old raw transcript context is replaced.
411
+ - Observation logs are reflected around `60_000` tokens, targeting about `40_000` tokens after reflection.
412
+ - Raw-tail retention keeps about `30_000` exact message tokens after context replacement activates.
353
413
  - Observation context is injected as reminder messages; replacing raw context with observations/reflections is the compaction path.
354
414
 
355
415
  ## Skills
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duetso/agent",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "An opinionated full-stack agent turn runner with native memories, interrupts, and multi-agent orchestration",
5
5
  "keywords": [
6
6
  "agent",
package/dist/src/cli.d.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  *
5
5
  * Usage:
6
6
  * duet "build a todo app in React"
7
- * duet --model claude-opus-4-7 "refactor auth system"
7
+ * duet --model opus-4.7 "refactor auth system"
8
8
  * echo "fix the bug in server.ts" | duet
9
9
  */
10
+ import { type ModelResolution } from "./model-resolution/resolver.js";
11
+ import type { TurnRunnerConfig } from "./types/config.js";
10
12
  declare const PACKAGE_MANAGERS: readonly ["npm", "bun", "pnpm", "yarn"];
11
13
  type PackageManager = (typeof PACKAGE_MANAGERS)[number];
12
14
  type PackageManagerDetectionContext = {
@@ -15,11 +17,30 @@ type PackageManagerDetectionContext = {
15
17
  cliFilePath?: string;
16
18
  scriptPath?: string;
17
19
  };
20
+ export interface CliTurnConfigInput {
21
+ modelName?: string;
22
+ memoryModelName?: string;
23
+ disableDurableMemory?: boolean;
24
+ workDir: string;
25
+ systemInstructions?: string;
26
+ systemPromptFiles?: string[];
27
+ }
28
+ export interface CliTurnConfigResolution {
29
+ config: TurnRunnerConfig;
30
+ modelResolution: ModelResolution;
31
+ memoryModelResolution: ModelResolution;
32
+ }
33
+ export declare function buildCliTurnConfig(input: CliTurnConfigInput, dotenvKeys: Set<string>): CliTurnConfigResolution;
18
34
  export declare function resolveUserPath(path: string, baseDir?: string): string;
19
35
  export declare function defaultDuetEnvFilePath(): string;
20
36
  export declare function cliEnvFilePaths(workDir: string, envFilePath?: string): string[];
21
37
  export declare function loadCliEnvFiles(workDir: string, envFilePath?: string): Set<string>;
22
38
  export declare function parseResumeHistoryLines(value: string, optionName?: string): number;
39
+ export declare function shouldUseTui(input: {
40
+ interactive: boolean;
41
+ jsonOutput: boolean;
42
+ prompt?: string;
43
+ }): boolean;
23
44
  export declare function formatNewVersionNotice(packageName: string, currentVersion: string, latestVersion: string): string;
24
45
  export declare function compareSemverVersions(left: string, right: string): number;
25
46
  interface EnvCommandIO {
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG;AA6BH,QAAA,MAAM,gBAAgB,yCAA0C,CAAC;AAUjE,KAAK,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,KAAK,8BAA8B,GAAG;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA4bF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAgB,GAAG,MAAM,CAI7E;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CASlF;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,UAAU,SAA2B,GACpC,MAAM,CAKR;AAeD,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,GACpB,MAAM,CAER;AAmBD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAWzE;AA0FD,UAAU,YAAY;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DxF;AAED,UAAU,cAAc;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmD5F;AAqDD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAErE;AAqBD,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,8BAA8B,GACtC,cAAc,CAiBhB;AAED,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,MAAM,EAAE,CAMV;AAiCD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IACL,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,GACA,MAAM,CAoCR"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/src/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Usage:
6
6
  * duet "build a todo app in React"
7
- * duet --model claude-opus-4-7 "refactor auth system"
7
+ * duet --model opus-4.7 "refactor auth system"
8
8
  * echo "fix the bug in server.ts" | duet
9
9
  */
10
10
  import { spawn } from "node:child_process";
@@ -16,11 +16,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
16
16
  import dotenv from "dotenv";
17
17
  import packageJson from "../package.json" with { type: "json" };
18
18
  import { shimDuetApiKeyToAiGateway } from "./model-resolution/duet-gateway.js";
19
- import { formatCompactJson } from "./lib/compact-json.js";
20
19
  import { resolveDuetAppBaseUrl } from "./lib/duet-app-url.js";
21
20
  import { loginWithBrowser } from "./lib/login.js";
22
21
  import { maybeAutoSyncDefaultSkills, syncDefaultSkills } from "./lib/sync-skills.js";
23
- import { describeModelResolution, resolveCliMemoryModel, resolveCliModel, } from "./model-resolution/index.js";
22
+ import { describeModelResolution, resolveCliMemoryModel, resolveCliModel, } from "./model-resolution/resolver.js";
24
23
  import { SessionManager } from "./session/session-manager.js";
25
24
  import { discoverInstalledSkills, resolveSkillScope } from "./turn-runner/skills.js";
26
25
  import { runTui } from "./tui/app.js";
@@ -202,30 +201,23 @@ async function main() {
202
201
  if (process.env.DUET_API_KEY) {
203
202
  await maybeAutoSyncDefaultSkills({ apiKey: process.env.DUET_API_KEY });
204
203
  }
205
- const modelResolution = resolveCliModel(modelName, dotenvKeys);
204
+ const { config, modelResolution, memoryModelResolution } = buildCliTurnConfig({
205
+ modelName,
206
+ memoryModelName,
207
+ disableDurableMemory,
208
+ workDir,
209
+ systemInstructions,
210
+ systemPromptFiles,
211
+ }, dotenvKeys);
206
212
  modelName = modelResolution.modelName;
207
- const memoryModelResolution = resolveCliMemoryModel(memoryModelName, dotenvKeys);
208
213
  memoryModelName = memoryModelResolution.modelName;
209
- if (modelName && modelName.indexOf(":") <= 0) {
210
- throw new Error("Models must use provider:modelId syntax");
211
- }
212
- if (memoryModelName && memoryModelName.indexOf(":") <= 0) {
213
- throw new Error("Memory model must use provider:modelId syntax");
214
- }
215
- // Build config
216
- const config = {
217
- ...(modelName ? { model: modelName } : {}),
218
- ...(memoryModelName ? { memoryModel: memoryModelName } : {}),
219
- ...(disableDurableMemory ? { memoryDbPath: false } : {}),
220
- cwd: workDir,
221
- ...(systemInstructions ? { systemInstructions } : {}),
222
- ...(systemPromptFiles ? { systemPromptFiles } : {}),
223
- };
224
- // The TUI owns rendering when active, so we suppress stdout step printing
225
- // there to avoid corrupting the alternate-screen UI.
226
- const useTui = interactive && !jsonOutput;
214
+ // The CLI has exactly two rendering modes: the interactive TUI, or JSONL
215
+ // events. Supplying a prompt selects JSONL so one-shot runs have a stable
216
+ // machine-readable contract by default.
217
+ const useTui = shouldUseTui({ interactive, jsonOutput, prompt });
218
+ const useJson = !useTui;
227
219
  const newVersionNotice = await getNewVersionNotice();
228
- if (!useTui) {
220
+ if (useJson) {
229
221
  if (newVersionNotice)
230
222
  process.stderr.write(`${newVersionNotice}\n`);
231
223
  process.stderr.write(`Model: ${modelName}\n`);
@@ -234,81 +226,18 @@ async function main() {
234
226
  process.stderr.write(`Memory source: ${describeModelResolution(memoryModelResolution)}\n`);
235
227
  }
236
228
  const manager = new SessionManager(config);
237
- let streamedTextThisTurn = false;
238
- let activeTextDelta = false;
239
- let activeTextDeltaNeedsNewline = false;
240
- let activeReasoningDelta = false;
241
- let activeReasoningDeltaNeedsNewline = false;
242
- const finishActiveDeltaStreams = () => {
243
- if (activeTextDelta) {
244
- if (activeTextDeltaNeedsNewline)
245
- process.stdout.write("\n");
246
- activeTextDelta = false;
247
- activeTextDeltaNeedsNewline = false;
248
- }
249
- if (activeReasoningDelta) {
250
- if (activeReasoningDeltaNeedsNewline)
251
- process.stderr.write("\n");
252
- process.stderr.write("[/reasoning]\n");
253
- activeReasoningDelta = false;
254
- activeReasoningDeltaNeedsNewline = false;
255
- }
256
- };
257
229
  manager.subscribe(({ event }) => {
258
- if (jsonOutput) {
230
+ if (useJson) {
259
231
  process.stdout.write(`${JSON.stringify(event)}\n`);
260
232
  }
261
- else if (event.type === "step" && !useTui) {
262
- if (event.step.type === "text_delta") {
263
- streamedTextThisTurn = true;
264
- activeTextDelta = true;
265
- activeTextDeltaNeedsNewline = !event.step.delta.endsWith("\n");
266
- process.stdout.write(event.step.delta);
267
- }
268
- else if (event.step.type === "reasoning_delta") {
269
- if (!activeReasoningDelta)
270
- process.stderr.write("\n[reasoning]\n");
271
- activeReasoningDelta = true;
272
- activeReasoningDeltaNeedsNewline = !event.step.delta.endsWith("\n");
273
- process.stderr.write(event.step.delta);
274
- }
275
- else if (event.step.type === "text") {
276
- streamedTextThisTurn = true;
277
- if (activeTextDelta) {
278
- finishActiveDeltaStreams();
279
- }
280
- else {
281
- handleStep(event.step);
282
- }
283
- }
284
- else if (event.step.type === "reasoning" && activeReasoningDelta) {
285
- finishActiveDeltaStreams();
286
- }
287
- else {
288
- handleStep(event.step);
289
- }
290
- }
291
- else if (event.type === "step" &&
292
- (event.step.type === "text" || event.step.type === "text_delta")) {
293
- // Track streaming even when the TUI is rendering, so post-TUI fallback
294
- // result handling stays consistent.
295
- streamedTextThisTurn = true;
296
- }
297
- else if (!jsonOutput && !useTui && isTerminalEvent(event)) {
298
- finishActiveDeltaStreams();
299
- }
300
233
  });
301
234
  try {
302
235
  const session = resumeSessionId
303
236
  ? manager.resume(resumeSessionId)
304
237
  : manager.create({
305
238
  ...(config.mode ? { mode: config.mode } : {}),
306
- // Defer the prompt for TUI sessions so the user sees the prompt
307
- // streamed inside the UI rather than during the pre-TUI phase.
308
239
  ...(useTui || !prompt ? {} : { prompt }),
309
240
  });
310
- let terminal;
311
- let initialTuiPrompt;
312
241
  let resumedHistory;
313
242
  if (resumeSessionId) {
314
243
  // Force-load the persisted state.json so setup hands the resumed
@@ -323,35 +252,15 @@ async function main() {
323
252
  resumedHistory = session.getState()?.agent.messages;
324
253
  }
325
254
  if (prompt && resumeSessionId) {
326
- streamedTextThisTurn = false;
327
- if (useTui) {
328
- initialTuiPrompt = prompt;
329
- }
330
- else {
331
- await session.prompt({ message: prompt });
332
- terminal = await session.waitForTerminal();
333
- handleTerminal(terminal, {
334
- suppressHumanOutput: jsonOutput,
335
- suppressResult: streamedTextThisTurn,
336
- });
337
- }
255
+ await session.prompt({ message: prompt });
256
+ await session.waitForTerminal();
338
257
  }
339
258
  else if (prompt && !resumeSessionId) {
340
- if (useTui) {
341
- initialTuiPrompt = prompt;
342
- }
343
- else {
344
- terminal = await session.waitForTerminal();
345
- handleTerminal(terminal, {
346
- suppressHumanOutput: jsonOutput,
347
- suppressResult: streamedTextThisTurn,
348
- });
349
- }
259
+ await session.waitForTerminal();
350
260
  }
351
261
  if (useTui) {
352
- terminal = await runTui({
262
+ await runTui({
353
263
  session,
354
- ...(initialTuiPrompt ? { initialPrompt: initialTuiPrompt } : {}),
355
264
  ...(resumedHistory ? { history: resumedHistory } : {}),
356
265
  resumeHistoryLines,
357
266
  modelName,
@@ -383,91 +292,26 @@ async function main() {
383
292
  await manager.dispose();
384
293
  }
385
294
  }
386
- function handleTerminal(terminal, options = {}) {
387
- if (options.suppressHumanOutput)
388
- return;
389
- if (terminal.type === "complete" && terminal.error) {
390
- throw new Error(terminal.error);
391
- }
392
- if (terminal.type === "complete" && terminal.result && !options.suppressResult) {
393
- process.stdout.write(`${terminal.result}\n`);
394
- }
395
- if (terminal.type === "ask") {
396
- for (const question of terminal.questions) {
397
- process.stdout.write(`${question.question}\n`);
398
- }
399
- }
400
- if (terminal.type === "interrupted") {
401
- process.stderr.write("Interrupted.\n");
402
- }
403
- if (terminal.type === "sleep") {
404
- process.stderr.write(`Sleeping until ${new Date(terminal.wakeAt).toISOString()}.\n`);
405
- }
406
- if (terminal.usage) {
407
- process.stderr.write(`${formatUsage(terminal.usage)}\n`);
408
- }
409
- }
410
- function isTerminalEvent(event) {
411
- return (event.type === "complete" ||
412
- event.type === "ask" ||
413
- event.type === "interrupted" ||
414
- event.type === "sleep");
415
- }
416
- function formatUsage(usage) {
417
- const parts = [`in=${usage.input}`, `out=${usage.output}`];
418
- if (usage.cacheRead > 0)
419
- parts.push(`cached=${usage.cacheRead}`);
420
- let line = `Tokens: ${parts.join(" ")}`;
421
- if (usage.cost.total > 0)
422
- line += ` \u00b7 Cost: $${usage.cost.total.toFixed(4)}`;
423
- return line;
424
- }
425
- function handleStep(step) {
426
- if (step.type === "text_delta") {
427
- process.stdout.write(step.delta);
428
- }
429
- if (step.type === "reasoning_delta") {
430
- process.stderr.write(step.delta);
431
- }
432
- if (step.type === "text") {
433
- process.stdout.write(`${step.text}\n`);
434
- }
435
- if (step.type === "reasoning") {
436
- process.stderr.write(formatReasoning(step.text));
437
- }
438
- if (step.type === "tool_call") {
439
- process.stderr.write(formatToolCall(step));
440
- }
441
- if (step.type === "system") {
442
- process.stderr.write(`${step.message}\n`);
443
- }
444
- }
445
- function formatReasoning(text) {
446
- const trimmed = text.trim();
447
- if (!trimmed)
448
- return "";
449
- return `\n[reasoning]\n${trimmed}\n[/reasoning]\n`;
450
- }
451
- function formatToolCall(step) {
452
- const status = step.status ? ` ${step.status}` : "";
453
- const input = step.input === undefined ? "" : `\n${formatCompactJson(step.input)}`;
454
- let output = "";
455
- if (step.output && step.output.length > 0) {
456
- const text = step.output
457
- .filter((b) => b.type === "text")
458
- .map((b) => b.text)
459
- .join("\n");
460
- if (text) {
461
- const label = step.status === "error" ? "output error" : "output";
462
- output = `\n[${label}]\n${text}\n[/output]\n`;
463
- }
464
- }
465
- return `\n[tool ${step.toolName}${status}]${input}${output}\n[/tool]\n`;
466
- }
467
295
  function fail(message) {
468
296
  console.error(`Fatal: ${message}`);
469
297
  process.exit(1);
470
298
  }
299
+ export function buildCliTurnConfig(input, dotenvKeys) {
300
+ const modelResolution = resolveCliModel(input.modelName, dotenvKeys);
301
+ const memoryModelResolution = resolveCliMemoryModel(input.memoryModelName, dotenvKeys);
302
+ return {
303
+ config: {
304
+ model: modelResolution.modelName,
305
+ memoryModel: memoryModelResolution.modelName,
306
+ ...(input.disableDurableMemory ? { memoryDbPath: false } : {}),
307
+ cwd: input.workDir,
308
+ ...(input.systemInstructions ? { systemInstructions: input.systemInstructions } : {}),
309
+ ...(input.systemPromptFiles ? { systemPromptFiles: input.systemPromptFiles } : {}),
310
+ },
311
+ modelResolution,
312
+ memoryModelResolution,
313
+ };
314
+ }
471
315
  export function resolveUserPath(path, baseDir = process.cwd()) {
472
316
  if (path === "~")
473
317
  return homedir();
@@ -500,6 +344,9 @@ export function parseResumeHistoryLines(value, optionName = "--resume-history-li
500
344
  }
501
345
  return Number(value);
502
346
  }
347
+ export function shouldUseTui(input) {
348
+ return input.interactive && !input.jsonOutput && !input.prompt;
349
+ }
503
350
  async function getNewVersionNotice() {
504
351
  try {
505
352
  const latestVersion = await fetchLatestPackageVersion(PACKAGE_METADATA.name);
@@ -952,7 +799,7 @@ OPTIONS
952
799
  Load a file into the system prompt; repeatable
953
800
  --no-system-prompt-files Disable default AGENTS.md system prompt loading
954
801
  --env-file <path> Shared env file to load after <workdir>/.env (default: ${DEFAULT_DUET_ENV_FILE})
955
- --json Print streamed events as JSON lines
802
+ --json Force JSONL event output instead of the TUI
956
803
  -v, --version Print the installed duet version and exit
957
804
  -h, --help Show this help
958
805
 
@@ -961,7 +808,9 @@ INTERACTIVE
961
808
  Type /exit or /quit to end the conversation.
962
809
 
963
810
  MODELS
964
- Use provider:modelId syntax, e.g. anthropic:claude-opus-4-7.
811
+ Prefer shorthands like opus-4.7, sonnet-4.6, haiku-4.5, and gpt-5.5.
812
+ They map to the first configured provider that supports that model.
813
+ Full provider:modelId syntax is also supported, e.g. anthropic:claude-opus-4-7.
965
814
  If omitted, duet infers a default from ANTHROPIC_API_KEY,
966
815
  DUET_API_KEY, AI_GATEWAY_API_KEY, OPENROUTER_API_KEY, or
967
816
  OPENAI_API_KEY after loading <workdir>/.env and the shared duet env file.
@@ -973,10 +822,9 @@ MODELS
973
822
 
974
823
  EXAMPLES
975
824
  duet "build a REST API with Express and TypeScript"
976
- duet -m openai:gpt-5.5 "analyze the performance of our test suite"
977
- duet --memory-model anthropic:claude-sonnet-4-6 "summarize this repo"
978
- duet -m vercel-ai-gateway:anthropic/claude-opus-4.7 "refactor the auth module"
979
- duet -m duet-gateway:anthropic/claude-opus-4.7 "review this repo"
825
+ duet -m gpt-5.5 "analyze the performance of our test suite"
826
+ duet --memory-model sonnet-4.6 "summarize this repo"
827
+ duet -m opus-4.7 "refactor the auth module"
980
828
  duet --system-prompt "Prefer concise answers." "review this repo"
981
829
  duet --system-prompt-file TEAM.md "review this repo"
982
830
  duet --env-file ~/.config/duet/env "review this repo"