Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ codiva/
running ──(canUseTool 発火)───────────────▶ awaiting_permission
awaiting_permission ──(ユーザー応答)────────▶ running
running ──(result 受信 & 質問で終了)────────▶ awaiting_input
running ──(result 受信 & 正常終了)──────────▶ completed
running ──(result 受信 & 正常終了 & サブエージェント未稼働)──▶ completed
running ──(result 受信 & サブエージェント稼働中)──▶ running # 結果を deferredResult に保留し running 継続
running ──(最後の task_notification で全タスク完了 & 保留結果あり)──▶ completed
running ──(result subtype がエラー系)───────▶ failed
running ──(レート制限に到達)─────────────────▶ rate_limited # rate_limit_event(rejected) / error='rate_limit' / usage-limit result・throw
awaiting_input ──(追加指示送信)─────────────▶ running
Expand All @@ -124,6 +126,17 @@ codiva/
`interrupted` に丸める(正常終了した `completed` とは区別する)。復元後は `completed` と同じく
idle で resumable、追加指示で resume できる。

**サブエージェント(Task ツール)の完了ゲート**: サブエージェントが **バックグラウンド実行**されると、
その tool_result は即座に返り本体ターンは続行するため、サブエージェントがまだ稼働中でも**トップレベルの
`result/success` が先に届く**。この result をそのまま `completed` にすると、実際には作業継続中なのに
バッジが「Completed」へ倒れてしまう(本 issue の不具合)。対策として `system/task_started` /
`system/task_notification` で稼働中タスク集合(`activeTaskIds`)を追跡し、result 受信時にタスクが残って
いれば `completed` にせず結果を `deferredResult` に保留して `running` を維持する。最後のタスクが
`task_notification` で settle し集合が空になった時点で保留結果を使って `completed` を確定する
(`sdk-parse.ts` の `onTaskStarted` / `onTaskSettled` / `completeWith`)。`skip_transcript` の雑務タスクは
ゲート対象外。`activeTaskIds` / `deferredResult` は transient で永続しない。実データは
`__fixtures__/session-subagent.jsonl`(スパイクの `subagent` シナリオで採取)。

`rate_limited` は「使用量/レート制限に達して止まった」セッションを表す。`completed`/`failed` と同じく
idle だが、エラー扱い(`failed`)にはせず「制限が解けるのを待って再開できる」状態として区別する。
検知元は SDK の `rate_limit_event`(`rate_limit_info.status === 'rejected'`)、assistant メッセージの
Expand Down
2 changes: 2 additions & 0 deletions docs/TECH_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ const options = {

**注意**: streaming input mode では `result` はターンの区切りごとに届き、セッション終了を意味しない。`result` 受信 = 「Claude のターンが終わり入力待ちになった」と解釈する(completed 判定はこのタイミング)。

**サブエージェント(Task ツール)**: 本体エージェントが Task ツールで作業を委譲すると、サブエージェントのメッセージは `parent_tool_use_id`(= Task の tool_use id)付きで流れてくる(トップレベルは `null`)。サブエージェント自身は独自の `result` を出さず、**`result/success` は最後にトップレベル1件だけ**。ライフサイクルは `system/task_started` → `system/task_progress` → `system/task_notification`(`status: 'completed'|'failed'|'stopped'`)。Task が**バックグラウンド実行**されると tool_result が即返り本体ターンが続行するため、サブエージェント稼働中に `result/success` が先に届きうる。このとき素直に completed 判定すると「作業中なのに Completed」になる。対策は `task_started`/`task_notification` で稼働中タスクを追跡し、稼働中に届いた result を保留 → 全タスク settle 後に completed 確定(ARCHITECTURE.md「完了ゲート」参照)。実データは `scripts/spike.ts` の `subagent` シナリオで採取(`__fixtures__/session-subagent.jsonl`)。

`includePartialMessages: true` にすると `{ type: 'stream_event', event }`(`SDKPartialAssistantMessage`)で生のストリーミングデルタが届く。**Phase 6 で採用**:`event.type === 'content_block_delta'` かつ `delta.type === 'text_delta'` のときのみ `delta.text` を `state.streamingText` に連結し、詳細ビューにタイピング風プレビューを出す。確定 `assistant` メッセージ / `result` / 追加入力で `streamingText` はクリア(確定ログが正)。`streamingText` は transient で永続しない。非テキストデルタ(`input_json_delta`・thinking 等)は状態を変えない。`~100ms` スロットル(`useSessions`)で再描画コストを抑える。

### TODO進捗の抽出(Step n/m)— 要スパイク検証
Expand Down
16 changes: 15 additions & 1 deletion scripts/spike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { join } from 'node:path';
import type { PermissionResult, SDKMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
import { query } from '@anthropic-ai/claude-agent-sdk';

type Scenario = 'basic' | 'followup' | 'interrupt';
type Scenario = 'basic' | 'followup' | 'interrupt' | 'subagent';

const positional = process.argv.slice(2).filter((a) => !a.startsWith('--'));
const scenario = (positional[0] as Scenario) ?? 'basic';
Expand All @@ -39,6 +39,12 @@ const PROMPTS: Record<Scenario, string> = {
interrupt:
'Count slowly: create files step1.txt, step2.txt, step3.txt, step4.txt, step5.txt one at a time, ' +
'each containing its number.',
subagent:
'This is a test harness for observing sub-agent delegation. You MUST delegate the actual work ' +
'to a sub-agent using the Task tool (subagent_type "general-purpose"): ask that sub-agent to ' +
'create a file report.txt containing the single line "done by subagent" and then report back. ' +
'Do not do the file work yourself in the main thread — delegate it. After the sub-agent finishes, ' +
'tell me it is complete.',
};

const prompt = PROMPTS[scenario];
Expand Down Expand Up @@ -216,6 +222,14 @@ async function main(): Promise<void> {
input.push('Now append the word "world" to notes.txt.');
continue;
}
// subagent: close input but keep draining so any post-result background
// task settling (task_notification / background_tasks_changed / a 2nd
// result) is captured — we want to see how a result relates to sub-agent
// work still in flight rather than stopping at the first result.
if (scenario === 'subagent') {
input.close();
continue;
}
// basic / followup(after 2nd) / interrupt: end the session.
input.close();
break;
Expand Down
Loading
Loading