axexec 1.0.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/LICENSE +21 -0
- package/README.md +196 -0
- package/bin/axexec +17 -0
- package/dist/agents/claude-code/adapter.d.ts +6 -0
- package/dist/agents/claude-code/adapter.js +71 -0
- package/dist/agents/claude-code/parse-event.d.ts +16 -0
- package/dist/agents/claude-code/parse-event.js +185 -0
- package/dist/agents/claude-code/types.d.ts +180 -0
- package/dist/agents/claude-code/types.js +112 -0
- package/dist/agents/codex/adapter.d.ts +6 -0
- package/dist/agents/codex/adapter.js +62 -0
- package/dist/agents/codex/item-parsers.d.ts +17 -0
- package/dist/agents/codex/item-parsers.js +126 -0
- package/dist/agents/codex/parse-event.d.ts +38 -0
- package/dist/agents/codex/parse-event.js +141 -0
- package/dist/agents/codex/types.d.ts +407 -0
- package/dist/agents/codex/types.js +188 -0
- package/dist/agents/copilot/adapter.d.ts +11 -0
- package/dist/agents/copilot/adapter.js +81 -0
- package/dist/agents/copilot/parse-event.d.ts +20 -0
- package/dist/agents/copilot/parse-event.js +137 -0
- package/dist/agents/copilot/stream-session.d.ts +10 -0
- package/dist/agents/copilot/stream-session.js +120 -0
- package/dist/agents/copilot/tail-file.d.ts +11 -0
- package/dist/agents/copilot/tail-file.js +52 -0
- package/dist/agents/copilot/transform-event.d.ts +19 -0
- package/dist/agents/copilot/transform-event.js +100 -0
- package/dist/agents/copilot/types.d.ts +320 -0
- package/dist/agents/copilot/types.js +220 -0
- package/dist/agents/copilot/watch-session.d.ts +26 -0
- package/dist/agents/copilot/watch-session.js +186 -0
- package/dist/agents/gemini/adapter.d.ts +6 -0
- package/dist/agents/gemini/adapter.js +78 -0
- package/dist/agents/gemini/parse-event.d.ts +18 -0
- package/dist/agents/gemini/parse-event.js +144 -0
- package/dist/agents/gemini/types.d.ts +162 -0
- package/dist/agents/gemini/types.js +103 -0
- package/dist/agents/opencode/adapter.d.ts +16 -0
- package/dist/agents/opencode/adapter.js +142 -0
- package/dist/agents/opencode/cleanup-session.d.ts +17 -0
- package/dist/agents/opencode/cleanup-session.js +41 -0
- package/dist/agents/opencode/create-session-start-event.d.ts +18 -0
- package/dist/agents/opencode/create-session-start-event.js +24 -0
- package/dist/agents/opencode/detect-empty-session.d.ts +25 -0
- package/dist/agents/opencode/detect-empty-session.js +42 -0
- package/dist/agents/opencode/map-error-to-event.d.ts +10 -0
- package/dist/agents/opencode/map-error-to-event.js +55 -0
- package/dist/agents/opencode/parse-message-part.d.ts +24 -0
- package/dist/agents/opencode/parse-message-part.js +112 -0
- package/dist/agents/opencode/parse-sse-event.d.ts +23 -0
- package/dist/agents/opencode/parse-sse-event.js +125 -0
- package/dist/agents/opencode/process-sse-events.d.ts +24 -0
- package/dist/agents/opencode/process-sse-events.js +66 -0
- package/dist/agents/opencode/server-types.d.ts +509 -0
- package/dist/agents/opencode/server-types.js +293 -0
- package/dist/agents/opencode/session-api.d.ts +56 -0
- package/dist/agents/opencode/session-api.js +151 -0
- package/dist/agents/opencode/spawn-server.d.ts +29 -0
- package/dist/agents/opencode/spawn-server.js +95 -0
- package/dist/agents/opencode/sse-client.d.ts +33 -0
- package/dist/agents/opencode/sse-client.js +145 -0
- package/dist/agents/registry.d.ts +15 -0
- package/dist/agents/registry.js +24 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +119 -0
- package/dist/credentials/get-credential-environment.d.ts +13 -0
- package/dist/credentials/get-credential-environment.js +46 -0
- package/dist/credentials/install-credentials.d.ts +27 -0
- package/dist/credentials/install-credentials.js +102 -0
- package/dist/credentials/save-json-file.d.ts +11 -0
- package/dist/credentials/save-json-file.js +21 -0
- package/dist/credentials/types.d.ts +24 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/write-agent-credentials.d.ts +36 -0
- package/dist/credentials/write-agent-credentials.js +91 -0
- package/dist/determine-session-success.d.ts +15 -0
- package/dist/determine-session-success.js +25 -0
- package/dist/format-event-tsv.d.ts +23 -0
- package/dist/format-event-tsv.js +136 -0
- package/dist/parse-credentials.d.ts +13 -0
- package/dist/parse-credentials.js +63 -0
- package/dist/parse-iso-timestamp.d.ts +21 -0
- package/dist/parse-iso-timestamp.js +37 -0
- package/dist/resolve-binary.d.ts +29 -0
- package/dist/resolve-binary.js +46 -0
- package/dist/resolve-output-mode.d.ts +39 -0
- package/dist/resolve-output-mode.js +39 -0
- package/dist/run-agent.d.ts +32 -0
- package/dist/run-agent.js +146 -0
- package/dist/stream-agent.d.ts +20 -0
- package/dist/stream-agent.js +207 -0
- package/dist/types/adapter.d.ts +101 -0
- package/dist/types/adapter.js +14 -0
- package/dist/types/events.d.ts +82 -0
- package/dist/types/events.js +13 -0
- package/dist/types/options.d.ts +41 -0
- package/dist/types/options.js +4 -0
- package/dist/validate-agent.d.ts +20 -0
- package/dist/validate-agent.js +50 -0
- package/dist/write-event.d.ts +23 -0
- package/dist/write-event.js +43 -0
- package/package.json +79 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode adapter implementation (server mode).
|
|
3
|
+
*
|
|
4
|
+
* Implements the {@link StreamableAdapter} interface for OpenCode using
|
|
5
|
+
* server mode with SSE for richer event streaming.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Spawn `opencode serve --port 0`
|
|
9
|
+
* 2. Wait for "listening on http://..." message
|
|
10
|
+
* 3. Connect to SSE endpoint `/event`
|
|
11
|
+
* 4. Create session via `POST /session`
|
|
12
|
+
* 5. Send prompt via `POST /session/:id/prompt_async`
|
|
13
|
+
* 6. Parse SSE events and yield AxexecEvents
|
|
14
|
+
* 7. Clean up server on completion
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode adapter implementation (server mode).
|
|
3
|
+
*
|
|
4
|
+
* Implements the {@link StreamableAdapter} interface for OpenCode using
|
|
5
|
+
* server mode with SSE for richer event streaming.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Spawn `opencode serve --port 0`
|
|
9
|
+
* 2. Wait for "listening on http://..." message
|
|
10
|
+
* 3. Connect to SSE endpoint `/event`
|
|
11
|
+
* 4. Create session via `POST /session`
|
|
12
|
+
* 5. Send prompt via `POST /session/:id/prompt_async`
|
|
13
|
+
* 6. Parse SSE events and yield AxexecEvents
|
|
14
|
+
* 7. Clean up server on completion
|
|
15
|
+
*/
|
|
16
|
+
import { registerAdapter } from "../registry.js";
|
|
17
|
+
import { cleanupSession } from "./cleanup-session.js";
|
|
18
|
+
import { createSessionStartEvent } from "./create-session-start-event.js";
|
|
19
|
+
import { determineSessionSuccess } from "../../determine-session-success.js";
|
|
20
|
+
import { resolveBinary } from "../../resolve-binary.js";
|
|
21
|
+
import { mapErrorToEvent } from "./map-error-to-event.js";
|
|
22
|
+
import { processSSEEvents } from "./process-sse-events.js";
|
|
23
|
+
import { createSession, sendPrompt } from "./session-api.js";
|
|
24
|
+
import { spawnServer } from "./spawn-server.js";
|
|
25
|
+
import { SSEConnectionError, streamSSE } from "./sse-client.js";
|
|
26
|
+
const OPENCODE_INFO = {
|
|
27
|
+
id: "opencode",
|
|
28
|
+
name: "OpenCode",
|
|
29
|
+
package: "opencode-ai",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Streams events from OpenCode server mode.
|
|
33
|
+
*/
|
|
34
|
+
async function* streamSession(options) {
|
|
35
|
+
const cwd = options.cwd ?? process.cwd();
|
|
36
|
+
const abortController = new AbortController();
|
|
37
|
+
const { signal } = abortController;
|
|
38
|
+
let serverProcess;
|
|
39
|
+
let serverUrl;
|
|
40
|
+
let sessionId;
|
|
41
|
+
let sessionComplete = false;
|
|
42
|
+
let sessionStartTime;
|
|
43
|
+
let sseReader;
|
|
44
|
+
// Forward termination signals
|
|
45
|
+
const forwardSignal = () => {
|
|
46
|
+
abortController.abort();
|
|
47
|
+
};
|
|
48
|
+
process.on("SIGINT", forwardSignal);
|
|
49
|
+
process.on("SIGTERM", forwardSignal);
|
|
50
|
+
try {
|
|
51
|
+
// 1. Spawn server
|
|
52
|
+
const server = await spawnServer({ cwd, signal, env: options.configEnv });
|
|
53
|
+
serverProcess = server.process;
|
|
54
|
+
serverUrl = server.url;
|
|
55
|
+
// 2. Connect to SSE and start streaming
|
|
56
|
+
const sseGenerator = streamSSE({
|
|
57
|
+
baseUrl: serverUrl,
|
|
58
|
+
directory: cwd,
|
|
59
|
+
signal,
|
|
60
|
+
onReader: (reader) => {
|
|
61
|
+
sseReader = reader;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// 3. Wait for server.connected event
|
|
65
|
+
const firstEvent = await sseGenerator.next();
|
|
66
|
+
if (firstEvent.done) {
|
|
67
|
+
throw new SSEConnectionError("SSE stream ended before server.connected");
|
|
68
|
+
}
|
|
69
|
+
if (firstEvent.value.type !== "server.connected") {
|
|
70
|
+
throw new SSEConnectionError(`Expected server.connected as first event, got: ${firstEvent.value.type}`);
|
|
71
|
+
}
|
|
72
|
+
// 4. Create session and send prompt
|
|
73
|
+
const session = await createSession(serverUrl, cwd);
|
|
74
|
+
sessionId = session.id;
|
|
75
|
+
sessionStartTime = Date.now();
|
|
76
|
+
await sendPrompt(serverUrl, sessionId, options.prompt, cwd, options.model);
|
|
77
|
+
// 5. Process SSE events
|
|
78
|
+
const eventProcessor = processSSEEvents(sseGenerator, {
|
|
79
|
+
serverUrl,
|
|
80
|
+
sessionId,
|
|
81
|
+
sessionStartTime,
|
|
82
|
+
requestedModel: options.model,
|
|
83
|
+
cwd,
|
|
84
|
+
signal,
|
|
85
|
+
onComplete: () => {
|
|
86
|
+
sessionComplete = true;
|
|
87
|
+
},
|
|
88
|
+
onAbort: () => {
|
|
89
|
+
sseReader?.cancel().catch(() => { });
|
|
90
|
+
abortController.abort();
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
for await (const event of eventProcessor) {
|
|
94
|
+
yield event;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errorEvent = mapErrorToEvent(error);
|
|
99
|
+
if (errorEvent) {
|
|
100
|
+
// Emit session.start if we have a session but haven't started streaming
|
|
101
|
+
if (sessionId) {
|
|
102
|
+
yield createSessionStartEvent({
|
|
103
|
+
sessionId,
|
|
104
|
+
timestamp: sessionStartTime ?? Date.now(),
|
|
105
|
+
modelInfo: undefined,
|
|
106
|
+
requestedModel: options.model,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
yield errorEvent;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
process.off("SIGINT", forwardSignal);
|
|
117
|
+
process.off("SIGTERM", forwardSignal);
|
|
118
|
+
abortController.abort();
|
|
119
|
+
await cleanupSession(serverProcess, serverUrl, sessionId, sessionComplete, sseReader, cwd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Stub parser - not used in server mode */
|
|
123
|
+
const noopParser = () => undefined;
|
|
124
|
+
/** OpenCode adapter (server mode) */
|
|
125
|
+
const openCodeAdapter = {
|
|
126
|
+
info: () => OPENCODE_INFO,
|
|
127
|
+
prepareCommand: () => ({
|
|
128
|
+
bin: resolveBinary({
|
|
129
|
+
name: "opencode",
|
|
130
|
+
command: "opencode",
|
|
131
|
+
environmentVariable: "AXEXEC_OPENCODE_PATH",
|
|
132
|
+
installHint: "npm install -g opencode-ai",
|
|
133
|
+
}),
|
|
134
|
+
args: ["serve", "--port", "0"],
|
|
135
|
+
env: {},
|
|
136
|
+
}),
|
|
137
|
+
createParser: () => noopParser,
|
|
138
|
+
isSuccess: determineSessionSuccess,
|
|
139
|
+
streamSession,
|
|
140
|
+
};
|
|
141
|
+
// Self-register on import
|
|
142
|
+
registerAdapter(openCodeAdapter);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cleanup utilities for OpenCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* Handles orderly shutdown of SSE connection, session abort, and server process.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChildProcess } from "node:child_process";
|
|
7
|
+
/**
|
|
8
|
+
* Cleans up session resources in the correct order.
|
|
9
|
+
*
|
|
10
|
+
* Order matters:
|
|
11
|
+
* 1. Cancel SSE reader (unblocks pending reads)
|
|
12
|
+
* 2. Abort session via API (needs server alive)
|
|
13
|
+
* 3. Dispose server instance via API (needs server alive)
|
|
14
|
+
* 4. Kill server process
|
|
15
|
+
*/
|
|
16
|
+
declare function cleanupSession(serverProcess: ChildProcess | undefined, serverUrl: string | undefined, sessionId: string | undefined, sessionComplete: boolean, sseReader: ReadableStreamDefaultReader<Uint8Array> | undefined, cwd: string): Promise<void>;
|
|
17
|
+
export { cleanupSession };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cleanup utilities for OpenCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* Handles orderly shutdown of SSE connection, session abort, and server process.
|
|
5
|
+
*/
|
|
6
|
+
import { abortSession, disposeInstance } from "./session-api.js";
|
|
7
|
+
/**
|
|
8
|
+
* Cleans up session resources in the correct order.
|
|
9
|
+
*
|
|
10
|
+
* Order matters:
|
|
11
|
+
* 1. Cancel SSE reader (unblocks pending reads)
|
|
12
|
+
* 2. Abort session via API (needs server alive)
|
|
13
|
+
* 3. Dispose server instance via API (needs server alive)
|
|
14
|
+
* 4. Kill server process
|
|
15
|
+
*/
|
|
16
|
+
async function cleanupSession(serverProcess, serverUrl, sessionId, sessionComplete, sseReader, cwd) {
|
|
17
|
+
// Cancel SSE reader directly to unblock any pending reads
|
|
18
|
+
sseReader?.cancel().catch(() => { });
|
|
19
|
+
// Try to cleanly abort session and dispose instance BEFORE killing server
|
|
20
|
+
if (serverUrl && sessionId && !sessionComplete) {
|
|
21
|
+
await abortSession(serverUrl, sessionId, cwd);
|
|
22
|
+
}
|
|
23
|
+
if (serverUrl) {
|
|
24
|
+
await disposeInstance(serverUrl, cwd);
|
|
25
|
+
}
|
|
26
|
+
// Kill server gracefully with SIGTERM, then force with SIGKILL after timeout
|
|
27
|
+
if (!serverProcess || serverProcess.killed)
|
|
28
|
+
return;
|
|
29
|
+
serverProcess.kill("SIGTERM");
|
|
30
|
+
// Wait for process to exit (with timeout for SIGKILL escalation)
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
const killTimeout = setTimeout(() => {
|
|
33
|
+
serverProcess.kill("SIGKILL");
|
|
34
|
+
}, 500);
|
|
35
|
+
serverProcess.once("exit", () => {
|
|
36
|
+
clearTimeout(killTimeout);
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export { cleanupSession };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a session.start event for OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* Extracted to reduce complexity in the main adapter.
|
|
5
|
+
*/
|
|
6
|
+
import type { AxexecEvent } from "../../types/events.js";
|
|
7
|
+
import type { MessageModelInfo } from "./session-api.js";
|
|
8
|
+
interface SessionStartEventOptions {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
modelInfo: MessageModelInfo | undefined;
|
|
12
|
+
requestedModel: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates a session.start event with model and provider info.
|
|
16
|
+
*/
|
|
17
|
+
declare function createSessionStartEvent(options: SessionStartEventOptions): AxexecEvent;
|
|
18
|
+
export { createSessionStartEvent };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a session.start event for OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* Extracted to reduce complexity in the main adapter.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Creates a session.start event with model and provider info.
|
|
8
|
+
*/
|
|
9
|
+
function createSessionStartEvent(options) {
|
|
10
|
+
const { sessionId, timestamp, modelInfo, requestedModel } = options;
|
|
11
|
+
const model = modelInfo
|
|
12
|
+
? `${modelInfo.providerID}/${modelInfo.modelID}`
|
|
13
|
+
: (requestedModel ?? "unknown");
|
|
14
|
+
const provider = modelInfo?.providerID;
|
|
15
|
+
return {
|
|
16
|
+
type: "session.start",
|
|
17
|
+
sessionId,
|
|
18
|
+
agent: "opencode",
|
|
19
|
+
model,
|
|
20
|
+
provider,
|
|
21
|
+
timestamp,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export { createSessionStartEvent };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty session detection for OpenCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode server mode silently returns session.idle when models don't exist,
|
|
5
|
+
* credentials are missing, or other configuration issues prevent execution.
|
|
6
|
+
* This module detects these "empty sessions" and converts them to errors.
|
|
7
|
+
*/
|
|
8
|
+
import type { AxexecEvent } from "../../types/events.js";
|
|
9
|
+
/** Tracks whether meaningful content was generated during a session */
|
|
10
|
+
interface ContentTracker {
|
|
11
|
+
hasContent: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Creates a new content tracker */
|
|
14
|
+
declare function createContentTracker(): ContentTracker;
|
|
15
|
+
/** Updates tracker when content events are observed */
|
|
16
|
+
declare function trackContentEvent(tracker: ContentTracker, event: AxexecEvent): void;
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a session.complete event should be converted to session.error.
|
|
19
|
+
*
|
|
20
|
+
* Returns an error event if the session completed without generating any
|
|
21
|
+
* content, indicating a silent failure (e.g., model not found, missing
|
|
22
|
+
* credentials, unsupported configuration).
|
|
23
|
+
*/
|
|
24
|
+
declare function checkEmptySession(event: AxexecEvent, tracker: ContentTracker): AxexecEvent | undefined;
|
|
25
|
+
export { checkEmptySession, createContentTracker, trackContentEvent };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty session detection for OpenCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode server mode silently returns session.idle when models don't exist,
|
|
5
|
+
* credentials are missing, or other configuration issues prevent execution.
|
|
6
|
+
* This module detects these "empty sessions" and converts them to errors.
|
|
7
|
+
*/
|
|
8
|
+
/** Creates a new content tracker */
|
|
9
|
+
function createContentTracker() {
|
|
10
|
+
return { hasContent: false };
|
|
11
|
+
}
|
|
12
|
+
/** Updates tracker when content events are observed */
|
|
13
|
+
function trackContentEvent(tracker, event) {
|
|
14
|
+
if (event.type === "agent.message" ||
|
|
15
|
+
event.type === "agent.reasoning" ||
|
|
16
|
+
event.type === "tool.call" ||
|
|
17
|
+
event.type === "tool.result") {
|
|
18
|
+
tracker.hasContent = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a session.complete event should be converted to session.error.
|
|
23
|
+
*
|
|
24
|
+
* Returns an error event if the session completed without generating any
|
|
25
|
+
* content, indicating a silent failure (e.g., model not found, missing
|
|
26
|
+
* credentials, unsupported configuration).
|
|
27
|
+
*/
|
|
28
|
+
function checkEmptySession(event, tracker) {
|
|
29
|
+
if (event.type !== "session.complete")
|
|
30
|
+
return undefined;
|
|
31
|
+
if (tracker.hasContent)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
type: "session.error",
|
|
35
|
+
code: "EMPTY_SESSION",
|
|
36
|
+
message: "Session completed without generating any output. " +
|
|
37
|
+
"This may indicate an invalid model, missing credentials, " +
|
|
38
|
+
"or an unsupported configuration.",
|
|
39
|
+
timestamp: event.timestamp,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export { checkEmptySession, createContentTracker, trackContentEvent };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps OpenCode adapter errors to session.error events.
|
|
3
|
+
*/
|
|
4
|
+
import type { AxexecEvent } from "../../types/events.js";
|
|
5
|
+
/**
|
|
6
|
+
* Maps a caught error to a session.error event, or returns undefined
|
|
7
|
+
* if the error should be rethrown.
|
|
8
|
+
*/
|
|
9
|
+
declare function mapErrorToEvent(error: unknown): AxexecEvent | undefined;
|
|
10
|
+
export { mapErrorToEvent };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps OpenCode adapter errors to session.error events.
|
|
3
|
+
*/
|
|
4
|
+
import { PromptSubmitError, SessionCreateError } from "./session-api.js";
|
|
5
|
+
import { ServerStartError } from "./spawn-server.js";
|
|
6
|
+
import { SSEConnectionError } from "./sse-client.js";
|
|
7
|
+
/**
|
|
8
|
+
* Maps a caught error to a session.error event, or returns undefined
|
|
9
|
+
* if the error should be rethrown.
|
|
10
|
+
*/
|
|
11
|
+
function mapErrorToEvent(error) {
|
|
12
|
+
if (error instanceof ServerStartError) {
|
|
13
|
+
return {
|
|
14
|
+
type: "session.error",
|
|
15
|
+
code: "SERVER_START_ERROR",
|
|
16
|
+
message: error.message,
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof SessionCreateError) {
|
|
21
|
+
return {
|
|
22
|
+
type: "session.error",
|
|
23
|
+
code: "SESSION_CREATE_ERROR",
|
|
24
|
+
message: error.message,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (error instanceof PromptSubmitError) {
|
|
29
|
+
return {
|
|
30
|
+
type: "session.error",
|
|
31
|
+
code: "PROMPT_SUBMIT_ERROR",
|
|
32
|
+
message: error.message,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof SSEConnectionError) {
|
|
37
|
+
return {
|
|
38
|
+
type: "session.error",
|
|
39
|
+
code: "SSE_CONNECTION_ERROR",
|
|
40
|
+
message: error.message,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
45
|
+
return {
|
|
46
|
+
type: "session.error",
|
|
47
|
+
code: "ABORTED",
|
|
48
|
+
message: "Session aborted",
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Unknown error - should be rethrown
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export { mapErrorToEvent };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message part parsing for OpenCode SSE events.
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing of message.part.updated events into AxexecEvents.
|
|
5
|
+
*/
|
|
6
|
+
import type { AxexecEvent } from "../../types/events.js";
|
|
7
|
+
import type { MessagePartUpdatedEvent } from "./server-types.js";
|
|
8
|
+
/** Aggregated session statistics */
|
|
9
|
+
interface AggregatedStats {
|
|
10
|
+
costUsd: number;
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
reasoningTokens: number;
|
|
14
|
+
cacheReadTokens: number;
|
|
15
|
+
cacheWriteTokens: number;
|
|
16
|
+
}
|
|
17
|
+
/** Tool call tracking state */
|
|
18
|
+
interface ToolCallState {
|
|
19
|
+
emittedToolCalls: Set<string>;
|
|
20
|
+
}
|
|
21
|
+
/** Parses a message.part.updated event */
|
|
22
|
+
declare function parseMessagePartEvent(event: MessagePartUpdatedEvent, toolState: ToolCallState, stats: AggregatedStats): AxexecEvent[];
|
|
23
|
+
export { parseMessagePartEvent };
|
|
24
|
+
export type { AggregatedStats, ToolCallState };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message part parsing for OpenCode SSE events.
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing of message.part.updated events into AxexecEvents.
|
|
5
|
+
*/
|
|
6
|
+
/** Parses a message.part.updated event */
|
|
7
|
+
function parseMessagePartEvent(event, toolState, stats) {
|
|
8
|
+
const part = event.properties.part;
|
|
9
|
+
switch (part.type) {
|
|
10
|
+
case "text": {
|
|
11
|
+
return parseTextPart(part);
|
|
12
|
+
}
|
|
13
|
+
case "reasoning": {
|
|
14
|
+
return parseReasoningPart(part);
|
|
15
|
+
}
|
|
16
|
+
case "tool": {
|
|
17
|
+
return parseToolPart(part, toolState);
|
|
18
|
+
}
|
|
19
|
+
case "step-finish": {
|
|
20
|
+
return parseStepFinishPart(part, stats);
|
|
21
|
+
}
|
|
22
|
+
case "step-start": {
|
|
23
|
+
// Ignore step-start events
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
default: {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Parses text part into agent.message */
|
|
32
|
+
function parseTextPart(part) {
|
|
33
|
+
// Only emit when text has content and time.end is set (completed)
|
|
34
|
+
if (!part.text || !part.time?.end) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
type: "agent.message",
|
|
40
|
+
content: part.text,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
/** Parses reasoning part into agent.reasoning */
|
|
46
|
+
function parseReasoningPart(part) {
|
|
47
|
+
// Only emit when text has content and time.end is set (completed)
|
|
48
|
+
if (!part.text || !part.time?.end) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
type: "agent.reasoning",
|
|
54
|
+
content: part.text,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/** Parses tool part into tool.call and/or tool.result */
|
|
60
|
+
function parseToolPart(part, toolState) {
|
|
61
|
+
const events = [];
|
|
62
|
+
const state = part.state;
|
|
63
|
+
// Emit tool.call when transitioning to running (or completed/error if we missed running)
|
|
64
|
+
if (!toolState.emittedToolCalls.has(part.callID) &&
|
|
65
|
+
(state.status === "running" ||
|
|
66
|
+
state.status === "completed" ||
|
|
67
|
+
state.status === "error")) {
|
|
68
|
+
toolState.emittedToolCalls.add(part.callID);
|
|
69
|
+
events.push({
|
|
70
|
+
type: "tool.call",
|
|
71
|
+
callId: part.callID,
|
|
72
|
+
tool: part.tool,
|
|
73
|
+
input: state.input,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Emit tool.result when completed or error
|
|
78
|
+
if (state.status === "completed") {
|
|
79
|
+
events.push({
|
|
80
|
+
type: "tool.result",
|
|
81
|
+
callId: part.callID,
|
|
82
|
+
tool: part.tool,
|
|
83
|
+
output: state.output,
|
|
84
|
+
success: true,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (state.status === "error") {
|
|
89
|
+
events.push({
|
|
90
|
+
type: "tool.result",
|
|
91
|
+
callId: part.callID,
|
|
92
|
+
tool: part.tool,
|
|
93
|
+
output: state.error,
|
|
94
|
+
success: false,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return events;
|
|
99
|
+
}
|
|
100
|
+
/** Parses step-finish part to aggregate stats */
|
|
101
|
+
function parseStepFinishPart(part, stats) {
|
|
102
|
+
// Aggregate stats
|
|
103
|
+
stats.costUsd += part.cost;
|
|
104
|
+
stats.inputTokens += part.tokens.input;
|
|
105
|
+
stats.outputTokens += part.tokens.output;
|
|
106
|
+
stats.reasoningTokens += part.tokens.reasoning;
|
|
107
|
+
stats.cacheReadTokens += part.tokens.cache.read;
|
|
108
|
+
stats.cacheWriteTokens += part.tokens.cache.write;
|
|
109
|
+
// step-finish doesn't produce any AxexecEvents directly
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
export { parseMessagePartEvent };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE event parser for OpenCode server mode.
|
|
3
|
+
*
|
|
4
|
+
* Transforms OpenCode SSE events into normalized {@link AxexecEvent} types.
|
|
5
|
+
* This parser is stateful and tracks:
|
|
6
|
+
* - Cumulative cost/token stats from step-finish parts
|
|
7
|
+
* - Tool call state to emit tool.call/tool.result at appropriate times
|
|
8
|
+
* - Session start time for duration calculation
|
|
9
|
+
*/
|
|
10
|
+
import type { AxexecEvent } from "../../types/events.js";
|
|
11
|
+
import type { OpenCodeSSEEvent } from "./server-types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Creates a parser function for SSE events.
|
|
14
|
+
*
|
|
15
|
+
* The parser maintains state across calls to:
|
|
16
|
+
* - Aggregate stats from step-finish events
|
|
17
|
+
* - Track which tool calls have been emitted
|
|
18
|
+
* - Calculate session duration
|
|
19
|
+
*
|
|
20
|
+
* @param startTimestamp - Optional timestamp when session started (for duration calculation)
|
|
21
|
+
*/
|
|
22
|
+
declare function createSSEEventParser(startTimestamp?: number): (event: OpenCodeSSEEvent) => AxexecEvent[];
|
|
23
|
+
export { createSSEEventParser };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE event parser for OpenCode server mode.
|
|
3
|
+
*
|
|
4
|
+
* Transforms OpenCode SSE events into normalized {@link AxexecEvent} types.
|
|
5
|
+
* This parser is stateful and tracks:
|
|
6
|
+
* - Cumulative cost/token stats from step-finish parts
|
|
7
|
+
* - Tool call state to emit tool.call/tool.result at appropriate times
|
|
8
|
+
* - Session start time for duration calculation
|
|
9
|
+
*/
|
|
10
|
+
import { isMessagePartUpdatedEvent, isPermissionUpdatedEvent, isSessionCreatedEvent, isSessionErrorEvent, isSessionIdleEvent, } from "./server-types.js";
|
|
11
|
+
import { parseMessagePartEvent } from "./parse-message-part.js";
|
|
12
|
+
/** Creates initial parser state */
|
|
13
|
+
function createParserState() {
|
|
14
|
+
return {
|
|
15
|
+
sessionId: undefined,
|
|
16
|
+
model: "unknown",
|
|
17
|
+
startTimestamp: undefined,
|
|
18
|
+
stats: {
|
|
19
|
+
costUsd: 0,
|
|
20
|
+
inputTokens: 0,
|
|
21
|
+
outputTokens: 0,
|
|
22
|
+
reasoningTokens: 0,
|
|
23
|
+
cacheReadTokens: 0,
|
|
24
|
+
cacheWriteTokens: 0,
|
|
25
|
+
},
|
|
26
|
+
toolState: {
|
|
27
|
+
emittedToolCalls: new Set(),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a parser function for SSE events.
|
|
33
|
+
*
|
|
34
|
+
* The parser maintains state across calls to:
|
|
35
|
+
* - Aggregate stats from step-finish events
|
|
36
|
+
* - Track which tool calls have been emitted
|
|
37
|
+
* - Calculate session duration
|
|
38
|
+
*
|
|
39
|
+
* @param startTimestamp - Optional timestamp when session started (for duration calculation)
|
|
40
|
+
*/
|
|
41
|
+
function createSSEEventParser(startTimestamp) {
|
|
42
|
+
const state = createParserState();
|
|
43
|
+
// Use provided start timestamp if available
|
|
44
|
+
if (startTimestamp !== undefined) {
|
|
45
|
+
state.startTimestamp = startTimestamp;
|
|
46
|
+
}
|
|
47
|
+
return (event) => {
|
|
48
|
+
return parseSSEEvent(event, state);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Parses an SSE event into zero or more AxexecEvents */
|
|
52
|
+
function parseSSEEvent(event, state) {
|
|
53
|
+
// Session lifecycle events
|
|
54
|
+
if (isSessionCreatedEvent(event)) {
|
|
55
|
+
// Skip if session.start was already emitted (startTimestamp was provided externally)
|
|
56
|
+
// This prevents duplicate session.start events when session is created via HTTP POST
|
|
57
|
+
if (state.startTimestamp !== undefined) {
|
|
58
|
+
// Just update sessionId without emitting
|
|
59
|
+
state.sessionId = event.properties.info.id;
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
state.sessionId = event.properties.info.id;
|
|
63
|
+
state.startTimestamp = Date.now();
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
type: "session.start",
|
|
67
|
+
sessionId: event.properties.info.id,
|
|
68
|
+
agent: "opencode",
|
|
69
|
+
model: state.model,
|
|
70
|
+
// Provider unknown in fallback path (session.created doesn't include it)
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
if (isSessionIdleEvent(event)) {
|
|
76
|
+
// Session completed - emit session.complete with aggregated stats
|
|
77
|
+
const durationMs = state.startTimestamp
|
|
78
|
+
? Date.now() - state.startTimestamp
|
|
79
|
+
: 0;
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
type: "session.complete",
|
|
83
|
+
stats: {
|
|
84
|
+
durationMs,
|
|
85
|
+
costUsd: state.stats.costUsd || undefined,
|
|
86
|
+
inputTokens: state.stats.inputTokens || undefined,
|
|
87
|
+
outputTokens: state.stats.outputTokens || undefined,
|
|
88
|
+
},
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
if (isSessionErrorEvent(event)) {
|
|
94
|
+
const errorName = event.properties.error?.name ?? "UNKNOWN_ERROR";
|
|
95
|
+
const errorMessage = event.properties.error?.data?.message || "Unknown error occurred";
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
type: "session.error",
|
|
99
|
+
code: errorName,
|
|
100
|
+
message: errorMessage,
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
// Permission events - fail with error as per plan
|
|
106
|
+
if (isPermissionUpdatedEvent(event)) {
|
|
107
|
+
const tool = event.properties.tool ?? "unknown";
|
|
108
|
+
const { title } = event.properties;
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
type: "session.error",
|
|
112
|
+
code: "PERMISSION_REQUIRED",
|
|
113
|
+
message: `OpenCode requires permission: ${tool}. ${title}. Pre-configure permissions in OpenCode settings.`,
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
// Message part events
|
|
119
|
+
if (isMessagePartUpdatedEvent(event)) {
|
|
120
|
+
return parseMessagePartEvent(event, state.toolState, state.stats);
|
|
121
|
+
}
|
|
122
|
+
// Ignore server.connected, server.heartbeat, session.status, session.updated
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
export { createSSEEventParser };
|