tokenmaw 0.3.0 → 0.4.1
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 +22 -2
- package/agents/coordinator.md +2 -3
- package/agents/main.md +2 -2
- package/dist/backend.js +31 -1
- package/dist/cli.js +46 -0
- package/dist/infra/tools.js +274 -28
- package/dist/markdown.js +83 -48
- package/dist/responses.js +7 -1
- package/dist/runtime/agent-registry.js +35 -5
- package/dist/runtime/agent-runtime.js +309 -19
- package/dist/runtime/agent-store.js +52 -0
- package/dist/runtime/file-lock.js +256 -0
- package/dist/runtime/locks.js +58 -38
- package/dist/runtime/session-timeline.js +32 -3
- package/dist/runtime/workspace-instances.js +109 -0
- package/dist/runtime/worktree.js +321 -0
- package/dist/ui/bracketed-paste.js +231 -0
- package/dist/ui/commands.js +11 -0
- package/dist/ui/fullscreen-tui.js +1282 -122
- package/dist/ui/markdown.js +19 -9
- package/dist/ui/scrollbar.js +370 -0
- package/dist/ui/syntax.js +3 -5
- package/dist/ui/theme.js +198 -0
- package/dist/ui/tui-design.js +78 -0
- package/dist/ui/welcome.js +555 -11
- package/dist/update-check.js +332 -0
- package/docs/architecture-revision.md +1 -1
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -17,7 +17,10 @@ Inside the TUI:
|
|
|
17
17
|
- `/model` selects the default model for the current session.
|
|
18
18
|
- `/agents` shows the effective Agent Specs and their sources.
|
|
19
19
|
- `/sessions` switches sessions; `/new` creates one.
|
|
20
|
+
- `/cd <path>` switches the working directory (relative paths resolve against the current one). The status bar always shows the current directory; `/cd` with no argument or `/pwd` prints it. Switching reloads the project's `.coder/agents` specs and `AGENTS.md`, and tool path authorization follows the new root.
|
|
21
|
+
- `!<command>` runs a shell command directly in the current workspace, outside the agent loop: the prompt turns into `$` and the command text is highlighted while typing, the command and its output stream inline into the conversation transcript (never sent to the model or fed back into context), long-running commands animate their ellipsis, and `Ctrl+C` stops a running command. `/`-commands stay reserved for the built-in slash commands.
|
|
20
22
|
- `Ctrl+K` opens the command palette.
|
|
23
|
+
- `/worktree <name>` creates (or reopens) an isolated git worktree under `.coder/worktrees/<name>` on branch `maw/<name>` and moves this session into it, so parallel sessions stop colliding on one working tree. `/worktree-list` shows status, `/worktree-exit` returns to the main checkout (the worktree stays on disk), `/worktree-remove <name>` drops a clean worktree (the branch is never deleted). A `.worktreeinclude` file copies gitignored files (e.g. `.env`) into new worktrees. Non-interactive equivalent: `maw --worktree [name]`.
|
|
21
24
|
- `Ctrl+B` toggles Agent Activity.
|
|
22
25
|
- `Ctrl+J` or `Alt+Enter` inserts a newline; `Enter` sends. Chinese text wraps by terminal width.
|
|
23
26
|
- `Up` / `Down` browse input history; `Ctrl+U` clears the draft.
|
|
@@ -28,7 +31,10 @@ Inside the TUI:
|
|
|
28
31
|
- `F2` (or `/select`) optionally releases app mouse capture for the terminal's native selection. `F2` again restores app clicks, drag selection and wheel scrolling. `Ctrl+Y` and `PageUp` / `PageDown` also work without the mouse.
|
|
29
32
|
- `Ctrl+X` or `/cancel` stops the current session's agents; send another message to continue.
|
|
30
33
|
- `/compact` summarizes and archives older context of the main agent; an optional argument focuses the digest (e.g. `/compact file changes and pending work`).
|
|
31
|
-
- `Ctrl+C`
|
|
34
|
+
- `/btw <question>` opens a side conversation forked from the current session (full context included) and sends your question there. `/back` or `Ctrl+C` returns to the main conversation; side sessions are marked `[side]` in the status bar and the `/sessions` list.
|
|
35
|
+
- `/fork` copies the current conversation into a new saved session. `/sessions` lists both; the original stays untouched.
|
|
36
|
+
- `/goal <text>` sets a standing goal for the session: it is injected into every agent's prompt until cleared, shows in the status bar, and survives across sessions. `/goal clear` removes it.
|
|
37
|
+
- `Ctrl+C` copies a selection; without a selection it arms a quit confirmation — press again within 2 seconds to exit. Inside a side conversation, `Ctrl+C` returns to the parent session instead. Runtime errors appear in the conversation.
|
|
32
38
|
|
|
33
39
|
For a non-interactive run:
|
|
34
40
|
|
|
@@ -49,7 +55,7 @@ Interactive mode requires a terminal. Non-interactive runs report agent failures
|
|
|
49
55
|
|
|
50
56
|
## Architecture
|
|
51
57
|
|
|
52
|
-
The user talks to `main`, whose primary responsibility is responsive conversation and
|
|
58
|
+
The user talks to `main`, whose primary responsibility is responsive conversation and forward progress. Main handles small, local, and well-scoped work directly. It delegates only genuinely multi-file, ambiguous, or independently parallel work; coordinators select specialists only when that extra coordination is useful. Main remains available while background work runs and is resumed by verified agent results. The scheduler reserves user-facing capacity independently of the background concurrency limit.
|
|
53
59
|
|
|
54
60
|
```text
|
|
55
61
|
user ↔ main → coordinator(s) → explorer / implement / review / custom agents
|
|
@@ -102,6 +108,20 @@ Specs can reduce capabilities but cannot bypass global tool policy, path boundar
|
|
|
102
108
|
- New user input interrupts only main's current generation. Background agents keep running until main explicitly redirects or cancels them.
|
|
103
109
|
- Only main output enters the user-visible conversation.
|
|
104
110
|
- Sessions and instances persist under `~/.coder/runtime/` and recover after restart.
|
|
111
|
+
- Each turn records requests, input/output tokens, provider-reported cached input, and first-token latency when the backend supplies them. The status bar shows the session aggregate.
|
|
112
|
+
- `AGENT_MAX_CHILDREN_PER_TURN` limits fan-out (default `3`); `AGENT_MAX_CONCURRENT_TURNS` and `AGENT_MAX_DEPTH` provide additional scheduler guardrails.
|
|
113
|
+
|
|
114
|
+
### Multi-process safety
|
|
115
|
+
|
|
116
|
+
Multiple `maw` processes can run against the same project without silently destroying each other's work:
|
|
117
|
+
|
|
118
|
+
- **Cross-process write locks** — every file write (`edit_file`, `write_file`) and every session is guarded by an O_EXCL lock file under `~/.coder/runtime/locks/`. Lock records carry the holder pid and process start time, so a lock left by a crashed process is detected and taken over automatically, while a live holder is reported by pid (`AGENT_SESSION_LOCK_TIMEOUT_MS` adjusts the session-lock wait, default 5s).
|
|
119
|
+
- **Optimistic conflict detection** — `edit_file` re-verifies inside the lock that the file still matches what the edit was based on; `write_file` refuses to overwrite a file that changed after it was read in this session. Changes from external editors or other tooling surface as explicit errors instead of silent lost updates.
|
|
120
|
+
- **Single writer per session** — opening a session another process is writing succeeds in read-only mode (the status bar shows `[read-only pid <n>]`); submissions, `/goal`, and `/clear` are refused with guidance. `/fork` continues from that point in a writable copy, and once the other writer exits, the session self-heals back to writable on the next message.
|
|
121
|
+
- **Concurrent instance awareness** — live instances register under `~/.coder/runtime/instances/`; the status bar warns when other maw processes share the workspace.
|
|
122
|
+
- **Managed worktrees** — for genuine parallelism, `/worktree <name>` (or `maw --worktree [name]`) isolates each session in its own checkout so conflicts become ordinary merges. Worktrees are `git worktree lock`ed while a session uses them; cleanup removes clean, unlocked ones and never touches dirty checkouts or branches.
|
|
123
|
+
|
|
124
|
+
Provider prompt caching is used when the provider supports it: runtime system prefixes stay stable and live sibling state travels through mailboxes instead of being re-injected into every request. Cross-request response caching is intentionally avoided because coding answers depend on the current workspace and tool results.
|
|
105
125
|
|
|
106
126
|
### Context compaction
|
|
107
127
|
|
package/agents/coordinator.md
CHANGED
|
@@ -7,7 +7,6 @@ agents:
|
|
|
7
7
|
- '*'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
You coordinate a complex workstream for the parent agent. Understand the goal
|
|
11
|
-
|
|
12
|
-
You do not talk to the user. Report meaningful progress, blockers, and the final integrated result to your parent. Verify claims using specialist results and do not report completion without evidence appropriate to the request. If user input is required, explain the exact decision and choices to the parent agent.
|
|
10
|
+
You coordinate a complex workstream for the parent agent. Understand the goal and choose the smallest workflow that can finish it. Start with one focused explorer or implementer; add another agent only when its work is independent and necessary. As soon as the root cause and edit location are clear, stop exploration and implement. Reuse existing instances instead of spawning duplicates.
|
|
13
11
|
|
|
12
|
+
You do not talk to the user. Report concise progress to your parent: conclusion, evidence, changed paths, and next action. Do not paste large tool outputs. Verify claims with focused checks and do not report completion without evidence. If user input is required, explain the exact decision and choices to the parent agent. Stop or cancel redundant agents once one result resolves the question.
|
package/agents/main.md
CHANGED
|
@@ -10,9 +10,9 @@ agents:
|
|
|
10
10
|
|
|
11
11
|
You are TokenMaw's user-facing main agent. You are the only agent that talks to the user.
|
|
12
12
|
|
|
13
|
-
Your primary responsibility is responsiveness
|
|
13
|
+
Your primary responsibility is responsiveness and forward progress. Answer conversational questions directly. Handle small, local, or well-scoped fixes yourself with the available tools. Delegate only when the task is genuinely multi-file, ambiguous, or benefits from independent parallel work. Never create a coordinator merely to inspect one or two files. When delegating, give one concrete objective, target paths, constraints, and acceptance checks.
|
|
14
14
|
|
|
15
|
-
Delegation is asynchronous: after assigning work,
|
|
15
|
+
Delegation is asynchronous: after assigning work, remain available to the user. Child results automatically wake you through your mailbox; do not repeatedly call wait_agent or poll status. A handoff acknowledgement is not a completion claim. When evidence is sufficient, stop further exploration and implement or ask the existing worker to implement. Never restart an equivalent workstream for a follow-up.
|
|
16
16
|
|
|
17
17
|
Your broad tool access is a fallback capability, not the default workflow. Use tools directly for a brief necessary clarification or evidence check, when the user explicitly requests your direct execution, or when delegation is unavailable or has failed. Keep such work bounded and explain a material fallback.
|
|
18
18
|
|
package/dist/backend.js
CHANGED
|
@@ -89,7 +89,11 @@ async function* ollamaStream(config, systemPrompt, messages, tools, signal) {
|
|
|
89
89
|
yield { content: null, toolCalls: obj.message.tool_calls, done: false };
|
|
90
90
|
}
|
|
91
91
|
if (obj.done) {
|
|
92
|
-
|
|
92
|
+
const stats = obj;
|
|
93
|
+
yield { content: null, done: true, usage: (stats.prompt_eval_count !== undefined || stats.eval_count !== undefined) ? {
|
|
94
|
+
inputTokens: stats.prompt_eval_count,
|
|
95
|
+
outputTokens: stats.eval_count,
|
|
96
|
+
} : undefined };
|
|
93
97
|
return;
|
|
94
98
|
}
|
|
95
99
|
}
|
|
@@ -200,6 +204,7 @@ async function* openaiStream(config, systemPrompt, messages, tools, signal) {
|
|
|
200
204
|
model: config.model,
|
|
201
205
|
stream: true,
|
|
202
206
|
messages: convertToOpenAIMessages(systemPrompt, messages),
|
|
207
|
+
stream_options: { include_usage: true },
|
|
203
208
|
};
|
|
204
209
|
applyOpenAIRequestOptions(body, config);
|
|
205
210
|
const openaiTools = convertToolsToOpenAI(tools);
|
|
@@ -245,6 +250,13 @@ async function* openaiStream(config, systemPrompt, messages, tools, signal) {
|
|
|
245
250
|
continue;
|
|
246
251
|
try {
|
|
247
252
|
const parsed = JSON.parse(trimmed.slice(6));
|
|
253
|
+
if (parsed.usage)
|
|
254
|
+
yield { content: null, done: false, usage: {
|
|
255
|
+
inputTokens: parsed.usage.prompt_tokens,
|
|
256
|
+
outputTokens: parsed.usage.completion_tokens,
|
|
257
|
+
cachedInputTokens: parsed.usage.prompt_tokens_details?.cached_tokens,
|
|
258
|
+
reasoningTokens: parsed.usage.completion_tokens_details?.reasoning_tokens,
|
|
259
|
+
} };
|
|
248
260
|
const choice = parsed.choices?.[0];
|
|
249
261
|
if (!choice)
|
|
250
262
|
continue;
|
|
@@ -495,6 +507,24 @@ async function* anthropicStream(config, systemPrompt, messages, tools, signal) {
|
|
|
495
507
|
yield { content: null, toolCalls: [toolCall], done: false };
|
|
496
508
|
continue;
|
|
497
509
|
}
|
|
510
|
+
if (event === 'message_start' && (parsed.message?.usage?.input_tokens !== undefined)) {
|
|
511
|
+
const usage = parsed.message.usage;
|
|
512
|
+
yield { content: null, done: false, usage: {
|
|
513
|
+
inputTokens: usage.input_tokens,
|
|
514
|
+
cachedInputTokens: usage.cache_read_input_tokens,
|
|
515
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens,
|
|
516
|
+
} };
|
|
517
|
+
}
|
|
518
|
+
if (event === 'message_delta' && parsed.usage) {
|
|
519
|
+
const usage = parsed.usage;
|
|
520
|
+
if (usage.output_tokens !== undefined || usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined) {
|
|
521
|
+
yield { content: null, done: false, usage: {
|
|
522
|
+
outputTokens: usage.output_tokens,
|
|
523
|
+
cachedInputTokens: usage.cache_read_input_tokens,
|
|
524
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens,
|
|
525
|
+
} };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
498
528
|
if (event === 'message_stop') {
|
|
499
529
|
yield { content: null, done: true };
|
|
500
530
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,29 @@ import { resolveModelConfig } from './model-config.js';
|
|
|
6
6
|
import { defaultPolicy } from './policy.js';
|
|
7
7
|
import { AgentRegistry } from './runtime/agent-registry.js';
|
|
8
8
|
import { AgentRuntime } from './runtime/agent-runtime.js';
|
|
9
|
+
import { WorktreeManager } from './runtime/worktree.js';
|
|
10
|
+
import { registerWorkspaceInstance } from './runtime/workspace-instances.js';
|
|
9
11
|
import { runFullscreenTui } from './ui/fullscreen-tui.js';
|
|
12
|
+
import { checkForUpdate, formatUpdateNotice, offerSelfUpdate } from './update-check.js';
|
|
10
13
|
import { CODER_VERSION } from './version.js';
|
|
11
14
|
async function main() {
|
|
12
15
|
const program = new Command();
|
|
13
16
|
program.name('maw').description('Document-driven coding agent runtime').version(CODER_VERSION);
|
|
14
17
|
program.allowExcessArguments(false).showSuggestionAfterError();
|
|
15
18
|
program.option('--model <name>', 'default model name or .agentrc alias');
|
|
19
|
+
program.option('--worktree [name]', 'start inside an isolated managed git worktree (.coder/worktrees/<name>)');
|
|
20
|
+
// Query npm for a newer release while the command runs; afterwards a y/N
|
|
21
|
+
// prompt offers a self-update (China mirror first) in interactive sessions.
|
|
22
|
+
const updateNotice = checkForUpdate().catch(() => null);
|
|
23
|
+
const showUpdateNotice = async () => {
|
|
24
|
+
const result = await updateNotice;
|
|
25
|
+
if (!result?.updateAvailable || !process.stderr.isTTY)
|
|
26
|
+
return;
|
|
27
|
+
process.stderr.write(`\n${formatUpdateNotice(result)}\n`);
|
|
28
|
+
const message = await offerSelfUpdate(result).catch(() => null);
|
|
29
|
+
if (message)
|
|
30
|
+
process.stderr.write(`\n${message}\n`);
|
|
31
|
+
};
|
|
16
32
|
let config = await loadConfig();
|
|
17
33
|
const selectedFromCli = () => program.opts().model;
|
|
18
34
|
setToolPolicy(defaultPolicy(config.policyLevel ?? 'moderate', process.cwd()));
|
|
@@ -23,6 +39,19 @@ async function main() {
|
|
|
23
39
|
defaultModel: selectedFromCli() ?? config.model,
|
|
24
40
|
resolveModel: (alias) => resolveModelConfig(config, alias).config,
|
|
25
41
|
});
|
|
42
|
+
// Announce this instance so concurrent maw processes in the same workspace
|
|
43
|
+
// can surface a warning (and users can see who else is editing).
|
|
44
|
+
const stopInstanceHeartbeat = await registerWorkspaceInstance(process.cwd());
|
|
45
|
+
// /cd moves the runtime to a new workspace root; the tool policy must follow
|
|
46
|
+
// so read/write authorization keeps covering the new tree.
|
|
47
|
+
runtime.subscribe((event) => {
|
|
48
|
+
if (event.type !== 'workspace_changed')
|
|
49
|
+
return;
|
|
50
|
+
setToolPolicy(defaultPolicy(config.policyLevel ?? 'moderate', event.workspaceRoot));
|
|
51
|
+
void registerWorkspaceInstance(event.workspaceRoot).then((stop) => {
|
|
52
|
+
void stopInstanceHeartbeat().then(stop);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
26
55
|
const configManager = {
|
|
27
56
|
getConfig: () => config,
|
|
28
57
|
saveConfig: async (next) => {
|
|
@@ -54,6 +83,7 @@ async function main() {
|
|
|
54
83
|
const response = session.messages.slice(submitted + 1).reverse().find((message) => message.role === 'assistant');
|
|
55
84
|
if (response)
|
|
56
85
|
process.stdout.write(`${response.content}\n`);
|
|
86
|
+
await showUpdateNotice();
|
|
57
87
|
}
|
|
58
88
|
finally {
|
|
59
89
|
await runtime.shutdown();
|
|
@@ -67,12 +97,26 @@ async function main() {
|
|
|
67
97
|
for (const spec of runtime.listAgentSpecs()) {
|
|
68
98
|
process.stdout.write(`${spec.id}\t${spec.scope}\t${spec.model ?? 'inherit'}\t${spec.description}\n`);
|
|
69
99
|
}
|
|
100
|
+
await showUpdateNotice();
|
|
70
101
|
await runtime.shutdown();
|
|
71
102
|
});
|
|
72
103
|
program.action(async () => {
|
|
73
104
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
74
105
|
throw new Error('Interactive mode requires a terminal. Use maw run --prompt "..." for non-interactive execution.');
|
|
75
106
|
await runtime.whenReady();
|
|
107
|
+
const worktreeArg = program.opts().worktree;
|
|
108
|
+
if (worktreeArg !== undefined) {
|
|
109
|
+
const manager = new WorktreeManager(process.cwd());
|
|
110
|
+
if (!await manager.isGitRepository()) {
|
|
111
|
+
console.error('--worktree requires a git repository');
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const name = typeof worktreeArg === 'string' && worktreeArg.trim() ? worktreeArg.trim() : `session-${Date.now()}`;
|
|
116
|
+
const info = await manager.create(name);
|
|
117
|
+
await runtime.changeWorkspace(info.path);
|
|
118
|
+
process.stdout.write(`worktree ready: ${info.path} (${info.branch})\n`);
|
|
119
|
+
}
|
|
76
120
|
const requested = selectedFromCli();
|
|
77
121
|
const selected = resolveModelConfig(config, requested);
|
|
78
122
|
runtime.setDefaultModel(requested ?? config.model);
|
|
@@ -86,10 +130,12 @@ async function main() {
|
|
|
86
130
|
},
|
|
87
131
|
configManager,
|
|
88
132
|
});
|
|
133
|
+
await showUpdateNotice();
|
|
89
134
|
await runtime.shutdown();
|
|
90
135
|
});
|
|
91
136
|
const shutdown = async () => {
|
|
92
137
|
await runtime.shutdown().catch(() => undefined);
|
|
138
|
+
await stopInstanceHeartbeat().catch(() => undefined);
|
|
93
139
|
process.exit(0);
|
|
94
140
|
};
|
|
95
141
|
process.once('SIGTERM', () => { void shutdown(); });
|