aiterm-mcp 0.15.0 → 0.16.0
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.ja.md +18 -17
- package/README.md +29 -29
- package/dist/aiterm-wait-cli.js +12 -3
- package/dist/core.js +105 -287
- package/dist/index.js +61 -62
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -52,14 +52,15 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
52
52
|
|
|
53
53
|
### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
|
|
54
54
|
|
|
55
|
-
同じ primitive が別エージェントの TUI を宿す。4 つの起動ツールが、Claude/Codex/Grok/Composer の対話 TUI を新しい永続端末の中に起動し、`session_id` を返す。既存の人間向けtextに加えて`aiterm.agent-launch-result.v1` structured receiptも返すため、durable callerは表示文字列を解析せずsession handleを取得できる。以後は同じ `pty_read` / `pty_send`
|
|
55
|
+
同じ primitive が別エージェントの TUI を宿す。4 つの起動ツールが、Claude/Codex/Grok/Composer の対話 TUI を新しい永続端末の中に起動し、`session_id` を返す。既存の人間向けtextに加えて`aiterm.agent-launch-result.v1` structured receiptも返すため、durable callerは表示文字列を解析せずsession handleを取得できる。以後は同じ `pty_read` / `pty_send` で継続操作する。**起動は常に managed**(aiterm 所有の Stop hook 付き)で、agent session への `pty_send` は非ブロックの **dispatch** になり `event_cursor` 入り receipt を即返す。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` をホストのバックグラウンドタスクとして実行し、その exit で受ける(親はブロックもポーリングもしない)。durable machine callerは`claude_turn({ action:"issue"|"recover", session_id, operation_id, ... })`を使い、人間向けerror文字列を解析せず`accepted`/`pending`/`completed`/`unknown`を判定できる。recoveryは再送せず、検証済み完了だけがexact `raw_output`を持つ。通常の`pty_send`/`pty_read`は対話callerと人間向けに維持する。`C-c`後もmarkerを保持し、Stopが来なければsessionをcloseする。`claude_agent` と `codex_agent` の初回 `prompt` は ready gate 経由で送信して待たずに返る(Grok/Composer は argv 渡し)。手動でキー操作したい場合は `pty_open` で素の端末を開き vendor CLI を自分で起動する。
|
|
56
56
|
|
|
57
57
|
```text
|
|
58
|
-
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
59
|
-
|
|
58
|
+
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
59
|
+
prompt: "port test/legacy.py to vitest" })
|
|
60
60
|
→ { session_id: "codex1", … } # Codex が永続端末で稼働開始
|
|
61
61
|
pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
|
|
62
|
-
pty_send("codex1", "also fix the imports it broke"
|
|
62
|
+
pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
|
|
63
|
+
$ aiterm-wait --session codex1 --cursor <event_cursor> # exitが完了push。回収は pty_read(agent_transcript:true)
|
|
63
64
|
→ 操舵し、Codex の次の入力境界で返る
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -67,12 +68,12 @@ pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
|
|
|
67
68
|
|
|
68
69
|
| ツール | 起動するもの | 主な引数 |
|
|
69
70
|
| --- | --- | --- |
|
|
70
|
-
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name
|
|
71
|
-
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name
|
|
72
|
-
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name
|
|
73
|
-
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name
|
|
71
|
+
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
72
|
+
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
|
|
73
|
+
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
74
|
+
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
74
75
|
|
|
75
|
-
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claude
|
|
76
|
+
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeは通常settingsを継承しないlaunch専用settingsとStop hookを使い、本文なしeventとowner-only bounded resultを分離する。`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返し、Claude private transcriptを読まない。後着resultは同じsessionからprompt再送なしで回収できる。managed ClaudeではC-c以外の`pty_key`を拒否する(手動介入は`pty_send`の`force:true`)。中断は`C-c`、解除は`pty_close`。
|
|
76
77
|
|
|
77
78
|
エージェント間の隠れたプロトコルは無い。起動したClaude/Codex/Grok/Composerは利用者がattachできるもう1本の永続sessionであり、MCPクライアントが通常のPTY操作で駆動する。
|
|
78
79
|
|
|
@@ -257,7 +258,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
257
258
|
| `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
|
|
258
259
|
| `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
|
|
259
260
|
| `pty_list` | セッション一覧 | (なし) |
|
|
260
|
-
| `claude_turn` | 相関済みmanaged Claude operation
|
|
261
|
+
| `claude_turn` | 相関済みmanaged Claude operationをdispatch(issue)または回収(recover) | `action`, `session_id`, `operation_id`, `text?` |
|
|
261
262
|
| `diagnostics` | 機械可読 JSON による read-only factory readiness | (なし) |
|
|
262
263
|
|
|
263
264
|
`diagnostics` は PTY やエージェントを起動しない。パッケージ版、MCP 呼出 readiness、read-only な PTY 一覧要約、bounded runtime-error-store status、任意 vendor launcher の可用性だけを返す。path・環境値・認証情報・コマンド本文・PTY 出力・raw log は意図的に返さない。通常未設定の任意依存は `not_applicable`、安全に確定できない状態は `unverified` と表す。
|
|
@@ -274,18 +275,18 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
|
|
|
274
275
|
|
|
275
276
|
| ツール | 起動するもの | 主な引数 |
|
|
276
277
|
| --- | --- | --- |
|
|
277
|
-
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name
|
|
278
|
-
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name
|
|
279
|
-
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name
|
|
280
|
-
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name
|
|
278
|
+
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
279
|
+
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
|
|
280
|
+
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
281
|
+
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
281
282
|
|
|
282
|
-
対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。Claude/Codex
|
|
283
|
+
対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。4 launcherすべてが同じ非ブロックdispatch契約を使い、Claude/Codexの初回promptはready gate経由で送信される。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。既存3 vendorのlive smokeはgreen、Claude実モデルsmokeは承認待ちでありfixture成功と混同しない。
|
|
283
284
|
|
|
284
|
-
|
|
285
|
+
エージェントの回答が画面 tailより長ければ、対話callerは`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。Claudeはmanaged Stop hookがowner-only resultへ保存した本文をdigest/byte数で検証して返し、private transcriptを読まない。durable machine callerは`claude_turn`を使う。`issue`は一度だけ送信し、`recover`は決して再送せず、`pending`を破損やidentity不一致と区別する。検証済みの`completed`だけがexact `raw_output`を持ち、`unknown`は未dispatchと帰属不能を区別する。不一致・破損は成功statusへ丸めずtool errorのままにする。IDなしの対話turnも匿名markerで直列化するため、現在Stop待ちの間に古い回答を返さない。CodexはStop hookの`turn_id`で構造化transcriptへjoinし、Grok/Composerは最後の実user行より後ろのassistant行を採る。不在・非agent・抽出不能は明示エラー。
|
|
285
286
|
|
|
286
287
|
### 完了検出(5 層)
|
|
287
288
|
|
|
288
|
-
`pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }`
|
|
289
|
+
`pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }` を読む。agent session は第6の正確な層を使う: vendor Stop hook が完了 event を書き、`pty_send` dispatch が返した `event_cursor` 境界から `aiterm-wait --cursor` が完了を観測する(親はブロックもポーリングもしない)。`pty_send` の送信前 ready 失敗は MCP エラー、launcher の初回 prompt ready 失敗は `initial_prompt=not_sent` を返す。agent session の通常 `pty_read` には `agent_event_seen=true completion_attribution=none` のような補助 metadata が付くことがあるが、古い hook event を `is_complete=True` に昇格しない。完結した hook JSONL 行が壊れていた場合は、`aiterm-wait` receipt の `malformed_events` に数えられる。ターンは完了したが端末 screen/log が flush 窓内で安定しなかった場合は、`agent_done_but_screen_unstable` が付く。
|
|
289
290
|
|
|
290
291
|
### トークン削減
|
|
291
292
|
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ runtime-error store collect only when canonical dotagents config explicitly sets
|
|
|
33
33
|
I/O. It ships via tag-triggered CI with npm provenance (OIDC Trusted Publishing);
|
|
34
34
|
the GitHub Release re-registers the Official MCP Registry entry.
|
|
35
35
|
|
|
36
|
-
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (
|
|
36
|
+
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (managed completion is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
37
37
|
|
|
38
38
|
## Why now
|
|
39
39
|
|
|
@@ -62,27 +62,29 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
62
62
|
|
|
63
63
|
### 2. Launch other coding agents into that terminal — the orchestration flagship
|
|
64
64
|
|
|
65
|
-
The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.)
|
|
65
|
+
The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Every launch is **managed**: aiterm installs its own Stop hook, so turn completion is a first-class event. Sending to an agent session is a non-blocking **dispatch** — the call returns immediately with an `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). Durable machine callers use `claude_turn({ action: "issue" | "recover", session_id, operation_id, ... })`: it returns fixed `accepted` / `pending` / `completed` / `unknown` states without parsing human-facing errors, never resends during recovery, and includes exact `raw_output` only for a verified completion. The same operation ID is carried through the dispatch receipt, active marker, Stop event, and result. The ordinary `pty_send` / `pty_read` surface remains available for interactive callers and humans. `C-c` keeps the marker for a delayed Stop; if no Stop arrives, close the session. An initial `prompt` on `claude_agent`/`codex_agent` is submitted through the same ready gate and the launcher returns without waiting; on Grok/Composer it is passed on the CLI's argv. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
|
|
66
66
|
|
|
67
67
|
```text
|
|
68
|
-
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
69
|
-
|
|
68
|
+
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
69
|
+
prompt: "port test/legacy.py to vitest" })
|
|
70
70
|
→ { session_id: "codex1", … } # Codex now live in a persistent terminal
|
|
71
71
|
pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
|
|
72
|
-
pty_send("codex1", "also fix the imports it broke"
|
|
73
|
-
→
|
|
72
|
+
pty_send("codex1", "also fix the imports it broke")
|
|
73
|
+
→ non-blocking dispatch; receipt carries event_cursor
|
|
74
|
+
$ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; its exit = completion push
|
|
75
|
+
pty_read("codex1", { agent_transcript: true }) → collect the full answer
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
One call per model, so the tool name itself tells you which model you get:
|
|
77
79
|
|
|
78
80
|
| Tool | Launches | Key args |
|
|
79
81
|
| --- | --- | --- |
|
|
80
|
-
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `
|
|
81
|
-
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name
|
|
82
|
-
| `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name
|
|
83
|
-
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name
|
|
82
|
+
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
83
|
+
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?` |
|
|
84
|
+
| `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
|
|
85
|
+
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
|
|
84
86
|
|
|
85
|
-
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless
|
|
87
|
+
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and a `launch_operation_id` formatted as `sha256:<64 lowercase hex>`. Repeating the identical launch returns the same structured session receipt without starting the CLI twice; a different correlation ID or launch argument for that session fails explicitly. Claude uses launch-local managed settings containing only aiterm's Stop hook: normal user/project/local hooks are not inherited, the hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. A late result remains recoverable from the same session without re-sending the prompt. Non-interrupt keys are rejected on the managed Claude route; use dispatch, `pty_key("C-c")`, and `pty_close` (`force:true` on `pty_send` is the manual-intervention escape). For unconstrained manual key-by-key driving, open a plain `pty_open` session and start the vendor CLI yourself. Codex uses a managed `CODEX_HOME`; Grok/Composer isolate their managed homes and pass validated OAuth state through `GROK_AUTH_PATH`. Before the first unbound dispatch, aiterm waits for the vendor TUI's input prompt and fails before sending if it is not ready. Managed completion requires POSIX filesystem semantics (Linux, WSL2, macOS).
|
|
86
88
|
|
|
87
89
|
There is no hidden protocol between agents: a launched Claude, Codex, Grok, or Composer is another user-visible persistent terminal session. The MCP client drives that TUI with ordinary PTY operations, and a human can attach to watch or take over.
|
|
88
90
|
|
|
@@ -264,12 +266,12 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
|
|
|
264
266
|
| Tool | Role | Key args |
|
|
265
267
|
| --- | --- | --- |
|
|
266
268
|
| `pty_open` | Grab one terminal, return a `session_id` | `name?`, `shell="bash"` |
|
|
267
|
-
| `pty_send` | Send text
|
|
269
|
+
| `pty_send` | Send text; on an agent session this is a non-blocking **dispatch** returning an `event_cursor` | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
|
|
268
270
|
| `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
269
271
|
| `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
|
|
270
272
|
| `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
|
|
271
273
|
| `pty_list` | List sessions (agent rows carry `agent=<kind>` metadata) | (none) |
|
|
272
|
-
| `claude_turn` | Issue or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text
|
|
274
|
+
| `claude_turn` | Issue (dispatch-only) or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text?` |
|
|
273
275
|
| `diagnostics` | Read-only factory readiness as machine-readable JSON | (none) |
|
|
274
276
|
|
|
275
277
|
`diagnostics` never starts a PTY or agent. It reports package version, MCP call readiness, a read-only PTY-list summary, bounded runtime-error-store status, and optional vendor-launcher availability. It deliberately excludes paths, environment values, credentials, command text, PTY output, and raw logs; normal unset optional dependencies are `not_applicable`, while an indeterminate probe is `unverified`.
|
|
@@ -286,31 +288,29 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
|
|
|
286
288
|
|
|
287
289
|
| Tool | Launches | Key args |
|
|
288
290
|
| --- | --- | --- |
|
|
289
|
-
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `
|
|
290
|
-
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name
|
|
291
|
-
| `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name
|
|
292
|
-
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name
|
|
291
|
+
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
292
|
+
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?` |
|
|
293
|
+
| `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
|
|
294
|
+
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
|
|
293
295
|
|
|
294
|
-
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created.
|
|
296
|
+
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created. All four share the same non-blocking dispatch contract for follow-up turns; `claude_agent`/`codex_agent` submit an initial `prompt` through the ready gate. Claude uses isolated managed settings and a hook-captured bounded result rather than private transcript access. Codex/Grok/Composer live smokes are green; Claude real-model smoke remains approval-gated and is not claimed from fixtures. Native Windows can launch agents but managed completion is not supported yet.
|
|
295
297
|
|
|
296
|
-
When an agent's answer is longer than the
|
|
298
|
+
When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. Claude reads the bounded owner-only result captured by the managed Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. `unknown` distinguishes `operation_not_found` from a receipt whose result can no longer be attributed. Mismatch and corruption remain tool errors rather than being folded into a successful status. ID-less interactive Claude turns are still serialized by an anonymous marker, so an older answer is not returned while the current Stop is pending. Codex joins its structured transcript on the Stop hook `turn_id`; Grok/Composer take the assistant rows after the last real user row. A missing result/transcript, a non-agent session, or an unextractable message is an explicit error, never a silent empty.
|
|
297
299
|
|
|
298
300
|
### Completion detection (5 layers)
|
|
299
301
|
|
|
300
|
-
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles.
|
|
302
|
+
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles. Agent sessions use the sixth, exact layer instead: the vendor Stop hook writes a completion event, `pty_send` dispatch returns the `event_cursor` boundary, and `aiterm-wait --cursor` observes the completion without the parent blocking or polling. Pre-send readiness failures are MCP errors for `pty_send`, while launch-time initial prompt readiness failures return the session with `initial_prompt=not_sent`. A late Claude completion remains recoverable from the same session with `pty_read({ agent_transcript:true })`, without resending. Normal `pty_read` on an agent session can append auxiliary metadata such as `agent_event_seen=true completion_attribution=none`, but a stale hook event is not promoted to `is_complete=True`. If a complete hook JSONL line is malformed, the `aiterm-wait` receipt counts it in `malformed_events` for diagnosis. If the turn is done but the terminal screen/log does not settle within the flush window, aiterm appends `agent_done_but_screen_unstable`.
|
|
301
303
|
|
|
302
304
|
### Completion push for parent agents (`aiterm-wait`)
|
|
303
305
|
|
|
304
|
-
|
|
306
|
+
As of v0.16.0 a parent agent **never blocks** on aiterm — there is no wait parameter anywhere. The whole flow is dispatch + one universal waiter:
|
|
305
307
|
|
|
306
|
-
1. Launch the child
|
|
307
|
-
2.
|
|
308
|
-
3.
|
|
309
|
-
4.
|
|
308
|
+
1. Launch the child (`claude_agent` / `codex_agent` / ...; every launch is managed). Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call passes the TUI ready gate, submits, and returns immediately with an `event_cursor` in its structured receipt.
|
|
309
|
+
2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]`. It observes the vendor Stop-hook completion event as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt (`done` / `timeout` / `closed`). The `--cursor` boundary makes it start-order independent: no completion can slip past even if the waiter starts late.
|
|
310
|
+
3. Host integration picks the invocation style: a harness that re-invokes its agent when a background task exits (Claude Code) runs the waiter **in the background** and gets woken with zero polling; a host without that mechanism runs it as a foreground shell command. Either way the aiterm MCP call itself never blocks.
|
|
311
|
+
4. Collect the result exactly as before: `pty_read(agent_transcript: true)`, or `claude_turn recover` for durable Claude operations. The waiter carries the signal, never the payload.
|
|
310
312
|
|
|
311
|
-
`aiterm-wait`
|
|
312
|
-
|
|
313
|
-
Codex CLI as the *parent* currently has no equivalent "wake on background completion" hook (its `notify` config is human-facing and MCP notifications are not surfaced to the model — see openai/codex#17543 / #18056), so a Codex parent still uses the blocking wait or manual recovery until upstream support lands.
|
|
313
|
+
`aiterm-wait` takes no locks, never writes session state, and never dispatches — any number can run beside the MCP server and each other, and `pty_close`/concurrent sends are unaffected.
|
|
314
314
|
|
|
315
315
|
### Token reduction
|
|
316
316
|
|
|
@@ -380,7 +380,7 @@ MIT
|
|
|
380
380
|
|
|
381
381
|
## Grok OAuth isolation
|
|
382
382
|
|
|
383
|
-
For
|
|
383
|
+
For managed completion, Grok/Composer keep launch-local `GROK_HOME` and fake `HOME`.
|
|
384
384
|
The child receives `GROK_AUTH_PATH` pointing at the validated normal auth
|
|
385
385
|
canonical file; managed homes never contain auth or lock symlinks/copies.
|
|
386
386
|
aiterm does not create locks or copy credentials back. An inherited
|
package/dist/aiterm-wait-cli.js
CHANGED
|
@@ -7,14 +7,15 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import { AitermError, observeAgentDone } from "./core.js";
|
|
8
8
|
const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
9
9
|
const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
|
|
10
|
-
const USAGE = "usage: aiterm-wait --session <name> [--operation sha256:<64hex>] [--timeout <sec>]";
|
|
10
|
+
const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]";
|
|
11
11
|
export function parseArgs(argv) {
|
|
12
12
|
let session = null;
|
|
13
13
|
let operationId = null;
|
|
14
14
|
let timeout = null;
|
|
15
|
+
let cursor = null;
|
|
15
16
|
for (let i = 0; i < argv.length; i++) {
|
|
16
17
|
const a = argv[i];
|
|
17
|
-
if (a === "--session" || a === "--operation" || a === "--timeout") {
|
|
18
|
+
if (a === "--session" || a === "--operation" || a === "--timeout" || a === "--cursor") {
|
|
18
19
|
const v = argv[i + 1];
|
|
19
20
|
if (v === undefined)
|
|
20
21
|
throw new Error(`${a} に値がありません。${USAGE}`);
|
|
@@ -33,6 +34,13 @@ export function parseArgs(argv) {
|
|
|
33
34
|
throw new Error(`--operation が不正です。${USAGE}`);
|
|
34
35
|
operationId = v;
|
|
35
36
|
}
|
|
37
|
+
else if (a === "--cursor") {
|
|
38
|
+
if (cursor !== null)
|
|
39
|
+
throw new Error(`--cursor が重複しています。${USAGE}`);
|
|
40
|
+
if (!/^\d+$/.test(v))
|
|
41
|
+
throw new Error(`--cursor は0以上の整数byte offsetだけを受理します。${USAGE}`);
|
|
42
|
+
cursor = Number(v);
|
|
43
|
+
}
|
|
36
44
|
else {
|
|
37
45
|
if (timeout !== null)
|
|
38
46
|
throw new Error(`--timeout が重複しています。${USAGE}`);
|
|
@@ -49,7 +57,7 @@ export function parseArgs(argv) {
|
|
|
49
57
|
}
|
|
50
58
|
if (session === null)
|
|
51
59
|
throw new Error(`--session は必須です。${USAGE}`);
|
|
52
|
-
return { session, operationId, timeout: timeout ?? 600 };
|
|
60
|
+
return { session, operationId, timeout: timeout ?? 600, cursor };
|
|
53
61
|
}
|
|
54
62
|
function emit(value) {
|
|
55
63
|
process.stdout.write(JSON.stringify(value) + "\n");
|
|
@@ -59,6 +67,7 @@ export async function main(argv) {
|
|
|
59
67
|
const result = await observeAgentDone(cmd.session, {
|
|
60
68
|
operation_id: cmd.operationId,
|
|
61
69
|
timeout: cmd.timeout,
|
|
70
|
+
cursor: cmd.cursor,
|
|
62
71
|
});
|
|
63
72
|
emit(result);
|
|
64
73
|
}
|
package/dist/core.js
CHANGED
|
@@ -209,12 +209,27 @@ function resolveTmux(observe = true) {
|
|
|
209
209
|
}
|
|
210
210
|
ptyDependencyError(tmuxMissingMessage(), observe);
|
|
211
211
|
}
|
|
212
|
+
// tmux は locale が C/POSIX/未設定だと UTF-8 を扱えない: server は send-keys/paste のマルチバイト
|
|
213
|
+
// 入力を破壊し(文字消失・周辺バイトの並べ替えを実測)、client は format 出力のタブ等を "_" へ
|
|
214
|
+
// サニタイズする。GUI 起動の MCP client は LANG を持たないことが多く、その環境で立った tmux server は
|
|
215
|
+
// 以後すべての入力を壊すため、UTF-8 locale が確定しない場合だけ LC_CTYPE を明示注入する。
|
|
216
|
+
// 利用者が C/POSIX 以外を明示設定している場合はその選択を尊重して触らない。
|
|
217
|
+
export function tmuxSpawnEnv() {
|
|
218
|
+
const effective = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
|
|
219
|
+
// 素の "C"/"POSIX" だけを壊れた既定とみなす。"C.UTF-8" を含む charset 付きの明示設定は尊重する。
|
|
220
|
+
if (effective && !/^(C|POSIX)$/i.test(effective))
|
|
221
|
+
return undefined;
|
|
222
|
+
const env = { ...process.env, LC_CTYPE: process.platform === "darwin" ? "UTF-8" : "C.UTF-8" };
|
|
223
|
+
// LC_ALL は LC_CTYPE より優先されるため、C/POSIX の LC_ALL が残ると注入が無効になる
|
|
224
|
+
delete env.LC_ALL;
|
|
225
|
+
return env;
|
|
226
|
+
}
|
|
212
227
|
function tmuxCommandWithInput(observe, input, ...args) {
|
|
213
228
|
// maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
|
|
214
229
|
// 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
|
|
215
230
|
// Windows は同じ tmux を WSL 経由(-e でログインシェル非経由=$ 展開やクオート崩れを防ぐ)で叩く。
|
|
216
231
|
let r;
|
|
217
|
-
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input };
|
|
232
|
+
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
|
|
218
233
|
if (isWin) {
|
|
219
234
|
ensureWinBridge(observe);
|
|
220
235
|
r = spawnSync("wsl.exe", ["-e", "tmux", "-S", SOCK, ...args], spawnOpts);
|
|
@@ -1200,9 +1215,6 @@ export function readOnlyPtyListDiagnostic() {
|
|
|
1200
1215
|
}
|
|
1201
1216
|
function closeSessionInternal(name, observeDependency = true) {
|
|
1202
1217
|
assertSessionName(name);
|
|
1203
|
-
if (agentWaitLocks.has(name)) {
|
|
1204
|
-
throw new AitermError(`agent session '${name}' は agent_done 待機中のため close できません`, 2);
|
|
1205
|
-
}
|
|
1206
1218
|
{
|
|
1207
1219
|
// 別プロセスの待機は in-memory Set に映らない。生きた file lock があれば close で state を消さない
|
|
1208
1220
|
const foreign = liveWaitLocks(name);
|
|
@@ -1251,9 +1263,6 @@ export function closeSessionResult(name) {
|
|
|
1251
1263
|
};
|
|
1252
1264
|
}
|
|
1253
1265
|
export function killAll() {
|
|
1254
|
-
if (agentWaitLocks.size > 0) {
|
|
1255
|
-
throw new AitermError(`agent_done 待機中の session があるため killAll できません: ${Array.from(agentWaitLocks).join(",")}`, 2);
|
|
1256
|
-
}
|
|
1257
1266
|
{
|
|
1258
1267
|
// 別プロセスの待機(file lock が生きているもの)も巻き添えにしない
|
|
1259
1268
|
const foreign = liveWaitLocks(null);
|
|
@@ -1317,16 +1326,8 @@ export function killAll() {
|
|
|
1317
1326
|
return "killed all sessions on this socket";
|
|
1318
1327
|
}
|
|
1319
1328
|
const DEFAULT_AGENT_DONE_TIMEOUT = 600;
|
|
1320
|
-
const agentWaitLocks = new Set();
|
|
1321
1329
|
const agentMetadataNegativeCache = new Map();
|
|
1322
1330
|
let agentTuiReadyStableSamplesTestOverride = null;
|
|
1323
|
-
function normalizeAgentLauncherWait(v) {
|
|
1324
|
-
if (v == null || v === "none")
|
|
1325
|
-
return "none";
|
|
1326
|
-
if (v === "agent_done")
|
|
1327
|
-
return "agent_done";
|
|
1328
|
-
throw new AitermError('wait は "none" または "agent_done" を指定してください', 2);
|
|
1329
|
-
}
|
|
1330
1331
|
function codexHookScriptPath() {
|
|
1331
1332
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "codex-stop-hook.js");
|
|
1332
1333
|
}
|
|
@@ -2016,61 +2017,12 @@ function liveWaitLocks(name) {
|
|
|
2016
2017
|
const session = f.slice(0, f.indexOf("."));
|
|
2017
2018
|
if (name != null && session !== name)
|
|
2018
2019
|
continue;
|
|
2019
|
-
if (agentWaitLocks.has(session))
|
|
2020
|
-
continue; // in-process 待機は呼び出し側の既存ガードが担当
|
|
2021
2020
|
const probe = probeWaitLock(path.join(dir, f));
|
|
2022
2021
|
if (probe.live)
|
|
2023
2022
|
out.push({ session, pid: probe.pid, at: probe.at });
|
|
2024
2023
|
}
|
|
2025
2024
|
return out;
|
|
2026
2025
|
}
|
|
2027
|
-
function waitLockBusyError(session, probe) {
|
|
2028
|
-
const detail = probe.pid != null ? `(pid ${probe.pid}${probe.at ? ` / ${probe.at} 開始` : ""})` : "";
|
|
2029
|
-
return new AitermError(`agent session '${session}' は別プロセスの agent_done 待機中です${detail}`, 2);
|
|
2030
|
-
}
|
|
2031
|
-
function acquireAgentWaitFileLock(meta) {
|
|
2032
|
-
const p = agentWaitLockPath(meta.aiterm_session, meta.launch_id);
|
|
2033
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
2034
|
-
let fd = null;
|
|
2035
|
-
for (let attempt = 0; attempt < 2 && fd == null; attempt++) {
|
|
2036
|
-
try {
|
|
2037
|
-
fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
2038
|
-
}
|
|
2039
|
-
catch (e) {
|
|
2040
|
-
if (e.code !== "EEXIST")
|
|
2041
|
-
throw e;
|
|
2042
|
-
const probe = probeWaitLock(p);
|
|
2043
|
-
// 生きた待機、または回収後の再取得でも EEXIST(=直後に別プロセスが取得した race)は拒否
|
|
2044
|
-
if (probe.live || attempt > 0)
|
|
2045
|
-
throw waitLockBusyError(meta.aiterm_session, probe);
|
|
2046
|
-
unlinkStaleWaitLock(p);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
if (fd == null)
|
|
2050
|
-
throw new AitermError(`agent session '${meta.aiterm_session}' は別プロセスの agent_done 待機中です`, 2);
|
|
2051
|
-
try {
|
|
2052
|
-
fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }) + "\n", undefined, "utf8");
|
|
2053
|
-
}
|
|
2054
|
-
finally {
|
|
2055
|
-
fs.closeSync(fd);
|
|
2056
|
-
}
|
|
2057
|
-
try {
|
|
2058
|
-
fs.chmodSync(p, 0o600);
|
|
2059
|
-
}
|
|
2060
|
-
catch {
|
|
2061
|
-
/* noop */
|
|
2062
|
-
}
|
|
2063
|
-
return () => {
|
|
2064
|
-
try {
|
|
2065
|
-
const st = fs.lstatSync(p);
|
|
2066
|
-
if (st.isFile() && !st.isSymbolicLink() && st.uid === currentUid())
|
|
2067
|
-
fs.unlinkSync(p);
|
|
2068
|
-
}
|
|
2069
|
-
catch {
|
|
2070
|
-
/* noop */
|
|
2071
|
-
}
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
2026
|
function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest) {
|
|
2075
2027
|
const launchId = randomBytes(16).toString("hex");
|
|
2076
2028
|
const eventFile = agentEventPath(name, launchId);
|
|
@@ -2696,42 +2648,6 @@ function assertInitialPromptNotPendingForSend(name, force) {
|
|
|
2696
2648
|
throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。通常 pty_send は混入防止のため送信しません。` +
|
|
2697
2649
|
`完了後に pty_send(wait:"agent_done") するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
|
|
2698
2650
|
}
|
|
2699
|
-
async function waitAgentDoneEvent(meta, startOffset, timeout, expectedOperationId = null) {
|
|
2700
|
-
const deadline = performance.now() + timeout * 1000;
|
|
2701
|
-
let cursor = startOffset;
|
|
2702
|
-
let carry = "";
|
|
2703
|
-
let malformedEvents = 0;
|
|
2704
|
-
for (;;) {
|
|
2705
|
-
const size = safeStatSize(meta.event_file);
|
|
2706
|
-
if (size < cursor) {
|
|
2707
|
-
cursor = 0;
|
|
2708
|
-
carry = "";
|
|
2709
|
-
}
|
|
2710
|
-
if (size > cursor) {
|
|
2711
|
-
if (size - cursor > AGENT_EVENT_MAX_BYTES) {
|
|
2712
|
-
throw new AitermError("agent event file の増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
|
|
2713
|
-
}
|
|
2714
|
-
carry += readFileRange(meta.event_file, cursor, size).toString("utf8");
|
|
2715
|
-
cursor = size;
|
|
2716
|
-
const parts = carry.split("\n");
|
|
2717
|
-
carry = parts.pop() ?? "";
|
|
2718
|
-
const scanned = scanAgentDoneLines(parts, meta, expectedOperationId);
|
|
2719
|
-
malformedEvents += scanned.malformedEvents;
|
|
2720
|
-
if (scanned.ambiguousVendorSession) {
|
|
2721
|
-
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
2722
|
-
}
|
|
2723
|
-
if (scanned.event) {
|
|
2724
|
-
bindAgentVendorSession(meta, scanned.event);
|
|
2725
|
-
if (meta.vendor_session_id)
|
|
2726
|
-
writeAgentMetadata(meta);
|
|
2727
|
-
return { event: scanned.event, malformedEvents };
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
if (performance.now() >= deadline)
|
|
2731
|
-
return { event: null, malformedEvents };
|
|
2732
|
-
await sleep(AGENT_DONE_POLL_MS);
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
2651
|
// 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
|
|
2736
2652
|
// event fileのtail規律(未終端行保持・増分上限)はwaitAgentDoneEventと同一だが、
|
|
2737
2653
|
// vendor_session_idのbind永続化を行わない点だけ意図的に異なる(waiterは観測者であって所有者でない)。
|
|
@@ -2741,11 +2657,14 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2741
2657
|
if (operationId && meta.kind !== "claude") {
|
|
2742
2658
|
throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
|
|
2743
2659
|
}
|
|
2660
|
+
if (o.cursor != null && (!Number.isInteger(o.cursor) || o.cursor < 0)) {
|
|
2661
|
+
throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
|
|
2662
|
+
}
|
|
2744
2663
|
const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
|
|
2745
2664
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
2746
|
-
// operation
|
|
2747
|
-
// (waiter
|
|
2748
|
-
const startOffset = operationId ? 0 : safeStatSize(meta.event_file);
|
|
2665
|
+
// 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
|
|
2666
|
+
// (operation_idの一意性で先頭から全走査できる)→ waiter起動時EOF(waiter先行起動が前提)。
|
|
2667
|
+
const startOffset = o.cursor ?? (operationId ? 0 : safeStatSize(meta.event_file));
|
|
2749
2668
|
const deadline = performance.now() + timeout * 1000;
|
|
2750
2669
|
let cursor = startOffset;
|
|
2751
2670
|
let carry = "";
|
|
@@ -2791,24 +2710,6 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2791
2710
|
await sleep(AGENT_DONE_POLL_MS);
|
|
2792
2711
|
}
|
|
2793
2712
|
}
|
|
2794
|
-
function agentDoneSuffix(wait, vendor, operationId = null) {
|
|
2795
|
-
const ev = wait.event;
|
|
2796
|
-
if (!ev) {
|
|
2797
|
-
const malformed = wait.malformedEvents ? ` malformed_events=${wait.malformedEvents}` : "";
|
|
2798
|
-
const operation = operationId ? ` operation_id=${operationId}` : "";
|
|
2799
|
-
return ` [is_complete=False via agent_timeout vendor=${vendor}${operation}${malformed}]`;
|
|
2800
|
-
}
|
|
2801
|
-
const bits = [
|
|
2802
|
-
"is_complete=True",
|
|
2803
|
-
"via agent_done",
|
|
2804
|
-
`vendor=${ev.vendor}`,
|
|
2805
|
-
ev.turn_id ? `turn_id=${ev.turn_id}` : null,
|
|
2806
|
-
ev.vendor_session_id ? `vendor_session_id=${ev.vendor_session_id}` : null,
|
|
2807
|
-
ev.operation_id ? `operation_id=${ev.operation_id}` : null,
|
|
2808
|
-
`done_status=${ev.done_status}`,
|
|
2809
|
-
].filter(Boolean);
|
|
2810
|
-
return ` [${bits.join(" ")}]`;
|
|
2811
|
-
}
|
|
2812
2713
|
function isAgentTuiReady(kind, screen) {
|
|
2813
2714
|
if (kind === "claude") {
|
|
2814
2715
|
return screen.includes("Claude Code") && /(^|\n)\s*❯/.test(screen);
|
|
@@ -2875,12 +2776,6 @@ async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
|
|
|
2875
2776
|
}
|
|
2876
2777
|
return { unstable: true, samples };
|
|
2877
2778
|
}
|
|
2878
|
-
async function settleAgentDoneScreen(name, lines) {
|
|
2879
|
-
return settleAgentDoneScreenImpl(() => ({
|
|
2880
|
-
screen: captureScreen(name, lines),
|
|
2881
|
-
logSize: safeStatSize(logpath(name)),
|
|
2882
|
-
}), sleep);
|
|
2883
|
-
}
|
|
2884
2779
|
export async function __testSettleAgentDoneScreen(samples, opts = {}) {
|
|
2885
2780
|
if (samples.length === 0)
|
|
2886
2781
|
throw new AitermError("screen settle test samples が空です", 2);
|
|
@@ -2924,135 +2819,90 @@ async function sendAgentPromptText(name, text) {
|
|
|
2924
2819
|
}
|
|
2925
2820
|
export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
2926
2821
|
assertSessionName(name);
|
|
2927
|
-
const waitMode = normalizeAgentLauncherWait(o.wait);
|
|
2928
2822
|
const meta = loadAgentMetadata(name);
|
|
2929
|
-
if (agentWaitLocks.has(name))
|
|
2930
|
-
throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
|
|
2931
2823
|
if (meta.initial_prompt === "done") {
|
|
2932
2824
|
throw new AitermError(`agent session '${name}' の起動時 prompt は既に完了しています`, 2);
|
|
2933
2825
|
}
|
|
2934
2826
|
if (meta.initial_prompt === "pending" || meta.initial_prompt === "sent") {
|
|
2935
2827
|
throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
|
|
2936
2828
|
}
|
|
2937
|
-
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2829
|
+
setInitialPromptState(meta, "not_sent");
|
|
2830
|
+
const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
2831
|
+
if (!ready.ready) {
|
|
2832
|
+
return (`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
|
|
2833
|
+
`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
|
|
2834
|
+
}
|
|
2835
|
+
const startOffset = safeStatSize(meta.event_file);
|
|
2941
2836
|
try {
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
return (`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
|
|
2946
|
-
`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
|
|
2947
|
-
}
|
|
2948
|
-
const startOffset = safeStatSize(meta.event_file);
|
|
2949
|
-
try {
|
|
2950
|
-
if (meta.kind === "claude") {
|
|
2951
|
-
prepareSendText(text, { raw: false, force: true });
|
|
2952
|
-
reserveAnonymousClaudeTurn(meta);
|
|
2953
|
-
}
|
|
2954
|
-
await sendAgentPromptText(name, text);
|
|
2955
|
-
setInitialPromptState(meta, "pending");
|
|
2956
|
-
}
|
|
2957
|
-
catch (e) {
|
|
2958
|
-
setInitialPromptState(meta, "failed");
|
|
2959
|
-
throw e;
|
|
2960
|
-
}
|
|
2961
|
-
if (waitMode === "none") {
|
|
2962
|
-
return (`initial_prompt=pending vendor=${meta.kind}\n` +
|
|
2963
|
-
`起動時 prompt を送信しました。完了後の follow-up は pty_send(wait:"agent_done") を使ってください。`);
|
|
2964
|
-
}
|
|
2965
|
-
const wait = await waitAgentDoneEvent(meta, startOffset, timeout);
|
|
2966
|
-
if (wait.event) {
|
|
2967
|
-
setInitialPromptState(meta, "done");
|
|
2837
|
+
if (meta.kind === "claude") {
|
|
2838
|
+
prepareSendText(text, { raw: false, force: true });
|
|
2839
|
+
reserveAnonymousClaudeTurn(meta);
|
|
2968
2840
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
}
|
|
2972
|
-
const settled = wait.event
|
|
2973
|
-
? await settleAgentDoneScreen(name, o.lines ?? 0)
|
|
2974
|
-
: { unstable: false, samples: 0 };
|
|
2975
|
-
const out = await readOutput(name, {
|
|
2976
|
-
screen,
|
|
2977
|
-
lines: o.lines ?? null,
|
|
2978
|
-
timeout: 0,
|
|
2979
|
-
});
|
|
2980
|
-
writeOffset(name, safeStatSize(logpath(name)));
|
|
2981
|
-
return out + agentDoneSuffix(wait, meta.kind) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
|
|
2841
|
+
await sendAgentPromptText(name, text);
|
|
2842
|
+
setInitialPromptState(meta, "pending");
|
|
2982
2843
|
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2844
|
+
catch (e) {
|
|
2845
|
+
setInitialPromptState(meta, "failed");
|
|
2846
|
+
throw e;
|
|
2986
2847
|
}
|
|
2848
|
+
return (`initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
|
|
2849
|
+
`起動時 prompt を送信した。完了通知は aiterm-wait --session ${name} --cursor ${startOffset}(ホストのバックグラウンドタスクとして実行し、exit を完了通知にする)、` +
|
|
2850
|
+
`回収は pty_read(agent_transcript:true) を使う。`);
|
|
2851
|
+
}
|
|
2852
|
+
export function isAgentSession(name) {
|
|
2853
|
+
assertSessionName(name);
|
|
2854
|
+
return tryLoadAgentMetadata(name) !== null;
|
|
2987
2855
|
}
|
|
2988
|
-
|
|
2856
|
+
// v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
|
|
2857
|
+
// dispatch として即返り、event_cursor(送信直前の event file 境界)を receipt で返す。
|
|
2858
|
+
// 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
|
|
2859
|
+
export async function dispatchAgentTurn(name, text, o = {}) {
|
|
2989
2860
|
assertSessionName(name);
|
|
2990
|
-
if (o.enter === false)
|
|
2991
|
-
throw new AitermError('wait:"agent_done" は enter:false と併用できません', 2);
|
|
2992
|
-
if (o.mark)
|
|
2993
|
-
throw new AitermError('wait:"agent_done" と mark:true は併用できません', 2);
|
|
2994
|
-
if (o.rtk)
|
|
2995
|
-
throw new AitermError('wait:"agent_done" と rtk:true は併用できません', 2);
|
|
2996
2861
|
const meta = loadAgentMetadata(name);
|
|
2997
2862
|
const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
|
|
2998
2863
|
if (operationId && meta.kind !== "claude") {
|
|
2999
2864
|
throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
|
|
3000
2865
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
try {
|
|
3008
|
-
bindCompletedInitialPrompt(meta);
|
|
3009
|
-
if (!meta.vendor_session_id) {
|
|
3010
|
-
const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3011
|
-
if (!ready.ready) {
|
|
3012
|
-
throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
|
|
3013
|
-
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send(wait:\"agent_done\") してください。", 2);
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
const startOffset = safeStatSize(meta.event_file);
|
|
3017
|
-
if (meta.kind === "claude") {
|
|
3018
|
-
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
3019
|
-
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
3020
|
-
prepareSendText(text, { raw: o.raw, force: o.force });
|
|
3021
|
-
if (operationId) {
|
|
3022
|
-
reserveClaudeOperation(meta, operationId);
|
|
3023
|
-
}
|
|
3024
|
-
else
|
|
3025
|
-
reserveAnonymousClaudeTurn(meta);
|
|
2866
|
+
bindCompletedInitialPrompt(meta);
|
|
2867
|
+
if (!meta.vendor_session_id) {
|
|
2868
|
+
const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
2869
|
+
if (!ready.ready) {
|
|
2870
|
+
throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
|
|
2871
|
+
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。", 2);
|
|
3026
2872
|
}
|
|
3027
|
-
send(name, text, {
|
|
3028
|
-
enter: false,
|
|
3029
|
-
force: o.force,
|
|
3030
|
-
raw: o.raw,
|
|
3031
|
-
mark: false,
|
|
3032
|
-
rtk: false,
|
|
3033
|
-
preserveAgentOperation: meta.kind === "claude",
|
|
3034
|
-
});
|
|
3035
|
-
// Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
|
|
3036
|
-
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3037
|
-
sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
|
|
3038
|
-
const wait = await waitAgentDoneEvent(meta, startOffset, timeout, operationId);
|
|
3039
|
-
const settled = wait.event
|
|
3040
|
-
? await settleAgentDoneScreen(name, o.lines ?? 0)
|
|
3041
|
-
: { unstable: false, samples: 0 };
|
|
3042
|
-
const out = await readOutput(name, {
|
|
3043
|
-
screen,
|
|
3044
|
-
lines: o.lines ?? null,
|
|
3045
|
-
timeout: 0,
|
|
3046
|
-
});
|
|
3047
|
-
writeOffset(name, safeStatSize(logpath(name)));
|
|
3048
|
-
return out + agentDoneSuffix(wait, meta.kind, operationId) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
|
|
3049
2873
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
2874
|
+
const startOffset = safeStatSize(meta.event_file);
|
|
2875
|
+
if (meta.kind === "claude") {
|
|
2876
|
+
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
2877
|
+
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
2878
|
+
prepareSendText(text, { raw: o.raw, force: o.force });
|
|
2879
|
+
if (operationId) {
|
|
2880
|
+
reserveClaudeOperation(meta, operationId);
|
|
2881
|
+
}
|
|
2882
|
+
else
|
|
2883
|
+
reserveAnonymousClaudeTurn(meta);
|
|
3053
2884
|
}
|
|
2885
|
+
send(name, text, {
|
|
2886
|
+
enter: false,
|
|
2887
|
+
force: o.force,
|
|
2888
|
+
raw: o.raw,
|
|
2889
|
+
mark: false,
|
|
2890
|
+
rtk: false,
|
|
2891
|
+
preserveAgentOperation: meta.kind === "claude",
|
|
2892
|
+
});
|
|
2893
|
+
// Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
|
|
2894
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
2895
|
+
sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
|
|
2896
|
+
return {
|
|
2897
|
+
schema: "aiterm.agent-dispatch.v1",
|
|
2898
|
+
session_id: meta.aiterm_session,
|
|
2899
|
+
launch_id: meta.launch_id,
|
|
2900
|
+
vendor: meta.kind,
|
|
2901
|
+
event_cursor: startOffset,
|
|
2902
|
+
operation_id: operationId,
|
|
2903
|
+
};
|
|
3054
2904
|
}
|
|
3055
|
-
export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text,
|
|
2905
|
+
export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
|
|
3056
2906
|
assertSessionName(name);
|
|
3057
2907
|
if (action !== "issue" && action !== "recover") {
|
|
3058
2908
|
throw new AitermError('action は "issue" または "recover" を指定してください', 2);
|
|
@@ -3065,22 +2915,12 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
|
|
|
3065
2915
|
if (typeof text !== "string" || text.length === 0) {
|
|
3066
2916
|
throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
|
|
3067
2917
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
throw new AitermError("claude_turn timeoutは0〜3600秒で指定してください", 2);
|
|
3071
|
-
}
|
|
3072
|
-
await sendAndWaitAgentDone(name, text, {
|
|
3073
|
-
operation_id: operationId,
|
|
3074
|
-
timeout: waitTimeout,
|
|
3075
|
-
screen: false,
|
|
3076
|
-
lines: 0,
|
|
3077
|
-
});
|
|
2918
|
+
// v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
|
|
2919
|
+
await dispatchAgentTurn(name, text, { operation_id: operationId });
|
|
3078
2920
|
}
|
|
3079
2921
|
else {
|
|
3080
2922
|
if (text != null)
|
|
3081
2923
|
throw new AitermError("claude_turn recoverにtextは指定できません", 2);
|
|
3082
|
-
if (timeout != null)
|
|
3083
|
-
throw new AitermError("claude_turn recoverにtimeoutは指定できません", 2);
|
|
3084
2924
|
}
|
|
3085
2925
|
const inspected = inspectClaudeOperation(meta, operationId, action);
|
|
3086
2926
|
if (action === "issue" && inspected.status === "pending") {
|
|
@@ -3482,37 +3322,23 @@ export function openAgent(kind, opts = {}) {
|
|
|
3482
3322
|
`起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
|
|
3483
3323
|
];
|
|
3484
3324
|
}
|
|
3485
|
-
async function sendInitialPromptWithoutAgentDone(name, kind, text, opts = {}) {
|
|
3486
|
-
const ready = await waitAgentTuiReadyByKind(name, kind, opts.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3487
|
-
if (!ready.ready) {
|
|
3488
|
-
return (`initial_prompt=not_sent vendor=${kind} ready=false samples=${ready.samples}\n` +
|
|
3489
|
-
`agent session '${name}' の ${agentLabel(kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
|
|
3490
|
-
}
|
|
3491
|
-
await sendAgentPromptText(name, text);
|
|
3492
|
-
return `initial_prompt=sent vendor=${kind}\n起動時 prompt を送信しました。完了待ちは通常の pty_read で行ってください。`;
|
|
3493
|
-
}
|
|
3494
3325
|
export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
3495
|
-
const waitMode = normalizeAgentLauncherWait(opts.wait);
|
|
3496
3326
|
const prompt = opts.prompt ?? null;
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
throw new AitermError(`${agentLabel(kind)} の起動時 prompt wait は未対応です。` +
|
|
3513
|
-
`agent_done:true で prompt なし起動後、TUI のログイン/ready を確認してから pty_send(wait:"agent_done") を使ってください。`, 2);
|
|
3514
|
-
}
|
|
3515
|
-
return openAgent(kind, opts);
|
|
3327
|
+
if (opts.launch_operation_id != null && prompt !== null) {
|
|
3328
|
+
throw new AitermError("launch_operation_idはpromptなしのmanaged Claude launchだけで指定できます", 2);
|
|
3329
|
+
}
|
|
3330
|
+
// v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
|
|
3331
|
+
// pty_open で素の PTY を開き、vendor CLI を自分で send する。
|
|
3332
|
+
if (!prompt || (kind !== "codex" && kind !== "claude")) {
|
|
3333
|
+
return openAgent(kind, {
|
|
3334
|
+
session_name: opts.session_name ?? null,
|
|
3335
|
+
model: opts.model ?? null,
|
|
3336
|
+
reasoning_effort: opts.reasoning_effort ?? null,
|
|
3337
|
+
cwd: opts.cwd ?? null,
|
|
3338
|
+
prompt,
|
|
3339
|
+
agent_done: true,
|
|
3340
|
+
launch_operation_id: opts.launch_operation_id ?? null,
|
|
3341
|
+
});
|
|
3516
3342
|
}
|
|
3517
3343
|
const [sid, hint] = openAgent(kind, {
|
|
3518
3344
|
session_name: opts.session_name ?? null,
|
|
@@ -3520,21 +3346,13 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
3520
3346
|
reasoning_effort: opts.reasoning_effort ?? null,
|
|
3521
3347
|
cwd: opts.cwd ?? null,
|
|
3522
3348
|
prompt: null,
|
|
3523
|
-
agent_done:
|
|
3349
|
+
agent_done: true,
|
|
3524
3350
|
launch_operation_id: opts.launch_operation_id ?? null,
|
|
3525
3351
|
});
|
|
3526
3352
|
try {
|
|
3527
|
-
const initial =
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
timeout: opts.timeout ?? undefined,
|
|
3531
|
-
ready_timeout: opts.ready_timeout ?? undefined,
|
|
3532
|
-
screen: opts.screen ?? undefined,
|
|
3533
|
-
lines: opts.lines ?? null,
|
|
3534
|
-
})
|
|
3535
|
-
: await sendInitialPromptWithoutAgentDone(sid, kind, prompt, {
|
|
3536
|
-
ready_timeout: opts.ready_timeout ?? undefined,
|
|
3537
|
-
});
|
|
3353
|
+
const initial = await sendInitialAgentPrompt(sid, prompt, {
|
|
3354
|
+
ready_timeout: opts.ready_timeout ?? undefined,
|
|
3355
|
+
});
|
|
3538
3356
|
return [sid, `${hint}\n${initial}`];
|
|
3539
3357
|
}
|
|
3540
3358
|
catch (e) {
|
package/dist/index.js
CHANGED
|
@@ -86,27 +86,18 @@ server.registerTool("pty_open", {
|
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
server.registerTool("pty_send", {
|
|
89
|
-
description: "
|
|
90
|
-
"
|
|
91
|
-
"
|
|
89
|
+
description: "セッションへテキストを送る。通常PTYへは送信のみ(出力は pty_read で取得)。" +
|
|
90
|
+
"agent session(launcher起動)への send は自動で dispatch になる: TUI の ready gate と submit 分離を通して即返り、" +
|
|
91
|
+
"receipt の event_cursor を返す=親はブロックしない。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を" +
|
|
92
|
+
"ホストのバックグラウンドタスクとして実行し、その exit で受ける(ポーリング不要)。" +
|
|
93
|
+
"結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
|
|
94
|
+
"force:true は agent session への手動介入用の素送信。",
|
|
92
95
|
inputSchema: {
|
|
93
96
|
session_id: z.string(),
|
|
94
97
|
text: z
|
|
95
98
|
.string()
|
|
96
|
-
.describe("
|
|
97
|
-
enter: z.boolean().default(true).describe("末尾で Enter
|
|
98
|
-
wait: z
|
|
99
|
-
.enum(["none", "agent_done"])
|
|
100
|
-
.default("none")
|
|
101
|
-
.describe("none=従来通り送信のみ(managed Claudeでは拒否)。agent_done=agent Stop hook まで待って最終画面を返す"),
|
|
102
|
-
timeout: z.number().default(600).describe("wait:'agent_done' の最大待ち秒数"),
|
|
103
|
-
screen: z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする"),
|
|
104
|
-
lines: z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行"),
|
|
105
|
-
operation_id: z
|
|
106
|
-
.string()
|
|
107
|
-
.regex(/^sha256:[0-9a-f]{64}$/)
|
|
108
|
-
.nullish()
|
|
109
|
-
.describe("Claudeのdurable caller operation ID。wait:'agent_done'時だけ指定し、timeout後の同一結果回収へ使う"),
|
|
99
|
+
.describe("送る文字列(コマンド/prompt)。UTF-8で最大64KiB"),
|
|
100
|
+
enter: z.boolean().default(true).describe("末尾で Enter を送る(agent dispatch では常に submit)"),
|
|
110
101
|
mark: z
|
|
111
102
|
.boolean()
|
|
112
103
|
.default(false)
|
|
@@ -116,28 +107,58 @@ server.registerTool("pty_send", {
|
|
|
116
107
|
force: z
|
|
117
108
|
.boolean()
|
|
118
109
|
.default(false)
|
|
119
|
-
.describe("破壊的コマンドゲートを越える。agent
|
|
110
|
+
.describe("破壊的コマンドゲートを越える。agent session では dispatch せず素送信する(手動介入用)"),
|
|
120
111
|
rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
|
|
121
112
|
raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
|
|
122
113
|
},
|
|
123
|
-
|
|
114
|
+
outputSchema: {
|
|
115
|
+
schema: z.literal("aiterm.pty-send-result.v1"),
|
|
116
|
+
mode: z.enum(["sent", "agent_dispatch"]),
|
|
117
|
+
session_id: z.string(),
|
|
118
|
+
event_cursor: z.number().int().nullable(),
|
|
119
|
+
launch_id: z.string().nullable(),
|
|
120
|
+
vendor: z.enum(["claude", "codex", "grok", "composer"]).nullable(),
|
|
121
|
+
},
|
|
122
|
+
}, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
|
|
124
123
|
try {
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
enter
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
if (!force && core.isAgentSession(session_id)) {
|
|
125
|
+
if (enter === false)
|
|
126
|
+
throw new Error("agent session への dispatch は enter:false と併用できません(手動介入は force:true)");
|
|
127
|
+
if (mark)
|
|
128
|
+
throw new Error("agent session への dispatch は mark:true と併用できません");
|
|
129
|
+
if (rtk)
|
|
130
|
+
throw new Error("agent session への dispatch は rtk:true と併用できません");
|
|
131
|
+
const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: `dispatchした(vendor=${receipt.vendor})。完了通知: aiterm-wait --session ${receipt.session_id} --cursor ${receipt.event_cursor} を` +
|
|
137
|
+
"ホストのバックグラウンドタスクとして実行し exit を待つ。回収: pty_read(agent_transcript:true)",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
structuredContent: {
|
|
141
|
+
schema: "aiterm.pty-send-result.v1",
|
|
142
|
+
mode: "agent_dispatch",
|
|
143
|
+
session_id: receipt.session_id,
|
|
144
|
+
event_cursor: receipt.event_cursor,
|
|
145
|
+
launch_id: receipt.launch_id,
|
|
146
|
+
vendor: receipt.vendor,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
137
149
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
const out = core.send(session_id, text, { enter, mark, force, rtk, raw });
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: out }],
|
|
153
|
+
structuredContent: {
|
|
154
|
+
schema: "aiterm.pty-send-result.v1",
|
|
155
|
+
mode: "sent",
|
|
156
|
+
session_id,
|
|
157
|
+
event_cursor: null,
|
|
158
|
+
launch_id: null,
|
|
159
|
+
vendor: null,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
141
162
|
}
|
|
142
163
|
catch (e) {
|
|
143
164
|
return fail(e);
|
|
@@ -279,7 +300,6 @@ server.registerTool("claude_turn", {
|
|
|
279
300
|
session_id: z.string(),
|
|
280
301
|
operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
281
302
|
text: z.string().optional().describe("issueだけに指定するbounded turn本文"),
|
|
282
|
-
timeout: z.number().min(0).max(3600).optional().describe("issueだけに指定するStop待ち秒数"),
|
|
283
303
|
},
|
|
284
304
|
outputSchema: {
|
|
285
305
|
schema: z.literal("aiterm.claude-operation-result.v1"),
|
|
@@ -290,14 +310,13 @@ server.registerTool("claude_turn", {
|
|
|
290
310
|
raw_output: z.string().nullable(),
|
|
291
311
|
reason: z.enum(["operation_not_found", "result_unknown"]).nullable(),
|
|
292
312
|
},
|
|
293
|
-
}, async ({ action, session_id, operation_id, text
|
|
313
|
+
}, async ({ action, session_id, operation_id, text }) => {
|
|
294
314
|
try {
|
|
295
315
|
const result = await core.runClaudeOperation({
|
|
296
316
|
action,
|
|
297
317
|
session_id,
|
|
298
318
|
operation_id,
|
|
299
319
|
text: text ?? undefined,
|
|
300
|
-
timeout,
|
|
301
320
|
});
|
|
302
321
|
return {
|
|
303
322
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
@@ -324,40 +343,25 @@ const agentEffortDesc = (kind) => kind === "claude"
|
|
|
324
343
|
: "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
|
|
325
344
|
"ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
|
|
326
345
|
function registerAgentTool(toolName, kind, desc) {
|
|
327
|
-
const initialPromptWaitSchema = {};
|
|
328
|
-
if (kind === "codex" || kind === "claude") {
|
|
329
|
-
initialPromptWaitSchema.wait = z
|
|
330
|
-
.enum(["none", "agent_done"])
|
|
331
|
-
.default("none")
|
|
332
|
-
.describe("none=従来通り起動/初回prompt送信のみ。agent_done=起動時promptのStop hookまで待つ(agent_done:true必須)");
|
|
333
|
-
initialPromptWaitSchema.timeout = z.number().default(600).describe("wait:'agent_done' の最大待ち秒数");
|
|
334
|
-
initialPromptWaitSchema.screen = z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする");
|
|
335
|
-
initialPromptWaitSchema.lines = z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行");
|
|
336
|
-
}
|
|
337
346
|
const correlatedLaunchSchema = {};
|
|
338
347
|
if (kind === "claude") {
|
|
339
348
|
correlatedLaunchSchema.launch_operation_id = z
|
|
340
349
|
.string()
|
|
341
350
|
.regex(/^sha256:[0-9a-f]{64}$/)
|
|
342
351
|
.optional()
|
|
343
|
-
.describe("promptless managed launchのexact replay相関ID。session_name
|
|
352
|
+
.describe("promptless managed launchのexact replay相関ID。session_name必須");
|
|
344
353
|
}
|
|
345
354
|
server.registerTool(toolName, {
|
|
346
355
|
description: desc,
|
|
347
356
|
inputSchema: {
|
|
348
|
-
prompt: z.string().nullish().describe("
|
|
357
|
+
prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。送信後は待たずに即返る"),
|
|
349
358
|
model: z.string().nullish().describe(agentModelDesc(kind)),
|
|
350
359
|
// grok/composer の effort は対話 TUI で無効(headless 専用)=core 側が起動前に明示エラーで拒否。
|
|
351
360
|
// codex は CLI 側の値集合が版で変わるため縛らない(core 側も同方針)。
|
|
352
361
|
reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
|
|
353
362
|
cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
|
|
354
363
|
session_name: z.string().nullish().describe("セッション名(省略で自動採番)"),
|
|
355
|
-
agent_done: z
|
|
356
|
-
.boolean()
|
|
357
|
-
.default(false)
|
|
358
|
-
.describe("managed Stop hook を有効化し、pty_send(wait:'agent_done') を使えるようにする"),
|
|
359
364
|
...correlatedLaunchSchema,
|
|
360
|
-
...initialPromptWaitSchema,
|
|
361
365
|
},
|
|
362
366
|
outputSchema: {
|
|
363
367
|
schema: z.literal("aiterm.agent-launch-result.v1"),
|
|
@@ -365,7 +369,7 @@ function registerAgentTool(toolName, kind, desc) {
|
|
|
365
369
|
session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
|
|
366
370
|
managed_completion: z.boolean(),
|
|
367
371
|
},
|
|
368
|
-
}, async ({ prompt, model, reasoning_effort, cwd, session_name,
|
|
372
|
+
}, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id }) => {
|
|
369
373
|
try {
|
|
370
374
|
const [sid, hint] = await core.openAgentWithInitialPrompt(kind, {
|
|
371
375
|
prompt: prompt ?? undefined,
|
|
@@ -373,18 +377,13 @@ function registerAgentTool(toolName, kind, desc) {
|
|
|
373
377
|
reasoning_effort: reasoning_effort ?? undefined,
|
|
374
378
|
cwd: cwd ?? undefined,
|
|
375
379
|
session_name: session_name ?? undefined,
|
|
376
|
-
agent_done: agent_done ?? false,
|
|
377
380
|
launch_operation_id: launch_operation_id ?? undefined,
|
|
378
|
-
wait: wait ?? "none",
|
|
379
|
-
timeout,
|
|
380
|
-
screen,
|
|
381
|
-
lines,
|
|
382
381
|
});
|
|
383
382
|
const structured = {
|
|
384
383
|
schema: "aiterm.agent-launch-result.v1",
|
|
385
384
|
provider: kind,
|
|
386
385
|
session_id: sid,
|
|
387
|
-
managed_completion:
|
|
386
|
+
managed_completion: true,
|
|
388
387
|
};
|
|
389
388
|
return {
|
|
390
389
|
content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
|
|
@@ -397,7 +396,7 @@ function registerAgentTool(toolName, kind, desc) {
|
|
|
397
396
|
});
|
|
398
397
|
}
|
|
399
398
|
registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
|
|
400
|
-
"同じ利用者可視sessionへpty_send
|
|
399
|
+
"同じ利用者可視sessionへpty_sendで継続入力する。常にmanaged(isolated settingsのStop hook)で起動し、完了通知はaiterm-wait、結果はpty_read(agent_transcript)/claude_turnで回収する。");
|
|
401
400
|
registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
|
|
402
401
|
"起動後は pty_read で画面を読み pty_send で操作する。model / reasoning_effort を引数で指定可" +
|
|
403
402
|
"(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"mcpName": "io.github.kitepon-rgb/aiterm-mcp",
|
|
5
5
|
"description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Claude/Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
|
|
6
6
|
"keywords": [
|