@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.
- package/README.md +79 -19
- package/dist/package.json +1 -1
- package/dist/src/cli.d.ts +22 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +47 -199
- package/dist/src/cli.js.map +1 -1
- package/dist/src/core/structured-output.js +1 -1
- package/dist/src/core/structured-output.js.map +1 -1
- package/dist/src/lib/sync-skills.d.ts.map +1 -1
- package/dist/src/lib/sync-skills.js +7 -1
- package/dist/src/lib/sync-skills.js.map +1 -1
- package/dist/src/memory/observational-prompts.d.ts.map +1 -1
- package/dist/src/memory/observational-prompts.js +6 -0
- package/dist/src/memory/observational-prompts.js.map +1 -1
- package/dist/src/memory/observational.d.ts +16 -4
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +123 -61
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/memory/storage.d.ts +5 -1
- package/dist/src/memory/storage.d.ts.map +1 -1
- package/dist/src/memory/storage.js +8 -3
- package/dist/src/memory/storage.js.map +1 -1
- package/dist/src/model-resolution/catalog.d.ts +28 -0
- package/dist/src/model-resolution/catalog.d.ts.map +1 -0
- package/dist/src/model-resolution/catalog.js +113 -0
- package/dist/src/model-resolution/catalog.js.map +1 -0
- package/dist/src/model-resolution/duet-gateway.d.ts +3 -2
- package/dist/src/model-resolution/duet-gateway.d.ts.map +1 -1
- package/dist/src/model-resolution/duet-gateway.js +4 -2
- package/dist/src/model-resolution/duet-gateway.js.map +1 -1
- package/dist/src/model-resolution/{index.d.ts → resolver.d.ts} +3 -3
- package/dist/src/model-resolution/resolver.d.ts.map +1 -0
- package/dist/src/model-resolution/resolver.js +107 -0
- package/dist/src/model-resolution/resolver.js.map +1 -0
- package/dist/src/tui/app.d.ts +2 -2
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +12 -0
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/turn-runner/turn-runner.d.ts +17 -6
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +40 -15
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/dist/src/types/memory.d.ts +6 -4
- package/dist/src/types/memory.d.ts.map +1 -1
- package/dist/src/types/protocol.d.ts +2 -2
- package/dist/src/types/protocol.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/src/model-resolution/index.d.ts.map +0 -1
- package/dist/src/model-resolution/index.js +0 -129
- 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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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: "
|
|
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
|
|
351
|
-
- Observation logs are reflected around `
|
|
352
|
-
- Raw-tail retention keeps about `
|
|
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
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
|
|
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 {
|
package/dist/src/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG;
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
977
|
-
duet --memory-model
|
|
978
|
-
duet -m
|
|
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"
|