@twelvehart/orcats 0.0.0-seed.0 → 0.2.3
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 +48 -0
- package/NOTICE +5 -0
- package/README.md +399 -0
- package/bin/orcats +4 -0
- package/dist/backends/acp-client.d.ts +74 -0
- package/dist/backends/acp-client.d.ts.map +1 -0
- package/dist/backends/acp-run.d.ts +31 -0
- package/dist/backends/acp-run.d.ts.map +1 -0
- package/dist/backends/claude-run.d.ts +35 -0
- package/dist/backends/claude-run.d.ts.map +1 -0
- package/dist/backends/claude-stream-json.d.ts +15 -0
- package/dist/backends/claude-stream-json.d.ts.map +1 -0
- package/dist/backends/codex-jsonl.d.ts +34 -0
- package/dist/backends/codex-jsonl.d.ts.map +1 -0
- package/dist/backends/codex-run.d.ts +40 -0
- package/dist/backends/codex-run.d.ts.map +1 -0
- package/dist/backends/codex.d.ts +5 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/conversation-config.d.ts +12 -0
- package/dist/backends/conversation-config.d.ts.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/opencode-run.d.ts +48 -0
- package/dist/backends/opencode-run.d.ts.map +1 -0
- package/dist/backends/opencode-sse.d.ts +35 -0
- package/dist/backends/opencode-sse.d.ts.map +1 -0
- package/dist/backends/pi-rpc.d.ts +21 -0
- package/dist/backends/pi-rpc.d.ts.map +1 -0
- package/dist/backends/pi-run.d.ts +21 -0
- package/dist/backends/pi-run.d.ts.map +1 -0
- package/dist/backends/select.d.ts +17 -0
- package/dist/backends/select.d.ts.map +1 -0
- package/dist/backends/subprocess-run.d.ts +68 -0
- package/dist/backends/subprocess-run.d.ts.map +1 -0
- package/dist/backends/types.d.ts +17 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/unsupported.d.ts +9 -0
- package/dist/backends/unsupported.d.ts.map +1 -0
- package/dist/baseline/index.d.ts +56 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/embedded.d.ts +7 -0
- package/dist/cli/embedded.d.ts.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/conversation/ask-user.d.ts +17 -0
- package/dist/conversation/ask-user.d.ts.map +1 -0
- package/dist/conversation/conversation.d.ts +46 -0
- package/dist/conversation/conversation.d.ts.map +1 -0
- package/dist/conversation/harness.d.ts +14 -0
- package/dist/conversation/harness.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/queue.d.ts +15 -0
- package/dist/conversation/queue.d.ts.map +1 -0
- package/dist/flow/accessors.d.ts +13 -0
- package/dist/flow/accessors.d.ts.map +1 -0
- package/dist/flow/approvals.d.ts +6 -0
- package/dist/flow/approvals.d.ts.map +1 -0
- package/dist/flow/args.d.ts +4 -0
- package/dist/flow/args.d.ts.map +1 -0
- package/dist/flow/context.d.ts +36 -0
- package/dist/flow/context.d.ts.map +1 -0
- package/dist/flow/index.d.ts +5 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loop/builder/index.d.ts +17 -0
- package/dist/loop/builder/index.d.ts.map +1 -0
- package/dist/loop/builder/types.d.ts +97 -0
- package/dist/loop/builder/types.d.ts.map +1 -0
- package/dist/loop/context/compaction.d.ts +38 -0
- package/dist/loop/context/compaction.d.ts.map +1 -0
- package/dist/loop/context/index.d.ts +4 -0
- package/dist/loop/context/index.d.ts.map +1 -0
- package/dist/loop/context/offload.d.ts +34 -0
- package/dist/loop/context/offload.d.ts.map +1 -0
- package/dist/loop/context/types.d.ts +13 -0
- package/dist/loop/context/types.d.ts.map +1 -0
- package/dist/loop/engine/bridge.d.ts +21 -0
- package/dist/loop/engine/bridge.d.ts.map +1 -0
- package/dist/loop/engine/index.d.ts +73 -0
- package/dist/loop/engine/index.d.ts.map +1 -0
- package/dist/loop/execution.d.ts +68 -0
- package/dist/loop/execution.d.ts.map +1 -0
- package/dist/loop/fanout.d.ts +148 -0
- package/dist/loop/fanout.d.ts.map +1 -0
- package/dist/loop/firing.d.ts +54 -0
- package/dist/loop/firing.d.ts.map +1 -0
- package/dist/loop/graph/analyze.d.ts +8 -0
- package/dist/loop/graph/analyze.d.ts.map +1 -0
- package/dist/loop/graph/index.d.ts +3 -0
- package/dist/loop/graph/index.d.ts.map +1 -0
- package/dist/loop/graph/types.d.ts +35 -0
- package/dist/loop/graph/types.d.ts.map +1 -0
- package/dist/loop/index.d.ts +7 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/io/index.d.ts +4 -0
- package/dist/loop/io/index.d.ts.map +1 -0
- package/dist/loop/io/linear.d.ts +142 -0
- package/dist/loop/io/linear.d.ts.map +1 -0
- package/dist/loop/io/sink.d.ts +47 -0
- package/dist/loop/io/sink.d.ts.map +1 -0
- package/dist/loop/io/source.d.ts +64 -0
- package/dist/loop/io/source.d.ts.map +1 -0
- package/dist/loop/serve.d.ts +97 -0
- package/dist/loop/serve.d.ts.map +1 -0
- package/dist/loop/state/index.d.ts +5 -0
- package/dist/loop/state/index.d.ts.map +1 -0
- package/dist/loop/state/manifest.d.ts +36 -0
- package/dist/loop/state/manifest.d.ts.map +1 -0
- package/dist/loop/state/port.d.ts +33 -0
- package/dist/loop/state/port.d.ts.map +1 -0
- package/dist/loop/state/snapshot.d.ts +15 -0
- package/dist/loop/state/snapshot.d.ts.map +1 -0
- package/dist/loop/state/sqlite.d.ts +31 -0
- package/dist/loop/state/sqlite.d.ts.map +1 -0
- package/dist/loop/termination-contract.d.ts +28 -0
- package/dist/loop/termination-contract.d.ts.map +1 -0
- package/dist/loop/termination.d.ts +33 -0
- package/dist/loop/termination.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +30 -0
- package/dist/model/backend-config.d.ts.map +1 -0
- package/dist/model/brand.d.ts +9 -0
- package/dist/model/brand.d.ts.map +1 -0
- package/dist/model/errors.d.ts +11 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/index.d.ts +8 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/json-schema.d.ts +4 -0
- package/dist/model/json-schema.d.ts.map +1 -0
- package/dist/model/result.d.ts +3 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/schemas.d.ts +435 -0
- package/dist/model/schemas.d.ts.map +1 -0
- package/dist/model/structured.d.ts +9 -0
- package/dist/model/structured.d.ts.map +1 -0
- package/dist/monitor/index.d.ts +139 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/persistent.d.ts +23 -0
- package/dist/plan/persistent.d.ts.map +1 -0
- package/dist/plan/verdict.d.ts +9 -0
- package/dist/plan/verdict.d.ts.map +1 -0
- package/dist/review/index.d.ts +4 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/loop.d.ts +117 -0
- package/dist/review/loop.d.ts.map +1 -0
- package/dist/review/reviewers.d.ts +13 -0
- package/dist/review/reviewers.d.ts.map +1 -0
- package/dist/review/tool.d.ts +20 -0
- package/dist/review/tool.d.ts.map +1 -0
- package/dist/run-output/index.d.ts +112 -0
- package/dist/run-output/index.d.ts.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/typecheck.d.ts +23 -0
- package/dist/runner/typecheck.d.ts.map +1 -0
- package/dist/test-utils/fakes.d.ts +76 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fixtures.d.ts +4 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +4 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/tier2.d.ts +8 -0
- package/dist/test-utils/tier2.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +11 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/git.d.ts +10 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/github.d.ts +13 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/linear.d.ts +117 -0
- package/dist/tools/linear.d.ts.map +1 -0
- package/dist/tools/process.d.ts +40 -0
- package/dist/tools/process.d.ts.map +1 -0
- package/dist/tools/terminal.d.ts +23 -0
- package/dist/tools/terminal.d.ts.map +1 -0
- package/package.json +89 -9
- package/src/backends/acp-client.ts +324 -0
- package/src/backends/acp-run.ts +636 -0
- package/src/backends/claude-run.ts +226 -0
- package/src/backends/claude-stream-json.ts +273 -0
- package/src/backends/codex-jsonl.ts +339 -0
- package/src/backends/codex-run.ts +177 -0
- package/src/backends/codex.ts +84 -0
- package/src/backends/conversation-config.ts +26 -0
- package/src/backends/index.ts +13 -0
- package/src/backends/opencode-run.ts +432 -0
- package/src/backends/opencode-sse.ts +360 -0
- package/src/backends/pi-rpc.ts +242 -0
- package/src/backends/pi-run.ts +154 -0
- package/src/backends/select.ts +77 -0
- package/src/backends/subprocess-run.ts +305 -0
- package/src/backends/types.ts +29 -0
- package/src/backends/unsupported.ts +48 -0
- package/src/baseline/index.ts +437 -0
- package/src/cli/args.ts +109 -0
- package/src/cli/embedded.ts +206 -0
- package/src/cli/main.ts +332 -0
- package/src/cli/version.ts +1 -0
- package/src/conversation/ask-user.ts +105 -0
- package/src/conversation/conversation.ts +109 -0
- package/src/conversation/harness.ts +39 -0
- package/src/conversation/index.ts +4 -0
- package/src/conversation/queue.ts +86 -0
- package/src/flow/accessors.ts +43 -0
- package/src/flow/approvals.ts +10 -0
- package/src/flow/args.ts +26 -0
- package/src/flow/context.ts +106 -0
- package/src/flow/index.ts +4 -0
- package/src/index.ts +17 -0
- package/src/loop/builder/index.ts +346 -0
- package/src/loop/builder/types.ts +131 -0
- package/src/loop/context/compaction.ts +175 -0
- package/src/loop/context/index.ts +3 -0
- package/src/loop/context/offload.ts +79 -0
- package/src/loop/context/types.ts +17 -0
- package/src/loop/engine/bridge.ts +61 -0
- package/src/loop/engine/index.ts +166 -0
- package/src/loop/execution.ts +453 -0
- package/src/loop/fanout.ts +444 -0
- package/src/loop/firing.ts +188 -0
- package/src/loop/graph/analyze.ts +128 -0
- package/src/loop/graph/index.ts +2 -0
- package/src/loop/graph/types.ts +45 -0
- package/src/loop/index.ts +11 -0
- package/src/loop/io/index.ts +3 -0
- package/src/loop/io/linear.ts +714 -0
- package/src/loop/io/sink.ts +170 -0
- package/src/loop/io/source.ts +255 -0
- package/src/loop/serve.ts +307 -0
- package/src/loop/state/index.ts +4 -0
- package/src/loop/state/manifest.ts +56 -0
- package/src/loop/state/port.ts +43 -0
- package/src/loop/state/snapshot.ts +129 -0
- package/src/loop/state/sqlite.ts +265 -0
- package/src/loop/termination-contract.ts +89 -0
- package/src/loop/termination.ts +156 -0
- package/src/model/backend-config.ts +34 -0
- package/src/model/brand.ts +11 -0
- package/src/model/errors.ts +28 -0
- package/src/model/index.ts +7 -0
- package/src/model/json-schema.ts +14 -0
- package/src/model/result.ts +28 -0
- package/src/model/schemas.ts +218 -0
- package/src/model/structured.ts +26 -0
- package/src/monitor/index.ts +367 -0
- package/src/plan/index.ts +2 -0
- package/src/plan/persistent.ts +98 -0
- package/src/plan/verdict.ts +3 -0
- package/src/review/index.ts +3 -0
- package/src/review/loop.ts +365 -0
- package/src/review/reviewers.ts +92 -0
- package/src/review/tool.ts +41 -0
- package/src/run-output/index.ts +319 -0
- package/src/runner/index.ts +1 -0
- package/src/runner/typecheck.ts +72 -0
- package/src/test-utils/fakes.ts +260 -0
- package/src/test-utils/fixtures.ts +33 -0
- package/src/test-utils/index.ts +3 -0
- package/src/test-utils/tier2.ts +12 -0
- package/src/tools/fs.ts +44 -0
- package/src/tools/git.ts +34 -0
- package/src/tools/github.ts +25 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/linear.ts +428 -0
- package/src/tools/process.ts +156 -0
- package/src/tools/terminal.ts +116 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { backendFailed, sessionId, type Usage } from "../model/index.ts";
|
|
2
|
+
import {
|
|
3
|
+
collectConversation,
|
|
4
|
+
StreamConversation,
|
|
5
|
+
type ConversationCapture
|
|
6
|
+
} from "../conversation/index.ts";
|
|
7
|
+
|
|
8
|
+
interface OpenCodeSseEvent {
|
|
9
|
+
readonly type?: string;
|
|
10
|
+
readonly properties?: {
|
|
11
|
+
readonly sessionID?: string;
|
|
12
|
+
readonly field?: string;
|
|
13
|
+
readonly delta?: string;
|
|
14
|
+
readonly error?: unknown;
|
|
15
|
+
readonly part?: {
|
|
16
|
+
readonly id?: string;
|
|
17
|
+
readonly sessionID?: string;
|
|
18
|
+
readonly type?: string;
|
|
19
|
+
readonly tool?: string;
|
|
20
|
+
readonly state?: {
|
|
21
|
+
readonly status?: string;
|
|
22
|
+
readonly input?: unknown;
|
|
23
|
+
readonly output?: unknown;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
readonly info?: {
|
|
27
|
+
readonly role?: string;
|
|
28
|
+
readonly sessionID?: string;
|
|
29
|
+
readonly structured?: unknown;
|
|
30
|
+
readonly error?: unknown;
|
|
31
|
+
readonly tokens?: {
|
|
32
|
+
readonly input?: number;
|
|
33
|
+
readonly output?: number;
|
|
34
|
+
readonly reasoning?: number;
|
|
35
|
+
readonly cache?: {
|
|
36
|
+
readonly read?: number;
|
|
37
|
+
readonly write?: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface OpenCodeTokens {
|
|
45
|
+
readonly input?: number;
|
|
46
|
+
readonly output?: number;
|
|
47
|
+
readonly reasoning?: number;
|
|
48
|
+
readonly cache?: {
|
|
49
|
+
readonly read?: number;
|
|
50
|
+
readonly write?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type OpenCodeParseResult = ConversationCapture<"opencode">;
|
|
55
|
+
|
|
56
|
+
export interface OpenCodeServerProcess {
|
|
57
|
+
readonly url: string;
|
|
58
|
+
/** Preemptive `Authorization` header value the server requires (Basic auth from
|
|
59
|
+
* the `OPENCODE_SERVER_PASSWORD` the spawn set). Absent for transports that
|
|
60
|
+
* don't authenticate. */
|
|
61
|
+
readonly authHeader?: string;
|
|
62
|
+
stop(signal?: NodeJS.Signals): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OpenCodeServerManager {
|
|
66
|
+
get(): Promise<OpenCodeServerProcess>;
|
|
67
|
+
shutdown(signal?: NodeJS.Signals): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function collectOpenCodeSse(lines: readonly string[]): Promise<OpenCodeParseResult> {
|
|
71
|
+
return await collectConversation({
|
|
72
|
+
backend: "opencode",
|
|
73
|
+
consume: async (conversation) => {
|
|
74
|
+
await consumeOpenCodeSse(lines, conversation);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface OpenCodeSseState {
|
|
80
|
+
output: string;
|
|
81
|
+
session: string;
|
|
82
|
+
structured: unknown;
|
|
83
|
+
usage: Usage | undefined;
|
|
84
|
+
error: unknown;
|
|
85
|
+
assistantSeen: boolean;
|
|
86
|
+
startedTools: Set<string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function newOpenCodeSseState(): OpenCodeSseState {
|
|
90
|
+
return {
|
|
91
|
+
output: "",
|
|
92
|
+
session: "",
|
|
93
|
+
structured: undefined,
|
|
94
|
+
usage: undefined,
|
|
95
|
+
error: undefined,
|
|
96
|
+
assistantSeen: false,
|
|
97
|
+
startedTools: new Set()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface OpenCodeLineOutcome {
|
|
102
|
+
/** Terminal event consumed (`session.idle`/`session.error` settled the turn). */
|
|
103
|
+
readonly terminal: boolean;
|
|
104
|
+
/** Line belonged to this turn's session — only relevant lines count as
|
|
105
|
+
* activity for the inactivity watchdog, so foreign-session traffic can't
|
|
106
|
+
* mask a dead turn. */
|
|
107
|
+
readonly relevant: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const IGNORED: OpenCodeLineOutcome = { terminal: false, relevant: false };
|
|
111
|
+
const CONSUMED: OpenCodeLineOutcome = { terminal: false, relevant: true };
|
|
112
|
+
const TERMINAL: OpenCodeLineOutcome = { terminal: true, relevant: true };
|
|
113
|
+
|
|
114
|
+
/** opencode wraps errors as `{name, data: {message}}` and sometimes as a bare
|
|
115
|
+
* `{message}` (Scala `OpencodeEvent.errorMessage`); fall back to the raw JSON so
|
|
116
|
+
* the failure is never silently empty. */
|
|
117
|
+
export function extractOpenCodeErrorMessage(error: unknown): string {
|
|
118
|
+
if (typeof error === "object" && error !== null) {
|
|
119
|
+
const shape = error as { readonly message?: unknown; readonly data?: { readonly message?: unknown } };
|
|
120
|
+
if (typeof shape.message === "string") {
|
|
121
|
+
return shape.message;
|
|
122
|
+
}
|
|
123
|
+
if (typeof shape.data?.message === "string") {
|
|
124
|
+
return shape.data.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return JSON.stringify(error);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Session id owner of an event, per the oracle's per-type extraction rules
|
|
131
|
+
* (Scala `OpencodeEvent.sessionId`). */
|
|
132
|
+
function eventSession(event: OpenCodeSseEvent): string | undefined {
|
|
133
|
+
switch (event.type) {
|
|
134
|
+
case "message.part.delta":
|
|
135
|
+
case "message.part.updated":
|
|
136
|
+
return event.properties?.sessionID ?? event.properties?.part?.sessionID;
|
|
137
|
+
case "message.updated":
|
|
138
|
+
return event.properties?.sessionID ?? event.properties?.info?.sessionID;
|
|
139
|
+
default:
|
|
140
|
+
return event.properties?.sessionID;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Apply one SSE line to the conversation, mutating `state`. The single source
|
|
145
|
+
* of truth for OpenCode event → conversation mapping, shared by the batch parser
|
|
146
|
+
* and the live incremental consumer so they can't drift.
|
|
147
|
+
*
|
|
148
|
+
* When `session` is given, the `/event` firehose is scoped to it: events owned
|
|
149
|
+
* by another session are dropped (opencode spawns child sessions for subagent
|
|
150
|
+
* work, and their idle/error/updated frames must not settle or pollute this
|
|
151
|
+
* turn). An event with a *missing* session id is treated as ours — terminal
|
|
152
|
+
* frames from a protocol deviation settle the turn instead of hanging it
|
|
153
|
+
* (Scala `forThisSession`'s `forall`). */
|
|
154
|
+
async function applyOpenCodeSseLine(
|
|
155
|
+
raw: string,
|
|
156
|
+
conversation: StreamConversation<"opencode">,
|
|
157
|
+
state: OpenCodeSseState,
|
|
158
|
+
session?: string
|
|
159
|
+
): Promise<OpenCodeLineOutcome> {
|
|
160
|
+
const event = parseSseLine(raw);
|
|
161
|
+
if (!event) {
|
|
162
|
+
return IGNORED;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const owner = eventSession(event);
|
|
166
|
+
if (session !== undefined && owner !== undefined && owner !== session) {
|
|
167
|
+
return IGNORED;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (event.type === "message.part.delta" && event.properties?.field === "text") {
|
|
171
|
+
const delta = event.properties.delta ?? "";
|
|
172
|
+
state.output += delta;
|
|
173
|
+
await conversation.emit({ type: "assistant_text_delta", text: delta });
|
|
174
|
+
return CONSUMED;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (event.type === "message.part.delta" && event.properties?.field === "reasoning") {
|
|
178
|
+
await conversation.emit({
|
|
179
|
+
type: "assistant_thinking_delta",
|
|
180
|
+
text: event.properties.delta ?? ""
|
|
181
|
+
});
|
|
182
|
+
return CONSUMED;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (event.type === "message.part.updated") {
|
|
186
|
+
const part = event.properties?.part;
|
|
187
|
+
if (part?.type === "tool") {
|
|
188
|
+
const toolCallId = part.id ?? part.tool ?? "";
|
|
189
|
+
const status = part.state?.status;
|
|
190
|
+
if (status === "running" && !state.startedTools.has(toolCallId)) {
|
|
191
|
+
state.startedTools.add(toolCallId);
|
|
192
|
+
await conversation.emit({
|
|
193
|
+
type: "assistant_tool_call",
|
|
194
|
+
id: toolCallId,
|
|
195
|
+
name: part.tool ?? "",
|
|
196
|
+
input: part.state?.input
|
|
197
|
+
});
|
|
198
|
+
} else if (status === "completed" || status === "error") {
|
|
199
|
+
await conversation.emit({
|
|
200
|
+
type: "tool_result",
|
|
201
|
+
toolCallId,
|
|
202
|
+
output: part.state?.output ?? "",
|
|
203
|
+
isError: status === "error"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return CONSUMED;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (event.type === "message.updated") {
|
|
211
|
+
const info = event.properties?.info;
|
|
212
|
+
// opencode echoes the user message (`role:"user"`, no structured/tokens);
|
|
213
|
+
// a non-assistant update must not masquerade as — or wipe — the result.
|
|
214
|
+
if (info?.role !== "assistant") {
|
|
215
|
+
return CONSUMED;
|
|
216
|
+
}
|
|
217
|
+
state.assistantSeen = true;
|
|
218
|
+
state.session = info.sessionID ?? state.session;
|
|
219
|
+
state.structured = info.structured;
|
|
220
|
+
state.usage = normalizeUsage(info.tokens);
|
|
221
|
+
state.error = info.error;
|
|
222
|
+
if (state.structured !== undefined) {
|
|
223
|
+
state.output = JSON.stringify(state.structured);
|
|
224
|
+
}
|
|
225
|
+
await conversation.emit({ type: "assistant_turn_end" });
|
|
226
|
+
return CONSUMED;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (event.type === "session.error") {
|
|
230
|
+
const error = event.properties?.error;
|
|
231
|
+
conversation.fail(
|
|
232
|
+
backendFailed(
|
|
233
|
+
"opencode",
|
|
234
|
+
error === undefined ? "OpenCode session failed" : extractOpenCodeErrorMessage(error)
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
return TERMINAL;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (event.type === "session.idle") {
|
|
241
|
+
// Mirrors Scala `finishTurn`: an error-bearing assistant message means the
|
|
242
|
+
// agent failed even though the session idled "normally" — succeeding here
|
|
243
|
+
// would return garbage output and bury the actual failure.
|
|
244
|
+
if (state.error !== undefined) {
|
|
245
|
+
conversation.fail(backendFailed("opencode", extractOpenCodeErrorMessage(state.error)));
|
|
246
|
+
return TERMINAL;
|
|
247
|
+
}
|
|
248
|
+
if (!state.assistantSeen && state.output === "") {
|
|
249
|
+
conversation.fail(
|
|
250
|
+
backendFailed("opencode", "session went idle without an assistant message")
|
|
251
|
+
);
|
|
252
|
+
return TERMINAL;
|
|
253
|
+
}
|
|
254
|
+
conversation.succeed({
|
|
255
|
+
backend: "opencode",
|
|
256
|
+
sessionId: sessionId("opencode", state.session),
|
|
257
|
+
output: state.output,
|
|
258
|
+
...(state.structured === undefined ? {} : { structured: state.structured }),
|
|
259
|
+
...(state.usage === undefined ? {} : { usage: state.usage })
|
|
260
|
+
});
|
|
261
|
+
return TERMINAL;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return CONSUMED;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function consumeOpenCodeSse(
|
|
268
|
+
lines: readonly string[],
|
|
269
|
+
conversation: StreamConversation<"opencode">,
|
|
270
|
+
session?: string
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
const state = newOpenCodeSseState();
|
|
273
|
+
for (const raw of lines) {
|
|
274
|
+
if ((await applyOpenCodeSseLine(raw, conversation, state, session)).terminal) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface OpenCodeSseConsumer {
|
|
281
|
+
readonly completed: boolean;
|
|
282
|
+
/** Returns whether the line was relevant to this turn's session (activity
|
|
283
|
+
* signal for the inactivity watchdog). */
|
|
284
|
+
consume(raw: string): Promise<boolean>;
|
|
285
|
+
finish(): void;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Incremental consumer for the live driver: feeds SSE lines off the wire,
|
|
289
|
+
* scoped to `session`, and fails cleanly if the stream ends before a terminal
|
|
290
|
+
* `session.idle`/`error`. */
|
|
291
|
+
export function createOpenCodeSseConsumer(
|
|
292
|
+
conversation: StreamConversation<"opencode">,
|
|
293
|
+
session: string
|
|
294
|
+
): OpenCodeSseConsumer {
|
|
295
|
+
const state = newOpenCodeSseState();
|
|
296
|
+
let completed = false;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
get completed() {
|
|
300
|
+
return completed;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async consume(raw: string): Promise<boolean> {
|
|
304
|
+
if (completed) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const outcome = await applyOpenCodeSseLine(raw, conversation, state, session);
|
|
308
|
+
if (outcome.terminal) {
|
|
309
|
+
completed = true;
|
|
310
|
+
}
|
|
311
|
+
return outcome.relevant;
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
finish(): void {
|
|
315
|
+
if (completed) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
completed = true;
|
|
319
|
+
conversation.fail(backendFailed("opencode", "opencode stream ended before session.idle"));
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function createOpenCodeServerManager(args: {
|
|
325
|
+
start: () => Promise<OpenCodeServerProcess>;
|
|
326
|
+
}): OpenCodeServerManager {
|
|
327
|
+
let processPromise: Promise<OpenCodeServerProcess> | undefined;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
async get() {
|
|
331
|
+
processPromise ??= args.start();
|
|
332
|
+
return await processPromise;
|
|
333
|
+
},
|
|
334
|
+
async shutdown(signal = "SIGINT") {
|
|
335
|
+
const process = await processPromise;
|
|
336
|
+
processPromise = undefined;
|
|
337
|
+
await process?.stop(signal);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parseSseLine(raw: string): OpenCodeSseEvent | undefined {
|
|
343
|
+
const trimmed = raw.trim();
|
|
344
|
+
if (!trimmed.startsWith("data:")) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return JSON.parse(trimmed.slice("data:".length).trim()) as OpenCodeSseEvent;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizeUsage(tokens: OpenCodeTokens | undefined): Usage | undefined {
|
|
351
|
+
if (!tokens) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
input: (tokens.input ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
|
|
357
|
+
output: tokens.output ?? 0,
|
|
358
|
+
reasoning: tokens.reasoning ?? 0
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
backendFailed,
|
|
4
|
+
parseStructuredOutput,
|
|
5
|
+
sessionId,
|
|
6
|
+
structuredOutputValidationFailed,
|
|
7
|
+
type RuntimeError,
|
|
8
|
+
type Usage
|
|
9
|
+
} from "../model/index.ts";
|
|
10
|
+
import {
|
|
11
|
+
collectConversation,
|
|
12
|
+
StreamConversation,
|
|
13
|
+
type ConversationCapture
|
|
14
|
+
} from "../conversation/index.ts";
|
|
15
|
+
|
|
16
|
+
interface PiLine {
|
|
17
|
+
readonly type?: string;
|
|
18
|
+
readonly success?: boolean;
|
|
19
|
+
readonly error?: string;
|
|
20
|
+
readonly toolCallId?: string;
|
|
21
|
+
readonly toolName?: string;
|
|
22
|
+
readonly args?: unknown;
|
|
23
|
+
readonly isError?: boolean;
|
|
24
|
+
readonly result?: {
|
|
25
|
+
readonly content?: ReadonlyArray<{ readonly type?: string; readonly text?: string }>;
|
|
26
|
+
};
|
|
27
|
+
readonly message?: {
|
|
28
|
+
readonly content?: ReadonlyArray<{ readonly type?: string; readonly text?: string }>;
|
|
29
|
+
readonly usage?: {
|
|
30
|
+
readonly input?: number;
|
|
31
|
+
readonly output?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type PiParseResult = ConversationCapture<"pi">;
|
|
37
|
+
|
|
38
|
+
export function piRpcArgs(sessionDir: string): readonly string[] {
|
|
39
|
+
return ["--mode", "rpc", "--session-dir", sessionDir];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function piPromptCommand(message: string): string {
|
|
43
|
+
return JSON.stringify({ id: "orca-prompt", type: "prompt", message });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function collectPiRpc(
|
|
47
|
+
lines: readonly string[],
|
|
48
|
+
piSessionId = "pi-session"
|
|
49
|
+
): Promise<PiParseResult> {
|
|
50
|
+
return await collectConversation({
|
|
51
|
+
backend: "pi",
|
|
52
|
+
consume: async (conversation) => {
|
|
53
|
+
await consumePiRpc(lines, conversation, piSessionId);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PiRpcOptions<Output = unknown> {
|
|
59
|
+
readonly schema?: z.ZodType<Output>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface PiRpcState {
|
|
63
|
+
output: string;
|
|
64
|
+
usage: Usage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function newPiRpcState(): PiRpcState {
|
|
68
|
+
return { output: "", usage: { input: 0, output: 0 } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Apply one Pi RPC line to the conversation, mutating `state`. Returns `true`
|
|
72
|
+
* when terminal (`agent_end` succeeded / a failed `response` failed). Single
|
|
73
|
+
* source of truth shared by the batch parser and the live incremental consumer. */
|
|
74
|
+
async function applyPiRpcLine<Output>(
|
|
75
|
+
raw: string,
|
|
76
|
+
conversation: StreamConversation<"pi">,
|
|
77
|
+
state: PiRpcState,
|
|
78
|
+
piSessionId: string,
|
|
79
|
+
options: PiRpcOptions<Output>
|
|
80
|
+
): Promise<boolean> {
|
|
81
|
+
if (raw.trim() === "") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const line = JSON.parse(raw) as PiLine;
|
|
85
|
+
|
|
86
|
+
if (line.type === "response" && line.success === false) {
|
|
87
|
+
const message = line.error ?? "Pi command failed";
|
|
88
|
+
await conversation.emit({ type: "error", message });
|
|
89
|
+
conversation.fail(backendFailed("pi", message));
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (line.type === "tool_execution_start") {
|
|
94
|
+
await conversation.emit({
|
|
95
|
+
type: "assistant_tool_call",
|
|
96
|
+
id: line.toolCallId ?? "",
|
|
97
|
+
name: line.toolName ?? "",
|
|
98
|
+
input: line.args ?? {}
|
|
99
|
+
});
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (line.type === "tool_execution_end") {
|
|
104
|
+
await conversation.emit({
|
|
105
|
+
type: "tool_result",
|
|
106
|
+
toolCallId: line.toolCallId ?? "",
|
|
107
|
+
output: textContent(line.result?.content),
|
|
108
|
+
isError: line.isError ?? false
|
|
109
|
+
});
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (line.type === "message_end") {
|
|
114
|
+
state.output = textContent(line.message?.content);
|
|
115
|
+
state.usage = {
|
|
116
|
+
input: line.message?.usage?.input ?? 0,
|
|
117
|
+
output: line.message?.usage?.output ?? 0
|
|
118
|
+
};
|
|
119
|
+
if (state.output) {
|
|
120
|
+
await conversation.emit({ type: "assistant_text_delta", text: state.output });
|
|
121
|
+
await conversation.emit({ type: "assistant_turn_end" });
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (line.type === "agent_end") {
|
|
127
|
+
const structured = parsePiStructuredOutput(options.schema, state.output);
|
|
128
|
+
if (structured.type === "failed") {
|
|
129
|
+
conversation.fail(structured.error);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
conversation.succeed({
|
|
133
|
+
backend: "pi",
|
|
134
|
+
sessionId: sessionId("pi", piSessionId),
|
|
135
|
+
output: state.output,
|
|
136
|
+
usage: state.usage,
|
|
137
|
+
...(structured.value === undefined ? {} : { structured: structured.value })
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function consumePiRpc(
|
|
146
|
+
lines: readonly string[],
|
|
147
|
+
conversation: StreamConversation<"pi">,
|
|
148
|
+
piSessionId = "pi-session"
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const state = newPiRpcState();
|
|
151
|
+
for (const raw of lines) {
|
|
152
|
+
if (await applyPiRpcLine(raw, conversation, state, piSessionId, {})) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface PiRpcConsumer {
|
|
159
|
+
readonly signal: AbortSignal;
|
|
160
|
+
consume(raw: string): Promise<void>;
|
|
161
|
+
finish(): void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Incremental consumer for the live driver: tolerates malformed JSON, validates
|
|
165
|
+
* structured output against a supplied schema (Pi has no native schema flag, so
|
|
166
|
+
* this is post-hoc text validation), and fails cleanly if the stream ends before
|
|
167
|
+
* `agent_end`. */
|
|
168
|
+
export function createPiRpcConsumer<Output = unknown>(
|
|
169
|
+
conversation: StreamConversation<"pi">,
|
|
170
|
+
piSessionId: string,
|
|
171
|
+
options: PiRpcOptions<Output> = {}
|
|
172
|
+
): PiRpcConsumer {
|
|
173
|
+
const state = newPiRpcState();
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
get signal() {
|
|
178
|
+
return controller.signal;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async consume(raw: string): Promise<void> {
|
|
182
|
+
if (controller.signal.aborted) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let terminal: boolean;
|
|
186
|
+
try {
|
|
187
|
+
terminal = await applyPiRpcLine(raw, conversation, state, piSessionId, options);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
controller.abort();
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
conversation.fail(backendFailed("pi", `invalid pi RPC: ${message}`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (terminal) {
|
|
195
|
+
controller.abort();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
finish(): void {
|
|
200
|
+
if (controller.signal.aborted) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
controller.abort();
|
|
204
|
+
conversation.fail(backendFailed("pi", "pi stream ended before agent_end"));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parsePiStructuredOutput<Output>(
|
|
210
|
+
schema: z.ZodType<Output> | undefined,
|
|
211
|
+
output: string
|
|
212
|
+
):
|
|
213
|
+
| { readonly type: "success"; readonly value?: Output }
|
|
214
|
+
| { readonly type: "failed"; readonly error: RuntimeError } {
|
|
215
|
+
if (!schema) {
|
|
216
|
+
return { type: "success" };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let raw: unknown;
|
|
220
|
+
try {
|
|
221
|
+
raw = JSON.parse(output) as unknown;
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
type: "failed",
|
|
225
|
+
error: structuredOutputValidationFailed({
|
|
226
|
+
raw: output,
|
|
227
|
+
issues: ["Pi structured output was not valid JSON"]
|
|
228
|
+
})
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const parsed = parseStructuredOutput(schema, raw);
|
|
233
|
+
if (parsed.isErr()) {
|
|
234
|
+
return { type: "failed", error: parsed.error };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { type: "success", value: parsed.value.value };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function textContent(content: ReadonlyArray<{ readonly text?: string }> | undefined): string {
|
|
241
|
+
return content?.map((part) => part.text ?? "").join("") ?? "";
|
|
242
|
+
}
|