@ymdvsymd/tornado 0.5.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/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@ymdvsymd/tornado",
3
+ "version": "0.5.0",
4
+ "description": "Multi-agent development orchestrator with TUI",
5
+ "bin": {
6
+ "tornado": "bin/tornado.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "sdk/"
11
+ ],
12
+ "type": "commonjs",
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/mizchi/tornado"
19
+ },
20
+ "homepage": "https://github.com/mizchi/tornado",
21
+ "bugs": {
22
+ "url": "https://github.com/mizchi/tornado/issues"
23
+ },
24
+ "keywords": [
25
+ "moonbit",
26
+ "agent",
27
+ "orchestration",
28
+ "tui",
29
+ "cli"
30
+ ],
31
+ "author": "mizchi",
32
+ "license": "MIT",
33
+ "scripts": {
34
+ "build:sdk": "tsc -p tsconfig.sdk.json"
35
+ },
36
+ "dependencies": {
37
+ "@anthropic-ai/claude-agent-sdk": "^0.2.49",
38
+ "@openai/codex-sdk": "^0.104.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.0.0",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ export type RunnerOptions = {
2
+ prompt: string;
3
+ cwd?: string;
4
+ model?: string;
5
+ systemPrompt?: string;
6
+ sessionId?: string;
7
+ threadId?: string;
8
+ };
9
+
10
+ export type AdapterEmission = {
11
+ event?: unknown;
12
+ log?: string;
13
+ };
14
+
15
+ export type AdapterStartResult<RawEvent> = {
16
+ sessionId: string;
17
+ stream: AsyncIterable<RawEvent>;
18
+ initEvents?: readonly unknown[];
19
+ initLogs?: readonly string[];
20
+ };
21
+
22
+ export type AdapterIO = {
23
+ write(event: unknown): void;
24
+ log(line: string): void;
25
+ };
26
+
27
+ export interface AgentAdapter<RawEvent> {
28
+ readonly tag: string;
29
+ start(opts: RunnerOptions): Promise<AdapterStartResult<RawEvent>>;
30
+ emit(raw: RawEvent, sessionId: string): readonly AdapterEmission[];
31
+ }
@@ -0,0 +1,39 @@
1
+ import { createLogger, writeJsonl } from "./runner-io.mjs";
2
+ export async function runAdapter(adapter, opts, io) {
3
+ const sink = io ?? createDefaultIO(adapter.tag);
4
+ const started = await adapter.start(opts);
5
+ for (const event of started.initEvents ?? []) {
6
+ sink.write(event);
7
+ }
8
+ for (const line of started.initLogs ?? []) {
9
+ sink.log(line);
10
+ }
11
+ for await (const raw of started.stream) {
12
+ const emissions = adapter.emit(raw, started.sessionId);
13
+ for (const emission of emissions) {
14
+ if (emission.event !== undefined) {
15
+ sink.write(emission.event);
16
+ }
17
+ if (emission.log) {
18
+ sink.log(emission.log);
19
+ }
20
+ }
21
+ }
22
+ }
23
+ export async function runAdapterFromArgv(adapter, argv = process.argv) {
24
+ const arg = argv[2];
25
+ if (!arg) {
26
+ throw new Error("Missing runner options JSON in argv[2]");
27
+ }
28
+ const opts = JSON.parse(arg);
29
+ await runAdapter(adapter, opts);
30
+ }
31
+ function createDefaultIO(tag) {
32
+ const log = createLogger(tag);
33
+ return {
34
+ write(event) {
35
+ writeJsonl(event);
36
+ },
37
+ log,
38
+ };
39
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ AdapterIO,
3
+ AgentAdapter,
4
+ RunnerOptions,
5
+ } from "./agent-adapter.mjs";
6
+ import { createLogger, writeJsonl } from "./runner-io.mjs";
7
+
8
+ export async function runAdapter<RawEvent>(
9
+ adapter: AgentAdapter<RawEvent>,
10
+ opts: RunnerOptions,
11
+ io?: AdapterIO,
12
+ ): Promise<void> {
13
+ const sink = io ?? createDefaultIO(adapter.tag);
14
+ const started = await adapter.start(opts);
15
+
16
+ for (const event of started.initEvents ?? []) {
17
+ sink.write(event);
18
+ }
19
+ for (const line of started.initLogs ?? []) {
20
+ sink.log(line);
21
+ }
22
+
23
+ for await (const raw of started.stream) {
24
+ const emissions = adapter.emit(raw, started.sessionId);
25
+ for (const emission of emissions) {
26
+ if (emission.event !== undefined) {
27
+ sink.write(emission.event);
28
+ }
29
+ if (emission.log) {
30
+ sink.log(emission.log);
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ export async function runAdapterFromArgv<RawEvent>(
37
+ adapter: AgentAdapter<RawEvent>,
38
+ argv: readonly string[] = process.argv,
39
+ ): Promise<void> {
40
+ const arg = argv[2];
41
+ if (!arg) {
42
+ throw new Error("Missing runner options JSON in argv[2]");
43
+ }
44
+ const opts = JSON.parse(arg) as RunnerOptions;
45
+ await runAdapter(adapter, opts);
46
+ }
47
+
48
+ function createDefaultIO(tag: string): AdapterIO {
49
+ const log = createLogger(tag);
50
+ return {
51
+ write(event) {
52
+ writeJsonl(event);
53
+ },
54
+ log,
55
+ };
56
+ }
@@ -0,0 +1,69 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { runAdapter } from './agent-runner.mjs';
5
+
6
+ test('runAdapter emits init events and mapped stream events in order', async () => {
7
+ const events = [];
8
+ const logs = [];
9
+
10
+ const adapter = {
11
+ tag: 'Mock',
12
+ async start(opts) {
13
+ assert.equal(opts.prompt, 'hello');
14
+ return {
15
+ sessionId: 's-1',
16
+ initEvents: [{ type: 'system', subtype: 'init', session_id: 's-1' }],
17
+ initLogs: ['booted'],
18
+ stream: toAsync([{ kind: 'one' }, { kind: 'two' }]),
19
+ };
20
+ },
21
+ emit(raw, sessionId) {
22
+ return [{ event: { type: 'assistant', raw: raw.kind, session_id: sessionId }, log: `saw:${raw.kind}` }];
23
+ },
24
+ };
25
+
26
+ await runAdapter(adapter, { prompt: 'hello' }, {
27
+ write: (event) => events.push(event),
28
+ log: (line) => logs.push(line),
29
+ });
30
+
31
+ assert.deepEqual(events, [
32
+ { type: 'system', subtype: 'init', session_id: 's-1' },
33
+ { type: 'assistant', raw: 'one', session_id: 's-1' },
34
+ { type: 'assistant', raw: 'two', session_id: 's-1' },
35
+ ]);
36
+ assert.deepEqual(logs, ['booted', 'saw:one', 'saw:two']);
37
+ });
38
+
39
+ test('runAdapter supports log-only emissions', async () => {
40
+ const events = [];
41
+ const logs = [];
42
+
43
+ const adapter = {
44
+ tag: 'Mock',
45
+ async start() {
46
+ return {
47
+ sessionId: 's-2',
48
+ stream: toAsync([{ kind: 'tick' }]),
49
+ };
50
+ },
51
+ emit() {
52
+ return [{ log: 'only-log' }];
53
+ },
54
+ };
55
+
56
+ await runAdapter(adapter, { prompt: 'x' }, {
57
+ write: (event) => events.push(event),
58
+ log: (line) => logs.push(line),
59
+ });
60
+
61
+ assert.deepEqual(events, []);
62
+ assert.deepEqual(logs, ['only-log']);
63
+ });
64
+
65
+ async function* toAsync(values) {
66
+ for (const value of values) {
67
+ yield value;
68
+ }
69
+ }
@@ -0,0 +1,105 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ export function createClaudeAdapter() {
3
+ return {
4
+ tag: "Claude",
5
+ async start(opts) {
6
+ const queryOpts = buildQueryOptions(opts);
7
+ return {
8
+ sessionId: opts.sessionId || "",
9
+ stream: query({
10
+ prompt: opts.prompt,
11
+ options: queryOpts,
12
+ }),
13
+ };
14
+ },
15
+ emit(raw) {
16
+ const emissions = [{ event: raw }];
17
+ const logs = extractLogs(raw);
18
+ for (const line of logs) {
19
+ emissions.push({ log: line });
20
+ }
21
+ return emissions;
22
+ },
23
+ };
24
+ }
25
+ function buildQueryOptions(opts) {
26
+ const queryOptions = {
27
+ includePartialMessages: true,
28
+ permissionMode: "bypassPermissions",
29
+ allowDangerouslySkipPermissions: true,
30
+ cwd: opts.cwd || process.cwd(),
31
+ };
32
+ if (opts.sessionId)
33
+ queryOptions.resume = opts.sessionId;
34
+ if (opts.model)
35
+ queryOptions.model = opts.model;
36
+ if (opts.systemPrompt)
37
+ queryOptions.systemPrompt = opts.systemPrompt;
38
+ return queryOptions;
39
+ }
40
+ function extractLogs(message) {
41
+ switch (message.type) {
42
+ case "system":
43
+ return extractSystemLog(message);
44
+ case "stream_event":
45
+ return extractStreamEventLog(message);
46
+ case "assistant":
47
+ return extractToolUseLog(message);
48
+ case "result":
49
+ return [formatResultLog(message)];
50
+ default:
51
+ return [];
52
+ }
53
+ }
54
+ function extractSystemLog(message) {
55
+ if (message.subtype !== "init")
56
+ return [];
57
+ return [
58
+ `Session init: model=${message.model || "unknown"}, session=${message.session_id || "new"}`,
59
+ ];
60
+ }
61
+ function extractStreamEventLog(message) {
62
+ const event = message.event;
63
+ if (event?.type !== "content_block_start")
64
+ return [];
65
+ const block = event.content_block;
66
+ if (block?.type === "tool_use") {
67
+ return [`Tool: ${block.name}`];
68
+ }
69
+ if (block?.type === "thinking") {
70
+ return ["Thinking..."];
71
+ }
72
+ if (block?.type === "text") {
73
+ return ["Generating..."];
74
+ }
75
+ return [];
76
+ }
77
+ function extractToolUseLog(message) {
78
+ const logs = [];
79
+ const content = message.message?.content;
80
+ if (!Array.isArray(content))
81
+ return logs;
82
+ for (const block of content) {
83
+ if (block.type !== "tool_use")
84
+ continue;
85
+ const inputPreview = JSON.stringify(block.input).slice(0, 100);
86
+ logs.push(`${block.name}(${inputPreview})`);
87
+ }
88
+ return logs;
89
+ }
90
+ function formatResultLog(message) {
91
+ const parts = [`Result: ${message.subtype}`];
92
+ if (message.total_cost_usd) {
93
+ parts.push(`cost=$${message.total_cost_usd.toFixed(4)}`);
94
+ }
95
+ if (message.duration_ms) {
96
+ parts.push(`${(message.duration_ms / 1000).toFixed(1)}s`);
97
+ }
98
+ if (message.usage) {
99
+ const { input_tokens, output_tokens } = message.usage;
100
+ if (input_tokens || output_tokens) {
101
+ parts.push(`${input_tokens || 0}in/${output_tokens || 0}out`);
102
+ }
103
+ }
104
+ return parts.join(", ");
105
+ }
@@ -0,0 +1,143 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import type {
3
+ AdapterEmission,
4
+ AdapterStartResult,
5
+ AgentAdapter,
6
+ RunnerOptions,
7
+ } from "./agent-adapter.mjs";
8
+
9
+ type ClaudeMessage = {
10
+ type?: string;
11
+ subtype?: string;
12
+ model?: string;
13
+ session_id?: string;
14
+ event?: {
15
+ type?: string;
16
+ content_block?: { type?: string; name?: string };
17
+ };
18
+ message?: {
19
+ content?: Array<{
20
+ type?: string;
21
+ name?: string;
22
+ input?: unknown;
23
+ }>;
24
+ };
25
+ total_cost_usd?: number;
26
+ duration_ms?: number;
27
+ usage?: {
28
+ input_tokens?: number;
29
+ output_tokens?: number;
30
+ };
31
+ [key: string]: unknown;
32
+ };
33
+
34
+ export function createClaudeAdapter(): AgentAdapter<ClaudeMessage> {
35
+ return {
36
+ tag: "Claude",
37
+ async start(opts: RunnerOptions): Promise<AdapterStartResult<ClaudeMessage>> {
38
+ const queryOpts = buildQueryOptions(opts);
39
+ return {
40
+ sessionId: opts.sessionId || "",
41
+ stream: query({
42
+ prompt: opts.prompt,
43
+ options: queryOpts,
44
+ }) as AsyncIterable<ClaudeMessage>,
45
+ };
46
+ },
47
+ emit(raw: ClaudeMessage): readonly AdapterEmission[] {
48
+ const emissions: AdapterEmission[] = [{ event: raw }];
49
+ const logs = extractLogs(raw);
50
+ for (const line of logs) {
51
+ emissions.push({ log: line });
52
+ }
53
+ return emissions;
54
+ },
55
+ };
56
+ }
57
+
58
+ function buildQueryOptions(opts: RunnerOptions): Record<string, unknown> {
59
+ const queryOptions: Record<string, unknown> = {
60
+ includePartialMessages: true,
61
+ permissionMode: "bypassPermissions",
62
+ allowDangerouslySkipPermissions: true,
63
+ cwd: opts.cwd || process.cwd(),
64
+ };
65
+
66
+ if (opts.sessionId) queryOptions.resume = opts.sessionId;
67
+ if (opts.model) queryOptions.model = opts.model;
68
+ if (opts.systemPrompt) queryOptions.systemPrompt = opts.systemPrompt;
69
+
70
+ return queryOptions;
71
+ }
72
+
73
+ function extractLogs(message: ClaudeMessage): string[] {
74
+ switch (message.type) {
75
+ case "system":
76
+ return extractSystemLog(message);
77
+ case "stream_event":
78
+ return extractStreamEventLog(message);
79
+ case "assistant":
80
+ return extractToolUseLog(message);
81
+ case "result":
82
+ return [formatResultLog(message)];
83
+ default:
84
+ return [];
85
+ }
86
+ }
87
+
88
+ function extractSystemLog(message: ClaudeMessage): string[] {
89
+ if (message.subtype !== "init") return [];
90
+ return [
91
+ `Session init: model=${message.model || "unknown"}, session=${message.session_id || "new"}`,
92
+ ];
93
+ }
94
+
95
+ function extractStreamEventLog(message: ClaudeMessage): string[] {
96
+ const event = message.event;
97
+ if (event?.type !== "content_block_start") return [];
98
+
99
+ const block = event.content_block;
100
+ if (block?.type === "tool_use") {
101
+ return [`Tool: ${block.name}`];
102
+ }
103
+ if (block?.type === "thinking") {
104
+ return ["Thinking..."];
105
+ }
106
+ if (block?.type === "text") {
107
+ return ["Generating..."];
108
+ }
109
+ return [];
110
+ }
111
+
112
+ function extractToolUseLog(message: ClaudeMessage): string[] {
113
+ const logs: string[] = [];
114
+ const content = message.message?.content;
115
+ if (!Array.isArray(content)) return logs;
116
+
117
+ for (const block of content) {
118
+ if (block.type !== "tool_use") continue;
119
+ const inputPreview = JSON.stringify(block.input).slice(0, 100);
120
+ logs.push(`${block.name}(${inputPreview})`);
121
+ }
122
+
123
+ return logs;
124
+ }
125
+
126
+ function formatResultLog(message: ClaudeMessage): string {
127
+ const parts = [`Result: ${message.subtype}`];
128
+
129
+ if (message.total_cost_usd) {
130
+ parts.push(`cost=$${message.total_cost_usd.toFixed(4)}`);
131
+ }
132
+ if (message.duration_ms) {
133
+ parts.push(`${(message.duration_ms / 1000).toFixed(1)}s`);
134
+ }
135
+ if (message.usage) {
136
+ const { input_tokens, output_tokens } = message.usage;
137
+ if (input_tokens || output_tokens) {
138
+ parts.push(`${input_tokens || 0}in/${output_tokens || 0}out`);
139
+ }
140
+ }
141
+
142
+ return parts.join(", ");
143
+ }
@@ -0,0 +1,3 @@
1
+ import { runAdapterFromArgv } from "./agent-runner.mjs";
2
+ import { createClaudeAdapter } from "./claude-adapter.mjs";
3
+ await runAdapterFromArgv(createClaudeAdapter());
@@ -0,0 +1,4 @@
1
+ import { runAdapterFromArgv } from "./agent-runner.mjs";
2
+ import { createClaudeAdapter } from "./claude-adapter.mjs";
3
+
4
+ await runAdapterFromArgv(createClaudeAdapter());
@@ -0,0 +1,77 @@
1
+ import { Codex } from "@openai/codex-sdk";
2
+ import { formatTurnCompletedLog, formatTurnFailedLog, normalizeItemComplete, normalizeItemStart, normalizeTurnCompleted, normalizeTurnFailed, } from "./codex-normalizer.mjs";
3
+ export function createCodexAdapter(client = new Codex()) {
4
+ return {
5
+ tag: "Codex",
6
+ async start(opts) {
7
+ const threadOpts = {
8
+ model: opts.model || undefined,
9
+ workingDirectory: opts.cwd || process.cwd(),
10
+ approvalPolicy: "never",
11
+ };
12
+ const logs = [];
13
+ const thread = opts.threadId
14
+ ? resumeThread(client, opts.threadId, threadOpts, logs)
15
+ : startThread(client, threadOpts, logs);
16
+ logs.push(`Thread: ${thread.id}`);
17
+ const run = await thread.runStreamed(opts.prompt);
18
+ return {
19
+ sessionId: thread.id,
20
+ initEvents: [
21
+ {
22
+ type: "system",
23
+ subtype: "init",
24
+ session_id: thread.id,
25
+ model: opts.model || "default",
26
+ },
27
+ ],
28
+ initLogs: logs,
29
+ stream: run.events,
30
+ };
31
+ },
32
+ emit(raw, sessionId) {
33
+ switch (raw.type) {
34
+ case "item.started":
35
+ return emitItemStart(raw.item);
36
+ case "item.completed":
37
+ return emitItemComplete(raw.item);
38
+ case "turn.completed": {
39
+ const resultEvent = normalizeTurnCompleted(raw, sessionId);
40
+ return [{ event: resultEvent, log: formatTurnCompletedLog(raw) }];
41
+ }
42
+ case "turn.failed": {
43
+ const errorEvent = normalizeTurnFailed(raw, sessionId);
44
+ return [{ event: errorEvent, log: formatTurnFailedLog(raw) }];
45
+ }
46
+ default:
47
+ return [];
48
+ }
49
+ },
50
+ };
51
+ }
52
+ function resumeThread(client, threadId, opts, logs) {
53
+ logs.push(`Resuming thread: ${threadId}`);
54
+ return client.resumeThread(threadId, opts);
55
+ }
56
+ function startThread(client, opts, logs) {
57
+ logs.push("Starting new thread");
58
+ return client.startThread(opts);
59
+ }
60
+ function emitItemStart(item) {
61
+ const normalized = normalizeItemStart(item || {});
62
+ if (!normalized)
63
+ return [];
64
+ const display = typeof normalized._display === "string"
65
+ ? normalized._display
66
+ : item?.type || "item.started";
67
+ return [{ event: normalized, log: display }];
68
+ }
69
+ function emitItemComplete(item) {
70
+ const normalized = normalizeItemComplete(item || {});
71
+ if (!normalized)
72
+ return [];
73
+ const display = typeof normalized._display === "string"
74
+ ? normalized._display
75
+ : item?.type || "item.completed";
76
+ return [{ event: normalized, log: `Done: ${display}` }];
77
+ }