@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.
@@ -0,0 +1,140 @@
1
+ import { Codex } from "@openai/codex-sdk";
2
+ import type {
3
+ AdapterEmission,
4
+ AdapterStartResult,
5
+ AgentAdapter,
6
+ RunnerOptions,
7
+ } from "./agent-adapter.mjs";
8
+ import {
9
+ formatTurnCompletedLog,
10
+ formatTurnFailedLog,
11
+ normalizeItemComplete,
12
+ normalizeItemStart,
13
+ normalizeTurnCompleted,
14
+ normalizeTurnFailed,
15
+ } from "./codex-normalizer.mjs";
16
+
17
+ type CodexThreadOptions = {
18
+ model?: string;
19
+ workingDirectory: string;
20
+ approvalPolicy: "never";
21
+ };
22
+
23
+ type CodexThread = {
24
+ id: string;
25
+ runStreamed(prompt: string): Promise<{ events: AsyncIterable<CodexEvent> }>;
26
+ };
27
+
28
+ type CodexClient = {
29
+ startThread(opts: CodexThreadOptions): CodexThread;
30
+ resumeThread(threadId: string, opts: CodexThreadOptions): CodexThread;
31
+ };
32
+
33
+ type CodexItem = {
34
+ type?: string;
35
+ _display?: string;
36
+ [key: string]: unknown;
37
+ };
38
+
39
+ type CodexEvent = {
40
+ type?: string;
41
+ usage?: {
42
+ input_tokens?: number;
43
+ output_tokens?: number;
44
+ cached_input_tokens?: number;
45
+ };
46
+ error?: { message?: string };
47
+ item?: CodexItem;
48
+ };
49
+
50
+ export function createCodexAdapter(
51
+ client: CodexClient = new Codex() as unknown as CodexClient,
52
+ ): AgentAdapter<CodexEvent> {
53
+ return {
54
+ tag: "Codex",
55
+ async start(opts: RunnerOptions): Promise<AdapterStartResult<CodexEvent>> {
56
+ const threadOpts: CodexThreadOptions = {
57
+ model: opts.model || undefined,
58
+ workingDirectory: opts.cwd || process.cwd(),
59
+ approvalPolicy: "never",
60
+ };
61
+
62
+ const logs: string[] = [];
63
+ const thread = opts.threadId
64
+ ? resumeThread(client, opts.threadId, threadOpts, logs)
65
+ : startThread(client, threadOpts, logs);
66
+
67
+ logs.push(`Thread: ${thread.id}`);
68
+ const run = await thread.runStreamed(opts.prompt);
69
+
70
+ return {
71
+ sessionId: thread.id,
72
+ initEvents: [
73
+ {
74
+ type: "system",
75
+ subtype: "init",
76
+ session_id: thread.id,
77
+ model: opts.model || "default",
78
+ },
79
+ ],
80
+ initLogs: logs,
81
+ stream: run.events,
82
+ };
83
+ },
84
+ emit(raw: CodexEvent, sessionId: string): readonly AdapterEmission[] {
85
+ switch (raw.type) {
86
+ case "item.started":
87
+ return emitItemStart(raw.item);
88
+ case "item.completed":
89
+ return emitItemComplete(raw.item);
90
+ case "turn.completed": {
91
+ const resultEvent = normalizeTurnCompleted(raw, sessionId);
92
+ return [{ event: resultEvent, log: formatTurnCompletedLog(raw) }];
93
+ }
94
+ case "turn.failed": {
95
+ const errorEvent = normalizeTurnFailed(raw, sessionId);
96
+ return [{ event: errorEvent, log: formatTurnFailedLog(raw) }];
97
+ }
98
+ default:
99
+ return [];
100
+ }
101
+ },
102
+ };
103
+ }
104
+
105
+ function resumeThread(
106
+ client: CodexClient,
107
+ threadId: string,
108
+ opts: CodexThreadOptions,
109
+ logs: string[],
110
+ ): CodexThread {
111
+ logs.push(`Resuming thread: ${threadId}`);
112
+ return client.resumeThread(threadId, opts);
113
+ }
114
+
115
+ function startThread(
116
+ client: CodexClient,
117
+ opts: CodexThreadOptions,
118
+ logs: string[],
119
+ ): CodexThread {
120
+ logs.push("Starting new thread");
121
+ return client.startThread(opts);
122
+ }
123
+
124
+ function emitItemStart(item: CodexItem | undefined): readonly AdapterEmission[] {
125
+ const normalized = normalizeItemStart(item || {});
126
+ if (!normalized) return [];
127
+ const display = typeof normalized._display === "string"
128
+ ? normalized._display
129
+ : item?.type || "item.started";
130
+ return [{ event: normalized, log: display }];
131
+ }
132
+
133
+ function emitItemComplete(item: CodexItem | undefined): readonly AdapterEmission[] {
134
+ const normalized = normalizeItemComplete(item || {});
135
+ if (!normalized) return [];
136
+ const display = typeof normalized._display === "string"
137
+ ? normalized._display
138
+ : item?.type || "item.completed";
139
+ return [{ event: normalized, log: `Done: ${display}` }];
140
+ }
@@ -0,0 +1,152 @@
1
+ function truncate(value, max = 80) {
2
+ return String(value || "").slice(0, max);
3
+ }
4
+ export function normalizeItemStart(item) {
5
+ switch (item?.type) {
6
+ case "command_execution":
7
+ return {
8
+ type: "assistant",
9
+ message: {
10
+ content: [
11
+ {
12
+ type: "tool_use",
13
+ name: "Bash",
14
+ input: { command: item.command || "" },
15
+ },
16
+ ],
17
+ },
18
+ _display: `Bash(${truncate(item.command)})`,
19
+ };
20
+ case "file_change": {
21
+ const changes = item.changes || [];
22
+ const paths = changes.map((change) => change.path).join(", ");
23
+ return {
24
+ type: "assistant",
25
+ message: {
26
+ content: [
27
+ {
28
+ type: "tool_use",
29
+ name: "Edit",
30
+ input: { file_path: paths },
31
+ },
32
+ ],
33
+ },
34
+ _display: `Edit(${truncate(paths)})`,
35
+ };
36
+ }
37
+ case "mcp_tool_call":
38
+ return {
39
+ type: "assistant",
40
+ message: {
41
+ content: [
42
+ {
43
+ type: "tool_use",
44
+ name: item.server_name || "mcp",
45
+ input: item.arguments || {},
46
+ },
47
+ ],
48
+ },
49
+ _display: `MCP(${item.server_name || "?"})`,
50
+ };
51
+ case "agent_message":
52
+ return {
53
+ type: "content_block_start",
54
+ content_block: { type: "text" },
55
+ _display: "Generating...",
56
+ };
57
+ case "reasoning":
58
+ return {
59
+ type: "content_block_start",
60
+ content_block: { type: "thinking" },
61
+ _display: "Thinking...",
62
+ };
63
+ default:
64
+ return null;
65
+ }
66
+ }
67
+ export function normalizeItemComplete(item) {
68
+ switch (item?.type) {
69
+ case "command_execution":
70
+ return {
71
+ type: "tool_result",
72
+ tool_name: "Bash",
73
+ content: item.output || `exit_code=${item.exit_code || 0}`,
74
+ _display: `Bash: exit=${item.exit_code || 0}`,
75
+ };
76
+ case "file_change": {
77
+ const changes = item.changes || [];
78
+ const summary = changes
79
+ .map((change) => `${change.kind}: ${change.path}`)
80
+ .join(", ");
81
+ return {
82
+ type: "tool_result",
83
+ tool_name: "Edit",
84
+ content: summary || "file changed",
85
+ _display: `Edit: ${truncate(summary)}`,
86
+ };
87
+ }
88
+ case "mcp_tool_call": {
89
+ const output = item.result || item.error || "";
90
+ return {
91
+ type: "tool_result",
92
+ tool_name: item.server_name || "mcp",
93
+ content: output,
94
+ _display: `MCP: ${truncate(output)}`,
95
+ };
96
+ }
97
+ case "agent_message": {
98
+ const text = item.text || "";
99
+ return {
100
+ type: "assistant",
101
+ message: {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text,
106
+ },
107
+ ],
108
+ },
109
+ _display: `Message: ${truncate(text)}`,
110
+ };
111
+ }
112
+ default:
113
+ return null;
114
+ }
115
+ }
116
+ export function normalizeTurnCompleted(event, sessionId) {
117
+ const usage = event?.usage || {};
118
+ return {
119
+ type: "result",
120
+ subtype: "success",
121
+ session_id: sessionId,
122
+ usage: {
123
+ input_tokens: usage.input_tokens || 0,
124
+ output_tokens: usage.output_tokens || 0,
125
+ cache_read_input_tokens: usage.cached_input_tokens || 0,
126
+ cache_creation_input_tokens: 0,
127
+ },
128
+ };
129
+ }
130
+ export function normalizeTurnFailed(event, sessionId) {
131
+ return {
132
+ type: "result",
133
+ subtype: "error",
134
+ session_id: sessionId,
135
+ is_error: true,
136
+ result: event?.error?.message || "Turn failed",
137
+ };
138
+ }
139
+ export function formatTurnCompletedLog(event) {
140
+ const usage = event?.usage || {};
141
+ const parts = ["Result: success"];
142
+ if (usage.input_tokens || usage.output_tokens) {
143
+ parts.push(`${usage.input_tokens || 0}in/${usage.output_tokens || 0}out`);
144
+ if (usage.cached_input_tokens) {
145
+ parts.push(`cached=${usage.cached_input_tokens}`);
146
+ }
147
+ }
148
+ return parts.join(", ");
149
+ }
150
+ export function formatTurnFailedLog(event) {
151
+ return `Failed: ${event?.error?.message || "unknown error"}`;
152
+ }
@@ -0,0 +1,190 @@
1
+ type UnknownRecord = Record<string, unknown>;
2
+
3
+ type CodexItem = {
4
+ type?: string;
5
+ command?: string;
6
+ changes?: Array<{ kind?: string; path?: string }>;
7
+ server_name?: string;
8
+ arguments?: UnknownRecord;
9
+ output?: string;
10
+ exit_code?: number;
11
+ result?: string;
12
+ error?: string;
13
+ text?: string;
14
+ };
15
+
16
+ type CodexTurnUsage = {
17
+ input_tokens?: number;
18
+ output_tokens?: number;
19
+ cached_input_tokens?: number;
20
+ };
21
+
22
+ type CodexTurnEvent = {
23
+ usage?: CodexTurnUsage;
24
+ error?: { message?: string };
25
+ };
26
+
27
+ function truncate(value: unknown, max = 80): string {
28
+ return String(value || "").slice(0, max);
29
+ }
30
+
31
+ export function normalizeItemStart(item: CodexItem): UnknownRecord | null {
32
+ switch (item?.type) {
33
+ case "command_execution":
34
+ return {
35
+ type: "assistant",
36
+ message: {
37
+ content: [
38
+ {
39
+ type: "tool_use",
40
+ name: "Bash",
41
+ input: { command: item.command || "" },
42
+ },
43
+ ],
44
+ },
45
+ _display: `Bash(${truncate(item.command)})`,
46
+ };
47
+ case "file_change": {
48
+ const changes = item.changes || [];
49
+ const paths = changes.map((change) => change.path).join(", ");
50
+ return {
51
+ type: "assistant",
52
+ message: {
53
+ content: [
54
+ {
55
+ type: "tool_use",
56
+ name: "Edit",
57
+ input: { file_path: paths },
58
+ },
59
+ ],
60
+ },
61
+ _display: `Edit(${truncate(paths)})`,
62
+ };
63
+ }
64
+ case "mcp_tool_call":
65
+ return {
66
+ type: "assistant",
67
+ message: {
68
+ content: [
69
+ {
70
+ type: "tool_use",
71
+ name: item.server_name || "mcp",
72
+ input: item.arguments || {},
73
+ },
74
+ ],
75
+ },
76
+ _display: `MCP(${item.server_name || "?"})`,
77
+ };
78
+ case "agent_message":
79
+ return {
80
+ type: "content_block_start",
81
+ content_block: { type: "text" },
82
+ _display: "Generating...",
83
+ };
84
+ case "reasoning":
85
+ return {
86
+ type: "content_block_start",
87
+ content_block: { type: "thinking" },
88
+ _display: "Thinking...",
89
+ };
90
+ default:
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export function normalizeItemComplete(item: CodexItem): UnknownRecord | null {
96
+ switch (item?.type) {
97
+ case "command_execution":
98
+ return {
99
+ type: "tool_result",
100
+ tool_name: "Bash",
101
+ content: item.output || `exit_code=${item.exit_code || 0}`,
102
+ _display: `Bash: exit=${item.exit_code || 0}`,
103
+ };
104
+ case "file_change": {
105
+ const changes = item.changes || [];
106
+ const summary = changes
107
+ .map((change) => `${change.kind}: ${change.path}`)
108
+ .join(", ");
109
+ return {
110
+ type: "tool_result",
111
+ tool_name: "Edit",
112
+ content: summary || "file changed",
113
+ _display: `Edit: ${truncate(summary)}`,
114
+ };
115
+ }
116
+ case "mcp_tool_call": {
117
+ const output = item.result || item.error || "";
118
+ return {
119
+ type: "tool_result",
120
+ tool_name: item.server_name || "mcp",
121
+ content: output,
122
+ _display: `MCP: ${truncate(output)}`,
123
+ };
124
+ }
125
+ case "agent_message": {
126
+ const text = item.text || "";
127
+ return {
128
+ type: "assistant",
129
+ message: {
130
+ content: [
131
+ {
132
+ type: "text",
133
+ text,
134
+ },
135
+ ],
136
+ },
137
+ _display: `Message: ${truncate(text)}`,
138
+ };
139
+ }
140
+ default:
141
+ return null;
142
+ }
143
+ }
144
+
145
+ export function normalizeTurnCompleted(
146
+ event: CodexTurnEvent,
147
+ sessionId: string,
148
+ ): UnknownRecord {
149
+ const usage = event?.usage || {};
150
+ return {
151
+ type: "result",
152
+ subtype: "success",
153
+ session_id: sessionId,
154
+ usage: {
155
+ input_tokens: usage.input_tokens || 0,
156
+ output_tokens: usage.output_tokens || 0,
157
+ cache_read_input_tokens: usage.cached_input_tokens || 0,
158
+ cache_creation_input_tokens: 0,
159
+ },
160
+ };
161
+ }
162
+
163
+ export function normalizeTurnFailed(
164
+ event: CodexTurnEvent,
165
+ sessionId: string,
166
+ ): UnknownRecord {
167
+ return {
168
+ type: "result",
169
+ subtype: "error",
170
+ session_id: sessionId,
171
+ is_error: true,
172
+ result: event?.error?.message || "Turn failed",
173
+ };
174
+ }
175
+
176
+ export function formatTurnCompletedLog(event: CodexTurnEvent): string {
177
+ const usage = event?.usage || {};
178
+ const parts = ["Result: success"];
179
+ if (usage.input_tokens || usage.output_tokens) {
180
+ parts.push(`${usage.input_tokens || 0}in/${usage.output_tokens || 0}out`);
181
+ if (usage.cached_input_tokens) {
182
+ parts.push(`cached=${usage.cached_input_tokens}`);
183
+ }
184
+ }
185
+ return parts.join(", ");
186
+ }
187
+
188
+ export function formatTurnFailedLog(event: CodexTurnEvent): string {
189
+ return `Failed: ${event?.error?.message || "unknown error"}`;
190
+ }
@@ -0,0 +1,126 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ formatTurnCompletedLog,
6
+ formatTurnFailedLog,
7
+ normalizeItemStart,
8
+ normalizeItemComplete,
9
+ normalizeTurnCompleted,
10
+ normalizeTurnFailed,
11
+ } from './codex-normalizer.mjs';
12
+
13
+ test('normalizeItemStart: command_execution -> Bash tool_use event', () => {
14
+ const event = normalizeItemStart({
15
+ type: 'command_execution',
16
+ command: 'npm test',
17
+ });
18
+
19
+ assert.equal(event.type, 'assistant');
20
+ assert.equal(event.message.content[0].type, 'tool_use');
21
+ assert.equal(event.message.content[0].name, 'Bash');
22
+ assert.deepEqual(event.message.content[0].input, { command: 'npm test' });
23
+ assert.equal(event._display, 'Bash(npm test)');
24
+ });
25
+
26
+ test('normalizeItemStart: Task-style subagent event keeps Task tool name', () => {
27
+ const event = normalizeItemStart({
28
+ type: 'mcp_tool_call',
29
+ server_name: 'Task',
30
+ arguments: { subagent_type: 'Explore', description: 'scan repo' },
31
+ });
32
+
33
+ assert.equal(event.type, 'assistant');
34
+ assert.equal(event.message.content[0].type, 'tool_use');
35
+ assert.equal(event.message.content[0].name, 'Task');
36
+ });
37
+
38
+ test('normalizeItemStart: unknown item type returns null', () => {
39
+ const event = normalizeItemStart({ type: 'unknown_type' });
40
+ assert.equal(event, null);
41
+ });
42
+
43
+ test('normalizeItemComplete: command_execution uses output if present', () => {
44
+ const event = normalizeItemComplete({
45
+ type: 'command_execution',
46
+ output: 'ok',
47
+ exit_code: 0,
48
+ });
49
+
50
+ assert.equal(event.type, 'tool_result');
51
+ assert.equal(event.tool_name, 'Bash');
52
+ assert.equal(event.content, 'ok');
53
+ });
54
+
55
+ test('normalizeItemComplete: command_execution falls back to exit code', () => {
56
+ const event = normalizeItemComplete({
57
+ type: 'command_execution',
58
+ output: '',
59
+ exit_code: 7,
60
+ });
61
+
62
+ assert.equal(event.content, 'exit_code=7');
63
+ });
64
+
65
+ test('normalizeItemComplete: Task result keeps tool_name Task', () => {
66
+ const event = normalizeItemComplete({
67
+ type: 'mcp_tool_call',
68
+ server_name: 'Task',
69
+ result: 'done',
70
+ });
71
+
72
+ assert.equal(event.type, 'tool_result');
73
+ assert.equal(event.tool_name, 'Task');
74
+ assert.equal(event.content, 'done');
75
+ });
76
+
77
+ test('normalizeTurnCompleted maps usage fields to tornado format', () => {
78
+ const event = normalizeTurnCompleted(
79
+ {
80
+ usage: {
81
+ input_tokens: 120,
82
+ output_tokens: 30,
83
+ cached_input_tokens: 77,
84
+ },
85
+ },
86
+ 'thread-1',
87
+ );
88
+
89
+ assert.equal(event.type, 'result');
90
+ assert.equal(event.subtype, 'success');
91
+ assert.equal(event.session_id, 'thread-1');
92
+ assert.equal(event.usage.input_tokens, 120);
93
+ assert.equal(event.usage.output_tokens, 30);
94
+ assert.equal(event.usage.cache_read_input_tokens, 77);
95
+ assert.equal(event.usage.cache_creation_input_tokens, 0);
96
+ });
97
+
98
+ test('normalizeTurnFailed creates error result event', () => {
99
+ const event = normalizeTurnFailed(
100
+ { error: { message: 'boom' } },
101
+ 'thread-9',
102
+ );
103
+
104
+ assert.equal(event.type, 'result');
105
+ assert.equal(event.subtype, 'error');
106
+ assert.equal(event.session_id, 'thread-9');
107
+ assert.equal(event.is_error, true);
108
+ assert.equal(event.result, 'boom');
109
+ });
110
+
111
+ test('formatTurnCompletedLog includes token and cache info', () => {
112
+ const message = formatTurnCompletedLog({
113
+ usage: {
114
+ input_tokens: 10,
115
+ output_tokens: 4,
116
+ cached_input_tokens: 7,
117
+ },
118
+ });
119
+
120
+ assert.equal(message, 'Result: success, 10in/4out, cached=7');
121
+ });
122
+
123
+ test('formatTurnFailedLog falls back to unknown error', () => {
124
+ const message = formatTurnFailedLog({});
125
+ assert.equal(message, 'Failed: unknown error');
126
+ });
@@ -0,0 +1,3 @@
1
+ import { runAdapterFromArgv } from "./agent-runner.mjs";
2
+ import { createCodexAdapter } from "./codex-adapter.mjs";
3
+ await runAdapterFromArgv(createCodexAdapter());
@@ -0,0 +1,4 @@
1
+ import { runAdapterFromArgv } from "./agent-runner.mjs";
2
+ import { createCodexAdapter } from "./codex-adapter.mjs";
3
+
4
+ await runAdapterFromArgv(createCodexAdapter());
@@ -0,0 +1,8 @@
1
+ export function writeJsonl(event, stream = process.stdout) {
2
+ stream.write(`${JSON.stringify(event)}\n`);
3
+ }
4
+ export function createLogger(tag, stream = process.stderr) {
5
+ return function log(message) {
6
+ stream.write(`[${tag}] ${message}\n`);
7
+ };
8
+ }
@@ -0,0 +1,12 @@
1
+ export function writeJsonl(event: unknown, stream: NodeJS.WritableStream = process.stdout): void {
2
+ stream.write(`${JSON.stringify(event)}\n`);
3
+ }
4
+
5
+ export function createLogger(
6
+ tag: string,
7
+ stream: NodeJS.WritableStream = process.stderr,
8
+ ): (message: string) => void {
9
+ return function log(message: string): void {
10
+ stream.write(`[${tag}] ${message}\n`);
11
+ };
12
+ }
@@ -0,0 +1,34 @@
1
+ // Background stdin watcher - reads user input and writes to interrupt file
2
+ // Spawned by the main process with stdio: ['inherit', 'pipe', 'inherit']
3
+ import { createInterface } from "readline";
4
+ import { appendFileSync, mkdirSync, writeFileSync } from "fs";
5
+
6
+ const INTERRUPT_FILE = ".tornado/interrupt.txt";
7
+
8
+ try {
9
+ mkdirSync(".tornado", { recursive: true });
10
+ } catch {}
11
+ // Clear stale interrupt
12
+ try {
13
+ writeFileSync(INTERRUPT_FILE, "", "utf-8");
14
+ } catch {}
15
+
16
+ const rl = createInterface({
17
+ input: process.stdin,
18
+ output: process.stderr,
19
+ terminal: true,
20
+ prompt: "",
21
+ });
22
+
23
+ rl.on("line", (line) => {
24
+ const trimmed = line.trim();
25
+ if (trimmed) {
26
+ appendFileSync(INTERRUPT_FILE, trimmed + "\n", "utf-8");
27
+ process.stderr.write(`\x1b[33m[USER] Queued: ${trimmed}\x1b[0m\n`);
28
+ }
29
+ });
30
+
31
+ rl.on("close", () => process.exit(0));
32
+
33
+ // Exit when parent dies
34
+ process.on("disconnect", () => process.exit(0));