@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,154 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { z } from "zod";
|
|
5
|
+
import { composeBackendPrompt } from "./conversation-config.ts";
|
|
6
|
+
import { createPiRpcConsumer, piPromptCommand, piRpcArgs, type PiRpcOptions } from "./pi-rpc.ts";
|
|
7
|
+
import {
|
|
8
|
+
errorMessage,
|
|
9
|
+
runSubprocessConversation,
|
|
10
|
+
type SubprocessProcess,
|
|
11
|
+
type SubprocessSpawner
|
|
12
|
+
} from "./subprocess-run.ts";
|
|
13
|
+
import type { AutonomousRequest, LlmBackend } from "./types.ts";
|
|
14
|
+
import { StreamConversation } from "../conversation/index.ts";
|
|
15
|
+
import { backendFailed, type BackendConfig } from "../model/index.ts";
|
|
16
|
+
|
|
17
|
+
export type PiProcess = SubprocessProcess;
|
|
18
|
+
export type PiProcessSpawner = SubprocessSpawner;
|
|
19
|
+
|
|
20
|
+
export interface PiBackendOptions {
|
|
21
|
+
readonly command?: string;
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
24
|
+
readonly capacity?: number;
|
|
25
|
+
readonly spawnProcess?: PiProcessSpawner;
|
|
26
|
+
readonly config?: BackendConfig<"pi">;
|
|
27
|
+
readonly inactivityTimeoutMs?: number;
|
|
28
|
+
readonly wallClockTimeoutMs?: number;
|
|
29
|
+
/** Base directory for per-session Pi `--session-dir`s. Defaults to the OS temp dir. */
|
|
30
|
+
readonly sessionsDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ResolvedPiConfig<Output> {
|
|
34
|
+
model?: string;
|
|
35
|
+
systemPrompt?: string;
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
selfManagedGit?: boolean;
|
|
38
|
+
retryAttempts?: number;
|
|
39
|
+
schema?: z.ZodType<Output>;
|
|
40
|
+
resumeSessionId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Pi's read-only tool allow-list (Scala `PiArgs.ReadOnlyTools`). */
|
|
44
|
+
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"] as const;
|
|
45
|
+
|
|
46
|
+
/** Build the `pi --mode rpc` argv (Scala `PiArgs.rpc`): session dir, `--continue`
|
|
47
|
+
* on resume, model, and the read-only `--tools` gate. The prompt is sent as a
|
|
48
|
+
* stdin RPC command, not on argv. */
|
|
49
|
+
function piArgs<Output>(sessionDir: string, config: ResolvedPiConfig<Output>): readonly string[] {
|
|
50
|
+
return [
|
|
51
|
+
...piRpcArgs(sessionDir),
|
|
52
|
+
...(config.resumeSessionId ? ["--continue"] : []),
|
|
53
|
+
...(config.model ? ["--model", config.model] : []),
|
|
54
|
+
...(config.readOnly ? ["--tools", READ_ONLY_TOOLS.join(",")] : [])
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runPiConversation<Output>(
|
|
59
|
+
request: AutonomousRequest<Output, "pi">,
|
|
60
|
+
options: PiBackendOptions,
|
|
61
|
+
conversation: StreamConversation<"pi">,
|
|
62
|
+
setProcess: (process: PiProcess) => void
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const command = options.command ?? "pi";
|
|
65
|
+
const config = resolvePiConfig(request, options);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const piSessionId = config.resumeSessionId ?? crypto.randomUUID();
|
|
69
|
+
const sessionDir = join(options.sessionsDir ?? tmpdir(), `orca-pi-${piSessionId}`);
|
|
70
|
+
// Synchronous so the process spawns within the first microtask (matching
|
|
71
|
+
// codex/claude): a cancel right after start still finds the child to kill.
|
|
72
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const args = piArgs(sessionDir, config);
|
|
75
|
+
const consumerOptions: PiRpcOptions<Output> =
|
|
76
|
+
config.schema === undefined ? {} : { schema: config.schema };
|
|
77
|
+
|
|
78
|
+
await runSubprocessConversation({
|
|
79
|
+
backend: "pi",
|
|
80
|
+
command,
|
|
81
|
+
args,
|
|
82
|
+
conversation,
|
|
83
|
+
setProcess,
|
|
84
|
+
stdin: "pipe",
|
|
85
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
86
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
87
|
+
...(options.spawnProcess === undefined ? {} : { spawnProcess: options.spawnProcess }),
|
|
88
|
+
...(options.inactivityTimeoutMs === undefined ? {} : { inactivityTimeoutMs: options.inactivityTimeoutMs }),
|
|
89
|
+
...(options.wallClockTimeoutMs === undefined ? {} : { wallClockTimeoutMs: options.wallClockTimeoutMs }),
|
|
90
|
+
onStart: (process) => {
|
|
91
|
+
process.write?.(`${piPromptCommand(composeBackendPrompt(request.prompt, config))}\n`);
|
|
92
|
+
// Pi rpc keeps reading stdin for the next command; leave it open. The
|
|
93
|
+
// shared helper kills the process once `agent_end` settles the turn.
|
|
94
|
+
},
|
|
95
|
+
createConsumer: () => createPiRpcConsumer(conversation, piSessionId, consumerOptions)
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!conversation.signal.aborted) {
|
|
99
|
+
conversation.fail(backendFailed("pi", errorMessage(error)));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolvePiConfig<Output>(
|
|
105
|
+
request: AutonomousRequest<Output, "pi">,
|
|
106
|
+
options: PiBackendOptions
|
|
107
|
+
): ResolvedPiConfig<Output> {
|
|
108
|
+
const optionConfig = options.config;
|
|
109
|
+
const requestConfig = request.config;
|
|
110
|
+
const config: ResolvedPiConfig<Output> = {};
|
|
111
|
+
const model = requestConfig?.model ?? optionConfig?.model;
|
|
112
|
+
if (model !== undefined) config.model = model;
|
|
113
|
+
const systemPrompt = requestConfig?.systemPrompt ?? optionConfig?.systemPrompt;
|
|
114
|
+
if (systemPrompt !== undefined) config.systemPrompt = systemPrompt;
|
|
115
|
+
const readOnly = requestConfig?.readOnly ?? optionConfig?.readOnly;
|
|
116
|
+
if (readOnly !== undefined) config.readOnly = readOnly;
|
|
117
|
+
const selfManagedGit = requestConfig?.selfManagedGit ?? optionConfig?.selfManagedGit;
|
|
118
|
+
if (selfManagedGit !== undefined) config.selfManagedGit = selfManagedGit;
|
|
119
|
+
const retryAttempts = requestConfig?.retry?.attempts ?? optionConfig?.retry?.attempts;
|
|
120
|
+
if (retryAttempts !== undefined) config.retryAttempts = retryAttempts;
|
|
121
|
+
const schema =
|
|
122
|
+
requestConfig?.structuredOutput?.schema ??
|
|
123
|
+
request.schema ??
|
|
124
|
+
(optionConfig?.structuredOutput?.schema as z.ZodType<Output> | undefined);
|
|
125
|
+
if (schema !== undefined) config.schema = schema;
|
|
126
|
+
if (requestConfig?.resumeSessionId !== undefined)
|
|
127
|
+
config.resumeSessionId = String(requestConfig.resumeSessionId);
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function pi(options: PiBackendOptions = {}): LlmBackend<"pi"> {
|
|
132
|
+
return {
|
|
133
|
+
tag: "pi",
|
|
134
|
+
autonomous<Output = unknown>(request: AutonomousRequest<Output, "pi">) {
|
|
135
|
+
let child: PiProcess | undefined;
|
|
136
|
+
const conversation = new StreamConversation({
|
|
137
|
+
backend: "pi",
|
|
138
|
+
capacity: options.capacity ?? 256,
|
|
139
|
+
canAskUser: false,
|
|
140
|
+
onCancel: () => {
|
|
141
|
+
child?.kill("SIGTERM");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
queueMicrotask(() => {
|
|
146
|
+
void runPiConversation(request, options, conversation, (process) => {
|
|
147
|
+
child = process;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return conversation;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BackendTagSchema, type BackendConfig, type BackendTag } from "../model/index.ts";
|
|
2
|
+
import { claude } from "./claude-run.ts";
|
|
3
|
+
import { codex } from "./codex.ts";
|
|
4
|
+
import { opencode } from "./opencode-run.ts";
|
|
5
|
+
import { pi } from "./pi-run.ts";
|
|
6
|
+
import type { LlmBackend } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export type PortableBackendConfig = Omit<BackendConfig, "resumeSessionId" | "structuredOutput">;
|
|
9
|
+
|
|
10
|
+
export interface SelectBackendOptions {
|
|
11
|
+
readonly default: BackendTag;
|
|
12
|
+
readonly config?: PortableBackendConfig;
|
|
13
|
+
readonly perBackend?: Partial<Record<BackendTag, PortableBackendConfig>>;
|
|
14
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SelectedBackend {
|
|
18
|
+
readonly tag: BackendTag;
|
|
19
|
+
readonly backend: LlmBackend;
|
|
20
|
+
readonly model?: string;
|
|
21
|
+
readonly shutdown?: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function selectBackend(options: SelectBackendOptions): SelectedBackend {
|
|
25
|
+
const env = options.env ?? process.env;
|
|
26
|
+
const raw = env.ORCA_BACKEND;
|
|
27
|
+
const candidate = raw === undefined || raw === "" ? options.default : raw;
|
|
28
|
+
const parsed = BackendTagSchema.safeParse(candidate);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unsupported backend "${candidate}" (expected one of: ${BackendTagSchema.options.join(", ")})`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tag = parsed.data;
|
|
36
|
+
const mergedConfig: PortableBackendConfig = {
|
|
37
|
+
...(options.config ?? {}),
|
|
38
|
+
...(options.perBackend?.[tag] ?? {})
|
|
39
|
+
};
|
|
40
|
+
const model = env.ORCA_BACKEND_MODEL === undefined || env.ORCA_BACKEND_MODEL === ""
|
|
41
|
+
? mergedConfig.model
|
|
42
|
+
: env.ORCA_BACKEND_MODEL;
|
|
43
|
+
const config: PortableBackendConfig = {
|
|
44
|
+
...mergedConfig,
|
|
45
|
+
...(model === undefined ? {} : { model })
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
switch (tag) {
|
|
49
|
+
case "claude":
|
|
50
|
+
return {
|
|
51
|
+
tag,
|
|
52
|
+
backend: claude({ config }),
|
|
53
|
+
...(model === undefined ? {} : { model })
|
|
54
|
+
};
|
|
55
|
+
case "codex":
|
|
56
|
+
return {
|
|
57
|
+
tag,
|
|
58
|
+
backend: codex({ config }),
|
|
59
|
+
...(model === undefined ? {} : { model })
|
|
60
|
+
};
|
|
61
|
+
case "opencode": {
|
|
62
|
+
const backend = opencode({ config });
|
|
63
|
+
return {
|
|
64
|
+
tag,
|
|
65
|
+
backend,
|
|
66
|
+
...(model === undefined ? {} : { model }),
|
|
67
|
+
shutdown: () => backend.shutdown()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "pi":
|
|
71
|
+
return {
|
|
72
|
+
tag,
|
|
73
|
+
backend: pi({ config }),
|
|
74
|
+
...(model === undefined ? {} : { model })
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
backendFailed,
|
|
4
|
+
type BackendTag
|
|
5
|
+
} from "../model/index.ts";
|
|
6
|
+
import type { StreamConversation } from "../conversation/index.ts";
|
|
7
|
+
|
|
8
|
+
const DefaultSubprocessInactivityTimeoutMs = 120_000;
|
|
9
|
+
|
|
10
|
+
interface Deferred<T> {
|
|
11
|
+
readonly promise: Promise<T>;
|
|
12
|
+
resolve(value: T | PromiseLike<T>): void;
|
|
13
|
+
reject(reason?: unknown): void;
|
|
14
|
+
}
|
|
15
|
+
const DefaultSubprocessWallClockTimeoutMs = 600_000;
|
|
16
|
+
|
|
17
|
+
/** A spawned agent subprocess: a stdout (and optional stderr) line/byte stream,
|
|
18
|
+
* an exit promise, a kill signal, and — for backends that feed their prompt over
|
|
19
|
+
* stdin (claude, pi) — an optional write/end pair. Codex leaves stdin ignored and
|
|
20
|
+
* never touches these. */
|
|
21
|
+
export interface SubprocessProcess {
|
|
22
|
+
readonly stdout: AsyncIterable<string | Uint8Array>;
|
|
23
|
+
readonly stderr?: AsyncIterable<string | Uint8Array>;
|
|
24
|
+
readonly exit: Promise<number | null>;
|
|
25
|
+
kill(signal?: NodeJS.Signals): void;
|
|
26
|
+
write?(data: string): void;
|
|
27
|
+
endStdin?(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SubprocessSpawnOptions {
|
|
31
|
+
readonly cwd?: string;
|
|
32
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** Whether stdin is wired. Defaults to "ignore" so codex stays byte-identical;
|
|
34
|
+
* claude/pi pass "pipe" to write the opening user turn. */
|
|
35
|
+
readonly stdin?: "ignore" | "pipe";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SubprocessSpawner = (
|
|
39
|
+
command: string,
|
|
40
|
+
args: readonly string[],
|
|
41
|
+
options: SubprocessSpawnOptions
|
|
42
|
+
) => SubprocessProcess;
|
|
43
|
+
|
|
44
|
+
/** The per-line consumer half a backend supplies: incremental `consume(line)`
|
|
45
|
+
* plus a `finish()` that the helper calls after a clean (zero-exit) stream end.
|
|
46
|
+
* `signal` aborts once the turn is settled — required for persistent processes
|
|
47
|
+
* (pi rpc) that don't exit after a turn. */
|
|
48
|
+
export interface SubprocessConsumer {
|
|
49
|
+
readonly signal: AbortSignal;
|
|
50
|
+
consume(raw: string): Promise<void>;
|
|
51
|
+
finish(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RunSubprocessOptions<B extends BackendTag> {
|
|
55
|
+
readonly backend: B;
|
|
56
|
+
readonly command: string;
|
|
57
|
+
readonly args: readonly string[];
|
|
58
|
+
readonly conversation: StreamConversation<B>;
|
|
59
|
+
readonly createConsumer: () => SubprocessConsumer;
|
|
60
|
+
readonly cwd?: string;
|
|
61
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
62
|
+
readonly stdin?: "ignore" | "pipe";
|
|
63
|
+
readonly spawnProcess?: SubprocessSpawner;
|
|
64
|
+
/** Fail when stdout produces no line for this many ms. Defaults to 120s. */
|
|
65
|
+
readonly inactivityTimeoutMs?: number;
|
|
66
|
+
/** Fail after this absolute turn duration even if stdout stays active. Defaults to 600s. */
|
|
67
|
+
readonly wallClockTimeoutMs?: number;
|
|
68
|
+
/** Records the spawned process so the accessor can `kill` it on cancellation. */
|
|
69
|
+
readonly setProcess?: (process: SubprocessProcess) => void;
|
|
70
|
+
/** Runs right after spawn (before the read loop) — claude/pi write the opening
|
|
71
|
+
* user turn to stdin and close it here. */
|
|
72
|
+
readonly onStart?: (process: SubprocessProcess) => void | Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type SubprocessTimeoutKind = "inactivity" | "wallclock";
|
|
76
|
+
|
|
77
|
+
interface SubprocessTimeout {
|
|
78
|
+
readonly type: "timeout";
|
|
79
|
+
readonly kind: SubprocessTimeoutKind;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Shared spawn → stdout-line-stream → consumer → outcome plumbing for
|
|
83
|
+
* subprocess-stream backends (codex, claude, pi). Owns process spawn, line
|
|
84
|
+
* splitting, stderr capture, non-zero-exit failure, cancellation checks, and
|
|
85
|
+
* timeout settlement; the per-backend command/args builder and line consumer
|
|
86
|
+
* plug in.
|
|
87
|
+
*
|
|
88
|
+
* The helper deliberately does NOT catch spawn / stream / consumer exceptions —
|
|
89
|
+
* each driver wraps the call in its own try/catch/finally so spawn-error→fail and
|
|
90
|
+
* resource teardown stay synchronous with the failure (matters for deterministic
|
|
91
|
+
* cleanup ordering; see codex's ask_user bridge). Use {@link errorMessage} +
|
|
92
|
+
* {@link backendFailed} in that catch. */
|
|
93
|
+
export async function runSubprocessConversation<B extends BackendTag>(
|
|
94
|
+
options: RunSubprocessOptions<B>
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const { conversation, backend } = options;
|
|
97
|
+
const spawnProcess = options.spawnProcess ?? spawnSubprocess;
|
|
98
|
+
const spawnOptions: { cwd?: string; env?: NodeJS.ProcessEnv; stdin?: "ignore" | "pipe" } = {};
|
|
99
|
+
if (options.cwd !== undefined) {
|
|
100
|
+
spawnOptions.cwd = options.cwd;
|
|
101
|
+
}
|
|
102
|
+
if (options.env !== undefined) {
|
|
103
|
+
spawnOptions.env = options.env;
|
|
104
|
+
}
|
|
105
|
+
if (options.stdin !== undefined) {
|
|
106
|
+
spawnOptions.stdin = options.stdin;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const process = spawnProcess(options.command, options.args, spawnOptions);
|
|
110
|
+
options.setProcess?.(process);
|
|
111
|
+
|
|
112
|
+
const stderr = collectText(process.stderr);
|
|
113
|
+
await options.onStart?.(process);
|
|
114
|
+
|
|
115
|
+
const timeout: Deferred<SubprocessTimeout> = Promise.withResolvers<SubprocessTimeout>();
|
|
116
|
+
const inactivityMs = options.inactivityTimeoutMs ?? DefaultSubprocessInactivityTimeoutMs;
|
|
117
|
+
const wallClockMs = options.wallClockTimeoutMs ?? DefaultSubprocessWallClockTimeoutMs;
|
|
118
|
+
let timeoutSettled = false;
|
|
119
|
+
let inactivityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
120
|
+
|
|
121
|
+
const settleTimeout = (kind: SubprocessTimeoutKind): void => {
|
|
122
|
+
if (timeoutSettled) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
timeoutSettled = true;
|
|
126
|
+
process.kill("SIGTERM");
|
|
127
|
+
timeout.resolve({ type: "timeout", kind });
|
|
128
|
+
};
|
|
129
|
+
const resetInactivityTimer = (): void => {
|
|
130
|
+
clearTimeout(inactivityTimer);
|
|
131
|
+
inactivityTimer = setTimeout(() => {
|
|
132
|
+
settleTimeout("inactivity");
|
|
133
|
+
}, Math.max(inactivityMs, 0));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const wallClockTimer = setTimeout(() => {
|
|
137
|
+
settleTimeout("wallclock");
|
|
138
|
+
}, Math.max(wallClockMs, 0));
|
|
139
|
+
resetInactivityTimer();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const consumer = options.createConsumer();
|
|
143
|
+
const lines = splitLines(process.stdout)[Symbol.asyncIterator]();
|
|
144
|
+
for (;;) {
|
|
145
|
+
const next: IteratorResult<string> | SubprocessTimeout = await Promise.race([lines.next(), timeout.promise]);
|
|
146
|
+
if (isSubprocessTimeout(next)) {
|
|
147
|
+
failSubprocessTimeout(conversation, backend, next.kind, inactivityMs, wallClockMs);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (next.done) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
resetInactivityTimer();
|
|
154
|
+
if (conversation.signal.aborted) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await consumer.consume(next.value);
|
|
158
|
+
if (consumer.signal.aborted) {
|
|
159
|
+
// Kill the child once the consumer has settled the conversation on a
|
|
160
|
+
// terminal event (success, modeled failure, or early parse/tool error).
|
|
161
|
+
// Safe because we only reach here after consuming that event — the agent's
|
|
162
|
+
// session rollout is already flushed — and it stops a persistent process
|
|
163
|
+
// (pi rpc) or a stalled CLI from lingering after Orca has the outcome.
|
|
164
|
+
process.kill();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The consumer already settled the conversation (success/failure); the
|
|
170
|
+
// exit-code path below is only for a stream that ended without one.
|
|
171
|
+
if (consumer.signal.aborted) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const exit: number | null | SubprocessTimeout = await Promise.race([process.exit, timeout.promise]);
|
|
176
|
+
if (isSubprocessTimeout(exit)) {
|
|
177
|
+
failSubprocessTimeout(conversation, backend, exit.kind, inactivityMs, wallClockMs);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const stderrText = (await stderr).trim();
|
|
181
|
+
if (conversation.signal.aborted) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (exit !== 0) {
|
|
186
|
+
const exitCodeText = exit === null ? "unknown" : String(exit);
|
|
187
|
+
conversation.fail(
|
|
188
|
+
backendFailed(
|
|
189
|
+
backend,
|
|
190
|
+
`${backend} exited with code ${exitCodeText}${stderrText ? `: ${stderrText}` : ""}`
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
consumer.finish();
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(inactivityTimer);
|
|
199
|
+
clearTimeout(wallClockTimer);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function spawnSubprocess(
|
|
204
|
+
command: string,
|
|
205
|
+
args: readonly string[],
|
|
206
|
+
options: SubprocessSpawnOptions
|
|
207
|
+
): SubprocessProcess {
|
|
208
|
+
const stdinMode = options.stdin ?? "ignore";
|
|
209
|
+
const child = spawn(command, [...args], {
|
|
210
|
+
cwd: options.cwd,
|
|
211
|
+
env: options.env,
|
|
212
|
+
stdio: [stdinMode === "pipe" ? "pipe" : "ignore", "pipe", "pipe"]
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!child.stdout) {
|
|
216
|
+
throw new Error(`failed to capture stdout for ${command}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const exit = Promise.withResolvers<number | null>();
|
|
220
|
+
child.on("error", exit.reject);
|
|
221
|
+
child.on("close", exit.resolve);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
stdout: child.stdout,
|
|
225
|
+
...(child.stderr ? { stderr: child.stderr } : {}),
|
|
226
|
+
exit: exit.promise,
|
|
227
|
+
kill(signal?: NodeJS.Signals) {
|
|
228
|
+
child.kill(signal);
|
|
229
|
+
},
|
|
230
|
+
write(data: string) {
|
|
231
|
+
child.stdin?.write(data);
|
|
232
|
+
},
|
|
233
|
+
endStdin() {
|
|
234
|
+
child.stdin?.end();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function* splitLines(
|
|
240
|
+
chunks: AsyncIterable<string | Uint8Array>
|
|
241
|
+
): AsyncIterable<string> {
|
|
242
|
+
const decoder = new TextDecoder();
|
|
243
|
+
let buffer = "";
|
|
244
|
+
|
|
245
|
+
for await (const chunk of chunks) {
|
|
246
|
+
buffer += decodeChunk(decoder, chunk);
|
|
247
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
248
|
+
while (newlineIndex !== -1) {
|
|
249
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
250
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
251
|
+
yield line;
|
|
252
|
+
newlineIndex = buffer.indexOf("\n");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
buffer += decoder.decode();
|
|
257
|
+
if (buffer.length > 0) {
|
|
258
|
+
yield buffer.replace(/\r$/, "");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function collectText(
|
|
263
|
+
chunks: AsyncIterable<string | Uint8Array> | undefined
|
|
264
|
+
): Promise<string> {
|
|
265
|
+
if (!chunks) {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const decoder = new TextDecoder();
|
|
270
|
+
const text: string[] = [];
|
|
271
|
+
for await (const chunk of chunks) {
|
|
272
|
+
text.push(decodeChunk(decoder, chunk));
|
|
273
|
+
}
|
|
274
|
+
text.push(decoder.decode());
|
|
275
|
+
return text.join("");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function decodeChunk(decoder: TextDecoder, chunk: string | Uint8Array): string {
|
|
279
|
+
return typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isSubprocessTimeout(value: IteratorResult<string> | number | null | SubprocessTimeout): value is SubprocessTimeout {
|
|
283
|
+
return typeof value === "object" && value !== null && (value as { readonly type?: unknown }).type === "timeout";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function failSubprocessTimeout<B extends BackendTag>(
|
|
287
|
+
conversation: StreamConversation<B>,
|
|
288
|
+
backend: B,
|
|
289
|
+
kind: SubprocessTimeoutKind,
|
|
290
|
+
inactivityMs: number,
|
|
291
|
+
wallClockMs: number
|
|
292
|
+
): void {
|
|
293
|
+
if (conversation.signal.aborted) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const message =
|
|
297
|
+
kind === "inactivity"
|
|
298
|
+
? `${backend} emitted no stdout for ${String(inactivityMs)}ms; treating the turn as stalled`
|
|
299
|
+
: `${backend} turn exceeded ${String(wallClockMs)}ms wall-clock limit`;
|
|
300
|
+
conversation.fail(backendFailed(backend, message));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function errorMessage(error: unknown): string {
|
|
304
|
+
return error instanceof Error ? error.message : String(error);
|
|
305
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Conversation } from "../conversation/index.ts";
|
|
3
|
+
import type { BackendConfig, BackendTag } from "../model/index.ts";
|
|
4
|
+
export type {
|
|
5
|
+
BackendApprovalPolicy,
|
|
6
|
+
BackendConfig,
|
|
7
|
+
BackendResult,
|
|
8
|
+
BackendRetryConfig,
|
|
9
|
+
BackendSandboxMode,
|
|
10
|
+
StructuredOutputConfig
|
|
11
|
+
} from "../model/index.ts";
|
|
12
|
+
|
|
13
|
+
export interface AutonomousRequest<Output = unknown, B extends BackendTag = BackendTag> {
|
|
14
|
+
readonly prompt: string;
|
|
15
|
+
readonly schema?: z.ZodType<Output>;
|
|
16
|
+
readonly config?: BackendConfig<B, Output>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LlmBackend<B extends BackendTag = BackendTag> {
|
|
20
|
+
readonly tag: B;
|
|
21
|
+
autonomous<Output = unknown>(request: AutonomousRequest<Output, B>): Conversation<B>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LlmTool {
|
|
25
|
+
autonomous<B extends BackendTag, Output = unknown>(
|
|
26
|
+
backend: LlmBackend<B>,
|
|
27
|
+
request: AutonomousRequest<Output, B>
|
|
28
|
+
): Conversation<B>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AutonomousRequest, LlmBackend } from "./types.ts";
|
|
2
|
+
import { StreamConversation } from "../conversation/index.ts";
|
|
3
|
+
import {
|
|
4
|
+
backendFailed,
|
|
5
|
+
sessionId,
|
|
6
|
+
unsupportedFeature,
|
|
7
|
+
type BackendTag,
|
|
8
|
+
type SessionId
|
|
9
|
+
} from "../model/index.ts";
|
|
10
|
+
|
|
11
|
+
export function unsupportedBackend<B extends BackendTag>(tag: B): LlmBackend<B> {
|
|
12
|
+
return {
|
|
13
|
+
tag,
|
|
14
|
+
autonomous(request: AutonomousRequest) {
|
|
15
|
+
const conversation = new StreamConversation({ backend: tag });
|
|
16
|
+
queueMicrotask(() => {
|
|
17
|
+
if (request.config?.resumeSessionId) {
|
|
18
|
+
conversation.fail(
|
|
19
|
+
unsupportedFeature(`${tag} resume`, `${tag} backend does not support session resume`)
|
|
20
|
+
);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (request.schema || request.config?.structuredOutput) {
|
|
24
|
+
conversation.fail(
|
|
25
|
+
unsupportedFeature(
|
|
26
|
+
`${tag} structured output`,
|
|
27
|
+
`${tag} backend does not support live structured output`
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
conversation.fail(backendFailed(tag, `${tag} backend is not implemented yet`));
|
|
33
|
+
});
|
|
34
|
+
return conversation;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function fakeResult<B extends BackendTag>(
|
|
40
|
+
backend: B,
|
|
41
|
+
output: string
|
|
42
|
+
): { readonly backend: B; readonly sessionId: SessionId<B>; readonly output: string } {
|
|
43
|
+
return {
|
|
44
|
+
backend,
|
|
45
|
+
sessionId: sessionId(backend, `${backend}-fake-session`),
|
|
46
|
+
output
|
|
47
|
+
};
|
|
48
|
+
}
|