@shivaedev/agent-opencode 0.1.0 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +58 -1
- package/dist/Provider.d.ts +16 -0
- package/dist/Provider.d.ts.map +1 -0
- package/dist/Provider.js +4 -0
- package/dist/Provider.js.map +1 -0
- package/dist/adapters/MessageUpdatedProperties.d.ts +10 -0
- package/dist/adapters/MessageUpdatedProperties.d.ts.map +1 -0
- package/dist/adapters/MessageUpdatedProperties.js +10 -0
- package/dist/adapters/MessageUpdatedProperties.js.map +1 -0
- package/dist/adapters/OpencodeRequest.d.ts +6 -0
- package/dist/adapters/OpencodeRequest.d.ts.map +1 -0
- package/dist/adapters/OpencodeRequest.js +2 -0
- package/dist/adapters/OpencodeRequest.js.map +1 -0
- package/dist/adapters/answerToolRequest.d.ts +4 -0
- package/dist/adapters/answerToolRequest.d.ts.map +1 -0
- package/dist/adapters/answerToolRequest.js +106 -0
- package/dist/adapters/answerToolRequest.js.map +1 -0
- package/dist/adapters/listeningUrl.d.ts +2 -0
- package/dist/adapters/listeningUrl.d.ts.map +1 -0
- package/dist/adapters/listeningUrl.js +5 -0
- package/dist/adapters/listeningUrl.js.map +1 -0
- package/dist/adapters/makeOpencodeServer.d.ts +7 -0
- package/dist/adapters/makeOpencodeServer.d.ts.map +1 -0
- package/dist/adapters/makeOpencodeServer.js +49 -0
- package/dist/adapters/makeOpencodeServer.js.map +1 -0
- package/dist/adapters/mapping.d.ts +7 -0
- package/dist/adapters/mapping.d.ts.map +1 -0
- package/dist/adapters/mapping.js +11 -0
- package/dist/adapters/mapping.js.map +1 -0
- package/dist/adapters/messageAuthors.d.ts +11 -0
- package/dist/adapters/messageAuthors.d.ts.map +1 -0
- package/dist/adapters/messageAuthors.js +24 -0
- package/dist/adapters/messageAuthors.js.map +1 -0
- package/dist/adapters/openSseBuffer.d.ts +6 -0
- package/dist/adapters/openSseBuffer.d.ts.map +1 -0
- package/dist/adapters/openSseBuffer.js +34 -0
- package/dist/adapters/openSseBuffer.js.map +1 -0
- package/dist/adapters/openTurnBoundary.d.ts +11 -0
- package/dist/adapters/openTurnBoundary.d.ts.map +1 -0
- package/dist/adapters/openTurnBoundary.js +46 -0
- package/dist/adapters/openTurnBoundary.js.map +1 -0
- package/dist/adapters/opencodePlugin.config.d.ts +11 -0
- package/dist/adapters/opencodePlugin.config.d.ts.map +1 -0
- package/dist/adapters/opencodePlugin.config.js +9 -0
- package/dist/adapters/opencodePlugin.config.js.map +1 -0
- package/dist/adapters/partEvents.d.ts +7 -0
- package/dist/adapters/partEvents.d.ts.map +1 -0
- package/dist/adapters/partEvents.js +62 -0
- package/dist/adapters/partEvents.js.map +1 -0
- package/dist/adapters/projection.d.ts +7 -0
- package/dist/adapters/projection.d.ts.map +1 -0
- package/dist/adapters/projection.js +41 -0
- package/dist/adapters/projection.js.map +1 -0
- package/dist/adapters/protocol.d.ts +36 -0
- package/dist/adapters/protocol.d.ts.map +1 -0
- package/dist/adapters/protocol.js +32 -0
- package/dist/adapters/protocol.js.map +1 -0
- package/dist/adapters/protocolParts.d.ts +48 -0
- package/dist/adapters/protocolParts.d.ts.map +1 -0
- package/dist/adapters/protocolParts.js +47 -0
- package/dist/adapters/protocolParts.js.map +1 -0
- package/dist/adapters/providerAddress.d.ts +5 -0
- package/dist/adapters/providerAddress.d.ts.map +1 -0
- package/dist/adapters/providerAddress.js +21 -0
- package/dist/adapters/providerAddress.js.map +1 -0
- package/dist/adapters/serveOpencode.d.ts +13 -0
- package/dist/adapters/serveOpencode.d.ts.map +1 -0
- package/dist/adapters/serveOpencode.js +75 -0
- package/dist/adapters/serveOpencode.js.map +1 -0
- package/dist/adapters/serveToolRequests.d.ts +4 -0
- package/dist/adapters/serveToolRequests.d.ts.map +1 -0
- package/dist/adapters/serveToolRequests.js +23 -0
- package/dist/adapters/serveToolRequests.js.map +1 -0
- package/dist/adapters/servedTools.d.ts +4 -0
- package/dist/adapters/servedTools.d.ts.map +1 -0
- package/dist/adapters/servedTools.js +9 -0
- package/dist/adapters/servedTools.js.map +1 -0
- package/dist/adapters/sessionFrames.d.ts +7 -0
- package/dist/adapters/sessionFrames.d.ts.map +1 -0
- package/dist/adapters/sessionFrames.js +16 -0
- package/dist/adapters/sessionFrames.js.map +1 -0
- package/dist/adapters/toolEvents.d.ts +10 -0
- package/dist/adapters/toolEvents.d.ts.map +1 -0
- package/dist/adapters/toolEvents.js +30 -0
- package/dist/adapters/toolEvents.js.map +1 -0
- package/dist/adapters/toolNames.d.ts +6 -0
- package/dist/adapters/toolNames.d.ts.map +1 -0
- package/dist/adapters/toolNames.js +8 -0
- package/dist/adapters/toolNames.js.map +1 -0
- package/dist/adapters/toolSessions.d.ts +11 -0
- package/dist/adapters/toolSessions.d.ts.map +1 -0
- package/dist/adapters/toolSessions.js +15 -0
- package/dist/adapters/toolSessions.js.map +1 -0
- package/dist/callWhileOpen.d.ts +4 -0
- package/dist/callWhileOpen.d.ts.map +1 -0
- package/dist/callWhileOpen.js +9 -0
- package/dist/callWhileOpen.js.map +1 -0
- package/dist/failure.d.ts +4 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +8 -0
- package/dist/failure.js.map +1 -0
- package/dist/listOpencodeModels.d.ts +11 -0
- package/dist/listOpencodeModels.d.ts.map +1 -0
- package/dist/listOpencodeModels.js +31 -0
- package/dist/listOpencodeModels.js.map +1 -0
- package/dist/openOpencodeSession.d.ts +5 -0
- package/dist/openOpencodeSession.d.ts.map +1 -0
- package/dist/openOpencodeSession.js +40 -0
- package/dist/openOpencodeSession.js.map +1 -0
- package/dist/opencode.d.ts +9 -1
- package/dist/opencode.d.ts.map +1 -1
- package/dist/opencode.js +49 -1
- package/dist/opencode.js.map +1 -1
- package/dist/operations.d.ts +367 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +87 -0
- package/dist/operations.js.map +1 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +2 -0
- package/dist/options.js.map +1 -0
- package/dist/sessionOpen.d.ts +19 -0
- package/dist/sessionOpen.d.ts.map +1 -0
- package/dist/sessionOpen.js +60 -0
- package/dist/sessionOpen.js.map +1 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +2 -0
- package/dist/tools.js.map +1 -0
- package/dist/turnRequests.d.ts +13 -0
- package/dist/turnRequests.d.ts.map +1 -0
- package/dist/turnRequests.js +19 -0
- package/dist/turnRequests.js.map +1 -0
- package/dist/turnState.d.ts +24 -0
- package/dist/turnState.d.ts.map +1 -0
- package/dist/turnState.js +27 -0
- package/dist/turnState.js.map +1 -0
- package/dist/turns.d.ts +19 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +57 -0
- package/dist/turns.js.map +1 -0
- package/package.json +35 -6
- package/src/Provider.ts +14 -0
- package/src/adapters/MessageUpdatedProperties.ts +9 -0
- package/src/adapters/OpencodeRequest.ts +5 -0
- package/src/adapters/answerToolRequest.ts +121 -0
- package/src/adapters/listeningUrl.ts +4 -0
- package/src/adapters/makeOpencodeServer.ts +77 -0
- package/src/adapters/mapping.ts +14 -0
- package/src/adapters/messageAuthors.ts +31 -0
- package/src/adapters/openSseBuffer.ts +36 -0
- package/src/adapters/openTurnBoundary.ts +58 -0
- package/src/adapters/opencodePlugin.config.ts +17 -0
- package/src/adapters/partEvents.ts +85 -0
- package/src/adapters/projection.ts +46 -0
- package/src/adapters/protocol.ts +31 -0
- package/src/adapters/protocolParts.ts +48 -0
- package/src/adapters/providerAddress.ts +22 -0
- package/src/adapters/serveOpencode.ts +93 -0
- package/src/adapters/serveToolRequests.ts +24 -0
- package/src/adapters/servedTools.ts +11 -0
- package/src/adapters/sessionFrames.ts +25 -0
- package/src/adapters/toolEvents.ts +42 -0
- package/src/adapters/toolNames.ts +7 -0
- package/src/adapters/toolSessions.ts +22 -0
- package/src/callWhileOpen.ts +16 -0
- package/src/failure.ts +7 -0
- package/src/listOpencodeModels.ts +34 -0
- package/src/openOpencodeSession.ts +60 -0
- package/src/opencode.ts +61 -1
- package/src/operations.ts +111 -0
- package/src/options.ts +29 -0
- package/src/sessionOpen.ts +74 -0
- package/src/tools.ts +13 -0
- package/src/turnRequests.ts +25 -0
- package/src/turnState.ts +41 -0
- package/src/turns.ts +89 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Raw } from "@shivaedev/agent/eventData.ts";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
|
|
4
|
+
type RawPayload = typeof Raw.Type;
|
|
5
|
+
|
|
6
|
+
import { Option, Schema } from "effect";
|
|
7
|
+
import { rawEvent } from "#adapters/mapping.ts";
|
|
8
|
+
import type { MessageAuthor, MessageAuthors } from "#adapters/messageAuthors.ts";
|
|
9
|
+
import { KnownPart, PartUpdatedProperties } from "#adapters/protocolParts.ts";
|
|
10
|
+
import { toolEvents } from "#adapters/toolEvents.ts";
|
|
11
|
+
|
|
12
|
+
const decodeProperties = Schema.decodeUnknownOption(PartUpdatedProperties);
|
|
13
|
+
const decodePart = Schema.decodeUnknownOption(KnownPart);
|
|
14
|
+
type Settled = Extract<
|
|
15
|
+
KnownPart,
|
|
16
|
+
{
|
|
17
|
+
type: "reasoning" | "text";
|
|
18
|
+
}
|
|
19
|
+
>;
|
|
20
|
+
function isSettled(part: Settled, author: MessageAuthor): boolean {
|
|
21
|
+
return author.role === "user" || part.time?.end !== undefined;
|
|
22
|
+
}
|
|
23
|
+
function spokenEvents(raw: RawPayload, part: Settled, author: MessageAuthor): AgentEvent[] {
|
|
24
|
+
return part.type === "reasoning"
|
|
25
|
+
? [{ raw, text: part.text, type: "thinking" }]
|
|
26
|
+
: [{ parts: [{ text: part.text, type: "text" }], raw, role: author.role, type: "message" }];
|
|
27
|
+
}
|
|
28
|
+
function usageEvents(
|
|
29
|
+
raw: RawPayload,
|
|
30
|
+
part: Extract<
|
|
31
|
+
KnownPart,
|
|
32
|
+
{
|
|
33
|
+
type: "step-finish";
|
|
34
|
+
}
|
|
35
|
+
>,
|
|
36
|
+
author: MessageAuthor,
|
|
37
|
+
): AgentEvent[] {
|
|
38
|
+
const model = author.model === undefined ? {} : { model: author.model };
|
|
39
|
+
const counted = {
|
|
40
|
+
...(part.tokens.cache?.read === undefined ? {} : { cacheReadTokens: part.tokens.cache.read }),
|
|
41
|
+
...(part.tokens.cache?.write === undefined ? {} : { cacheWriteTokens: part.tokens.cache.write }),
|
|
42
|
+
inputTokens: part.tokens.input,
|
|
43
|
+
outputTokens: part.tokens.output,
|
|
44
|
+
};
|
|
45
|
+
return [
|
|
46
|
+
{ ...counted, ...model, raw, scope: "delta", type: "usage", unit: "turn" },
|
|
47
|
+
...(part.cost === undefined
|
|
48
|
+
? []
|
|
49
|
+
: [
|
|
50
|
+
{
|
|
51
|
+
amount: part.cost,
|
|
52
|
+
currency: "USD",
|
|
53
|
+
...model,
|
|
54
|
+
raw,
|
|
55
|
+
scope: "delta" as const,
|
|
56
|
+
source: "provider" as const,
|
|
57
|
+
type: "cost" as const,
|
|
58
|
+
unit: "turn" as const,
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function authoredEvents(raw: RawPayload, part: KnownPart, author: MessageAuthor, firstReport: (key: string) => boolean): AgentEvent[] {
|
|
64
|
+
if (part.type === "tool") {
|
|
65
|
+
return toolEvents(raw, part, (callId) => firstReport(`${part.state.status}:${callId}`));
|
|
66
|
+
}
|
|
67
|
+
if (part.type === "step-finish") {
|
|
68
|
+
return firstReport(`part:${part.id}`) ? usageEvents(raw, part, author) : [];
|
|
69
|
+
}
|
|
70
|
+
return isSettled(part, author) && firstReport(`part:${part.id}`) ? spokenEvents(raw, part, author) : [];
|
|
71
|
+
}
|
|
72
|
+
export function partEvents(raw: RawPayload, properties: unknown, authors: MessageAuthors, firstReport: (key: string) => boolean): AgentEvent[] {
|
|
73
|
+
return Option.match(decodeProperties(properties), {
|
|
74
|
+
onNone: () => rawEvent(raw),
|
|
75
|
+
onSome: ({ part }) =>
|
|
76
|
+
Option.match(decodePart(part), {
|
|
77
|
+
onNone: () => rawEvent(raw),
|
|
78
|
+
onSome: (known) =>
|
|
79
|
+
Option.match(authors.of(known.messageID), {
|
|
80
|
+
onNone: () => rawEvent(raw),
|
|
81
|
+
onSome: (author) => authoredEvents(raw, known, author, firstReport),
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
2
|
+
import { rawEvent, rawOf } from "#adapters/mapping.ts";
|
|
3
|
+
import { openMessageAuthors } from "#adapters/messageAuthors.ts";
|
|
4
|
+
import { openTurnBoundary } from "#adapters/openTurnBoundary.ts";
|
|
5
|
+
import { partEvents } from "#adapters/partEvents.ts";
|
|
6
|
+
import type { SessionFrame } from "#adapters/sessionFrames.ts";
|
|
7
|
+
|
|
8
|
+
function openFirstReport(): (key: string) => boolean {
|
|
9
|
+
const reported = new Set<string>();
|
|
10
|
+
return (key) => {
|
|
11
|
+
if (reported.has(key)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
reported.add(key);
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface SessionProjection {
|
|
19
|
+
readonly events: (frame: SessionFrame) => AgentEvent[];
|
|
20
|
+
}
|
|
21
|
+
export function openSessionProjection(sessionModel: string | undefined): SessionProjection {
|
|
22
|
+
const authors = openMessageAuthors(sessionModel);
|
|
23
|
+
const boundary = openTurnBoundary();
|
|
24
|
+
const firstReport = openFirstReport();
|
|
25
|
+
function project(type: string, properties: unknown): AgentEvent[] {
|
|
26
|
+
const raw = rawOf(type, properties);
|
|
27
|
+
switch (type) {
|
|
28
|
+
case "message.updated":
|
|
29
|
+
authors.record(properties);
|
|
30
|
+
return [];
|
|
31
|
+
case "message.part.updated":
|
|
32
|
+
return partEvents(raw, properties, authors, firstReport);
|
|
33
|
+
case "message.part.delta":
|
|
34
|
+
return [];
|
|
35
|
+
case "session.error":
|
|
36
|
+
return boundary.errored(raw, properties);
|
|
37
|
+
case "session.idle":
|
|
38
|
+
return boundary.idled(raw);
|
|
39
|
+
case "session.status":
|
|
40
|
+
return boundary.status(raw, properties);
|
|
41
|
+
default:
|
|
42
|
+
return rawEvent(raw);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { events: (frame) => project(frame.type, frame.properties) };
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const SessionResponse = Schema.Struct({
|
|
3
|
+
directory: Schema.String,
|
|
4
|
+
id: Schema.String,
|
|
5
|
+
});
|
|
6
|
+
const ProviderModel = Schema.Struct({
|
|
7
|
+
id: Schema.String,
|
|
8
|
+
name: Schema.String,
|
|
9
|
+
variants: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
10
|
+
});
|
|
11
|
+
const Provider = Schema.Struct({
|
|
12
|
+
id: Schema.String,
|
|
13
|
+
models: Schema.Record(Schema.String, ProviderModel),
|
|
14
|
+
});
|
|
15
|
+
export const ProvidersResponse = Schema.Struct({
|
|
16
|
+
default: Schema.Record(Schema.String, Schema.String),
|
|
17
|
+
providers: Schema.Array(Provider),
|
|
18
|
+
});
|
|
19
|
+
export const GlobalFrame = Schema.Struct({
|
|
20
|
+
payload: Schema.Struct({
|
|
21
|
+
properties: Schema.optional(Schema.Unknown),
|
|
22
|
+
type: Schema.String,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
export const SessionScoped = Schema.Struct({ "sessionID": Schema.String });
|
|
26
|
+
export const SessionStatusProperties = Schema.Struct({
|
|
27
|
+
status: Schema.Struct({ type: Schema.String }),
|
|
28
|
+
});
|
|
29
|
+
export const SessionErrorProperties = Schema.Struct({
|
|
30
|
+
error: Schema.optional(Schema.Struct({ name: Schema.optional(Schema.String) })),
|
|
31
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
const part = {
|
|
4
|
+
id: Schema.String,
|
|
5
|
+
"messageID": Schema.String,
|
|
6
|
+
};
|
|
7
|
+
const PartTime = Schema.Struct({ end: Schema.optional(Schema.Number) });
|
|
8
|
+
const TextPart = Schema.Struct({
|
|
9
|
+
...part,
|
|
10
|
+
text: Schema.String,
|
|
11
|
+
time: Schema.optional(PartTime),
|
|
12
|
+
type: Schema.Literal("text"),
|
|
13
|
+
});
|
|
14
|
+
const ReasoningPart = Schema.Struct({
|
|
15
|
+
...part,
|
|
16
|
+
text: Schema.String,
|
|
17
|
+
time: Schema.optional(PartTime),
|
|
18
|
+
type: Schema.Literal("reasoning"),
|
|
19
|
+
});
|
|
20
|
+
const ToolPart = Schema.Struct({
|
|
21
|
+
...part,
|
|
22
|
+
"callID": Schema.String,
|
|
23
|
+
state: Schema.Struct({
|
|
24
|
+
error: Schema.optional(Schema.String),
|
|
25
|
+
input: Schema.optional(Schema.Unknown),
|
|
26
|
+
output: Schema.optional(Schema.String),
|
|
27
|
+
status: Schema.Literals(["pending", "running", "completed", "error"]),
|
|
28
|
+
}),
|
|
29
|
+
tool: Schema.String,
|
|
30
|
+
type: Schema.Literal("tool"),
|
|
31
|
+
});
|
|
32
|
+
const CacheTokens = Schema.Struct({
|
|
33
|
+
read: Schema.optional(Schema.Number),
|
|
34
|
+
write: Schema.optional(Schema.Number),
|
|
35
|
+
});
|
|
36
|
+
const StepFinishPart = Schema.Struct({
|
|
37
|
+
...part,
|
|
38
|
+
cost: Schema.optional(Schema.Number),
|
|
39
|
+
tokens: Schema.Struct({
|
|
40
|
+
cache: Schema.optional(CacheTokens),
|
|
41
|
+
input: Schema.Number,
|
|
42
|
+
output: Schema.Number,
|
|
43
|
+
}),
|
|
44
|
+
type: Schema.Literal("step-finish"),
|
|
45
|
+
});
|
|
46
|
+
export const KnownPart = Schema.Union([TextPart, ReasoningPart, ToolPart, StepFinishPart]);
|
|
47
|
+
export type KnownPart = typeof KnownPart.Type;
|
|
48
|
+
export const PartUpdatedProperties = Schema.Struct({ part: Schema.Unknown });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { serveOpencode } from "#adapters/serveOpencode.ts";
|
|
4
|
+
import type { ToolSessions } from "#adapters/toolSessions.ts";
|
|
5
|
+
import { unavailable } from "#failure.ts";
|
|
6
|
+
import type { OpencodeOptions } from "#options.ts";
|
|
7
|
+
export const providerAddress = Effect.fnUntraced(function* (options: OpencodeOptions, tools: ToolSessions) {
|
|
8
|
+
let endpoint: string | undefined;
|
|
9
|
+
if (options.tools?.length) {
|
|
10
|
+
const { answerToolRequest } = yield* Effect.tryPromise({ catch: unavailable, try: () => import("#adapters/answerToolRequest.ts") });
|
|
11
|
+
const { serveToolRequests } = yield* Effect.tryPromise({ catch: unavailable, try: () => import("#adapters/serveToolRequests.ts") });
|
|
12
|
+
endpoint = yield* serveToolRequests(answerToolRequest(options.tools, tools));
|
|
13
|
+
}
|
|
14
|
+
return yield* serveOpencode({
|
|
15
|
+
command: options.command ?? "opencode",
|
|
16
|
+
constrained: options.constrainedPrompt !== undefined,
|
|
17
|
+
cwd: options.cwd,
|
|
18
|
+
plugin: fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./opencodePlugin.config.ts" : "./opencodePlugin.config.js", import.meta.url)),
|
|
19
|
+
skills: options.skills,
|
|
20
|
+
tools: endpoint,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { Deferred, Effect, Fiber, Stream } from "effect";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
|
+
import { listeningUrl } from "#adapters/listeningUrl.ts";
|
|
5
|
+
import { CONSTRAINED_AGENT, TOOL_SERVER_NAME } from "#adapters/toolNames.ts";
|
|
6
|
+
import { unavailable } from "#failure.ts";
|
|
7
|
+
|
|
8
|
+
const LOOPBACK = "127.0.0.1";
|
|
9
|
+
interface ServeOptions {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly constrained: boolean;
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
readonly plugin: string;
|
|
14
|
+
readonly skills?: string | undefined;
|
|
15
|
+
readonly tools?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
function serveArgs(port: string) {
|
|
18
|
+
return ["serve", "--port", port, "--hostname", LOOPBACK];
|
|
19
|
+
}
|
|
20
|
+
const START_PATIENCE_MILLIS = 30_000;
|
|
21
|
+
// OpenCode resets this MCP deadline on progress notifications while a tool remains pending.
|
|
22
|
+
const TOOL_TIMEOUT = 300_000;
|
|
23
|
+
// Pure mode would disable the caller-session plugin and the tool bridge.
|
|
24
|
+
const CONSTRAINED_ENV = {
|
|
25
|
+
"OPENCODE_DISABLE_CLAUDE_CODE_PROMPT": "1",
|
|
26
|
+
"OPENCODE_DISABLE_EXTERNAL_SKILLS": "1",
|
|
27
|
+
"OPENCODE_DISABLE_PROJECT_CONFIG": "1",
|
|
28
|
+
};
|
|
29
|
+
const CONSTRAINED_CONFIG = {
|
|
30
|
+
agent: { [CONSTRAINED_AGENT]: { prompt: "Follow the instructions in the system message." } },
|
|
31
|
+
};
|
|
32
|
+
function configContent(options: ServeOptions): string {
|
|
33
|
+
const mode = options.constrained ? CONSTRAINED_CONFIG : {};
|
|
34
|
+
const skills = options.skills === undefined ? {} : { skills: { paths: [options.skills] } };
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
...(options.tools === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { mcp: { [TOOL_SERVER_NAME]: { timeout: TOOL_TIMEOUT, type: "remote", url: options.tools } }, plugin: [pathToFileURL(options.plugin).href] }),
|
|
39
|
+
...mode,
|
|
40
|
+
...(options.constrained ? {} : skills),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function serveEnv(options: ServeOptions) {
|
|
44
|
+
return {
|
|
45
|
+
"OPENCODE_CONFIG_CONTENT": configContent(options),
|
|
46
|
+
...(options.constrained ? CONSTRAINED_ENV : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const complaintsOf = Effect.fnUntraced(function* (child: ChildProcessSpawner.ChildProcessHandle) {
|
|
50
|
+
const said = yield* Effect.forkScoped(Stream.mkString(Stream.decodeText(child.stderr)));
|
|
51
|
+
return Effect.orElseSucceed(Fiber.join(said), () => "");
|
|
52
|
+
});
|
|
53
|
+
const addressPrintedBy = Effect.fnUntraced(function* (child: ChildProcessSpawner.ChildProcessHandle) {
|
|
54
|
+
const printed = yield* Deferred.make<string>();
|
|
55
|
+
yield* Effect.forkScoped(
|
|
56
|
+
Stream.runForEach(Stream.decodeText(child.stdout).pipe(Stream.splitLines), (chunk) =>
|
|
57
|
+
Effect.sync(() => {
|
|
58
|
+
const address = listeningUrl(chunk);
|
|
59
|
+
if (address !== undefined) {
|
|
60
|
+
Deferred.doneUnsafe(printed, Effect.succeed(address));
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
return Deferred.await(printed);
|
|
66
|
+
});
|
|
67
|
+
function exitedFirst(child: ChildProcessSpawner.ChildProcessHandle, complaints: Effect.Effect<string>) {
|
|
68
|
+
return Effect.flatMap(Effect.match(child.exitCode, { onFailure: String, onSuccess: String }), (reason) =>
|
|
69
|
+
Effect.flatMap(complaints, (said) => Effect.fail(`opencode serve exited with ${reason}: ${said.slice(0, 500)}`)),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
export const serveOpencode = Effect.fnUntraced(function* (options: ServeOptions) {
|
|
73
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
74
|
+
const port = "0";
|
|
75
|
+
const child = yield* spawner.spawn(
|
|
76
|
+
ChildProcess.make(options.command, serveArgs(port), {
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
env: serveEnv(options),
|
|
79
|
+
extendEnv: true,
|
|
80
|
+
forceKillAfter: 5000,
|
|
81
|
+
stdin: "ignore",
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
const complaints = yield* complaintsOf(child);
|
|
85
|
+
const printed = yield* addressPrintedBy(child);
|
|
86
|
+
const address = yield* Effect.raceFirst(printed, exitedFirst(child, complaints)).pipe(
|
|
87
|
+
Effect.timeoutOrElse({
|
|
88
|
+
duration: START_PATIENCE_MILLIS,
|
|
89
|
+
orElse: () => Effect.fail(`opencode serve did not print its address on port ${port} in time`),
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
return address;
|
|
93
|
+
}, Effect.mapError(unavailable));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
3
|
+
import { Effect, type Scope } from "effect";
|
|
4
|
+
import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
5
|
+
import type { BackendUnavailable } from "@shivaedev/agent/failures.ts";
|
|
6
|
+
import { unavailable } from "#failure.ts";
|
|
7
|
+
|
|
8
|
+
const LOOPBACK = "127.0.0.1";
|
|
9
|
+
const onlyPosted = HttpServerResponse.empty({ headers: { allow: "POST" }, status: 405 });
|
|
10
|
+
export function serveToolRequests(answer: (request: Request) => Promise<Response>): Effect.Effect<string, BackendUnavailable, Scope.Scope> {
|
|
11
|
+
return Effect.gen(function* () {
|
|
12
|
+
const server = yield* NodeHttpServer.make(createServer, { host: LOOPBACK, port: 0 });
|
|
13
|
+
const respond = Effect.gen(function* () {
|
|
14
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
15
|
+
if (request.method !== "POST") {
|
|
16
|
+
return onlyPosted;
|
|
17
|
+
}
|
|
18
|
+
const web = yield* HttpServerRequest.toWeb(request);
|
|
19
|
+
return HttpServerResponse.fromWeb(yield* Effect.promise(() => answer(web)));
|
|
20
|
+
});
|
|
21
|
+
yield* server.serve(respond);
|
|
22
|
+
return HttpServer.formatAddress(server.address);
|
|
23
|
+
}).pipe(Effect.mapError(unavailable));
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect, Scope } from "effect";
|
|
2
|
+
import { callWhileOpen } from "#callWhileOpen.ts";
|
|
3
|
+
import type { DirectTool } from "#tools.ts";
|
|
4
|
+
export const servedTools = Effect.fnUntraced(function* (tools: readonly DirectTool[]) {
|
|
5
|
+
const calls = yield* Effect.flatMap(Effect.scope, Scope.fork);
|
|
6
|
+
const services = yield* Effect.context<never>();
|
|
7
|
+
const run = Effect.runPromiseWith(services);
|
|
8
|
+
return new Map(
|
|
9
|
+
tools.map((tool) => [tool.name, (callId: string, args: unknown) => run(callWhileOpen(tool, callId, args).pipe(Scope.provide(calls)))]),
|
|
10
|
+
);
|
|
11
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import { GlobalFrame, SessionScoped } from "#adapters/protocol.ts";
|
|
3
|
+
|
|
4
|
+
const decodeFrame = Schema.decodeUnknownOption(GlobalFrame);
|
|
5
|
+
const decodeScoped = Schema.decodeUnknownOption(SessionScoped);
|
|
6
|
+
const decodeMessage = Schema.decodeUnknownOption(Schema.Struct({ info: SessionScoped }));
|
|
7
|
+
const decodePart = Schema.decodeUnknownOption(Schema.Struct({ part: SessionScoped }));
|
|
8
|
+
function sessionOf(properties: unknown) {
|
|
9
|
+
return decodeScoped(properties).pipe(
|
|
10
|
+
Option.orElse(() => Option.map(decodeMessage(properties), ({ info }) => info)),
|
|
11
|
+
Option.orElse(() => Option.map(decodePart(properties), ({ part }) => part)),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
export interface SessionFrame {
|
|
15
|
+
readonly properties?: unknown;
|
|
16
|
+
readonly type: string;
|
|
17
|
+
}
|
|
18
|
+
export function frameFor(sessionId: string, frame: unknown): Option.Option<SessionFrame> {
|
|
19
|
+
return Option.flatMap(decodeFrame(frame), ({ payload }) =>
|
|
20
|
+
Option.match(sessionOf(payload.properties), {
|
|
21
|
+
onNone: () => Option.none(),
|
|
22
|
+
onSome: (scoped) => (scoped.sessionID === sessionId ? Option.some(payload) : Option.none()),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Raw } from "@shivaedev/agent/eventData.ts";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
|
|
4
|
+
type RawPayload = typeof Raw.Type;
|
|
5
|
+
|
|
6
|
+
import type { KnownPart } from "#adapters/protocolParts.ts";
|
|
7
|
+
|
|
8
|
+
type ToolPart = Extract<
|
|
9
|
+
KnownPart,
|
|
10
|
+
{
|
|
11
|
+
type: "tool";
|
|
12
|
+
}
|
|
13
|
+
>;
|
|
14
|
+
export function toolEvents(raw: RawPayload, part: ToolPart, announce: (callId: string) => boolean): AgentEvent[] {
|
|
15
|
+
const { state } = part;
|
|
16
|
+
if (state.status === "completed" || state.status === "error") {
|
|
17
|
+
if (!announce(part.callID)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
ok: state.status === "completed",
|
|
23
|
+
output: state.output ?? state.error ?? "",
|
|
24
|
+
raw,
|
|
25
|
+
toolId: part.callID,
|
|
26
|
+
type: "tool.completed",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
if (state.status === "pending" || !announce(part.callID)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
input: JSON.stringify(state.input ?? {}),
|
|
36
|
+
name: part.tool,
|
|
37
|
+
raw,
|
|
38
|
+
toolId: part.callID,
|
|
39
|
+
type: "tool.started",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const CALLER_SESSION = "callerSession";
|
|
2
|
+
export const CALLER_CALL = "callerCall";
|
|
3
|
+
export const CONSTRAINED_AGENT = "platform";
|
|
4
|
+
export const TOOL_SERVER_NAME = "platform";
|
|
5
|
+
export function wireName(name: string): string {
|
|
6
|
+
return `${TOOL_SERVER_NAME}_${name}`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { ToolOutcome as DirectToolOutcome } from "#tools.ts";
|
|
3
|
+
export type ServedTool = (callId: string, args: unknown) => Promise<DirectToolOutcome>;
|
|
4
|
+
export interface ToolSessions {
|
|
5
|
+
readonly forget: (session: string) => void;
|
|
6
|
+
readonly names: readonly string[];
|
|
7
|
+
readonly remember: (session: string, tools: ReadonlyMap<string, ServedTool>) => void;
|
|
8
|
+
readonly served: (session: string) => Option.Option<ReadonlyMap<string, ServedTool>>;
|
|
9
|
+
}
|
|
10
|
+
export function makeToolSessions(names: readonly string[]): ToolSessions {
|
|
11
|
+
const open = new Map<string, ReadonlyMap<string, ServedTool>>();
|
|
12
|
+
return {
|
|
13
|
+
forget: (session) => {
|
|
14
|
+
open.delete(session);
|
|
15
|
+
},
|
|
16
|
+
names,
|
|
17
|
+
remember: (session, tools) => {
|
|
18
|
+
open.set(session, tools);
|
|
19
|
+
},
|
|
20
|
+
served: (session) => Option.fromNullishOr(open.get(session)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect, Exit, Fiber, type Scope } from "effect";
|
|
2
|
+
import type { DirectTool, ToolOutcome } from "#tools.ts";
|
|
3
|
+
|
|
4
|
+
const closed: ToolOutcome = {
|
|
5
|
+
ok: false,
|
|
6
|
+
text: "the session that served this tool closed before the call finished",
|
|
7
|
+
};
|
|
8
|
+
export function callWhileOpen(tool: DirectTool, callId: string, args: unknown): Effect.Effect<ToolOutcome, never, Scope.Scope> {
|
|
9
|
+
return Effect.flatMap(Effect.scope, (scope) =>
|
|
10
|
+
tool.call(callId, args).pipe(
|
|
11
|
+
Effect.forkIn(scope),
|
|
12
|
+
Effect.flatMap(Fiber.await),
|
|
13
|
+
Effect.flatMap((exit) => (Exit.hasInterrupts(exit) ? Effect.succeed(closed) : exit)),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
}
|
package/src/failure.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BackendUnavailable, ProtocolFailure } from "@shivaedev/agent/failures.ts";
|
|
2
|
+
export function opencodeFailure(detail: unknown): ProtocolFailure {
|
|
3
|
+
return new ProtocolFailure({ backend: "opencode", message: String(detail) });
|
|
4
|
+
}
|
|
5
|
+
export function unavailable(detail: unknown): BackendUnavailable {
|
|
6
|
+
return new BackendUnavailable({ backend: "opencode", message: String(detail) });
|
|
7
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { ModelChoice } from "@shivaedev/agent/eventData.ts";
|
|
3
|
+
import type { ReadFailure as BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import { ProvidersResponse } from "#adapters/protocol.ts";
|
|
5
|
+
import { opencodeFailure } from "#failure.ts";
|
|
6
|
+
import { Provider } from "#Provider.ts";
|
|
7
|
+
|
|
8
|
+
const decodeProviders = Schema.decodeUnknownOption(ProvidersResponse);
|
|
9
|
+
type Providers = typeof ProvidersResponse.Type;
|
|
10
|
+
function catalogued({ default: defaults, providers }: Providers): readonly ModelChoice[] {
|
|
11
|
+
const listed: ModelChoice[] = [];
|
|
12
|
+
let preferred: string | undefined;
|
|
13
|
+
for (const provider of providers) {
|
|
14
|
+
for (const model of Object.values(provider.models)) {
|
|
15
|
+
const id = `${provider.id}/${model.id}`;
|
|
16
|
+
if (preferred === undefined && defaults[provider.id] === model.id) {
|
|
17
|
+
preferred = id;
|
|
18
|
+
}
|
|
19
|
+
listed.push({ efforts: Object.keys(model.variants ?? {}), id, isDefault: false, name: model.name });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const standing = preferred ?? listed[0]?.id;
|
|
23
|
+
return listed.map((model) => (model.id === standing ? { ...model, isDefault: true } : model));
|
|
24
|
+
}
|
|
25
|
+
function catalogue(response: unknown): Effect.Effect<readonly ModelChoice[], BackendFailure> {
|
|
26
|
+
return Option.match(decodeProviders(response), {
|
|
27
|
+
onNone: () => Effect.fail(opencodeFailure("GET /config/providers returned no catalog")),
|
|
28
|
+
onSome: (listing) => Effect.succeed(catalogued(listing)),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export const listOpencodeModels = Effect.fn("OpenCode.listOpencodeModels")(function* () {
|
|
32
|
+
const server = yield* Provider;
|
|
33
|
+
return yield* server.get({ body: undefined, path: "/config/providers", query: {} }).pipe(Effect.flatMap(catalogue));
|
|
34
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Effect, Option, PubSub, type Scope, Stream } from "effect";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import { openSessionProjection, type SessionProjection } from "#adapters/projection.ts";
|
|
5
|
+
import { servedTools } from "#adapters/servedTools.ts";
|
|
6
|
+
import { frameFor } from "#adapters/sessionFrames.ts";
|
|
7
|
+
import type { OpenSessionOptions, SessionHandle } from "#options.ts";
|
|
8
|
+
import { Provider } from "#Provider.ts";
|
|
9
|
+
import { openSession, promptSettings, sessionIdOf, sessionOpened } from "#sessionOpen.ts";
|
|
10
|
+
import { turnRequests } from "#turnRequests.ts";
|
|
11
|
+
import { makeTurnDriver } from "#turns.ts";
|
|
12
|
+
|
|
13
|
+
function eventsOfFrame(sessionId: string, projection: SessionProjection) {
|
|
14
|
+
return (frame: unknown): Stream.Stream<AgentEvent> =>
|
|
15
|
+
Stream.fromIterable(
|
|
16
|
+
Option.match(frameFor(sessionId, frame), {
|
|
17
|
+
onNone: (): readonly AgentEvent[] => [],
|
|
18
|
+
onSome: projection.events,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export const openOpencodeSession = Effect.fn("OpenCode.openOpencodeSession")(function* (
|
|
23
|
+
options: OpenSessionOptions,
|
|
24
|
+
): Effect.fn.Return<SessionHandle, BackendFailure, Scope.Scope | Provider> {
|
|
25
|
+
const server = yield* Provider;
|
|
26
|
+
const settings = yield* promptSettings(options);
|
|
27
|
+
const forEvents = yield* PubSub.subscribe(server.frames);
|
|
28
|
+
const forDriver = yield* PubSub.subscribe(server.frames);
|
|
29
|
+
const [route, response] = yield* openSession(options);
|
|
30
|
+
const sessionId = yield* sessionIdOf(route, response);
|
|
31
|
+
const tools = yield* servedTools(options.tools);
|
|
32
|
+
yield* Effect.acquireRelease(
|
|
33
|
+
Effect.sync(() => server.tools.remember(sessionId, tools)),
|
|
34
|
+
() => Effect.sync(() => server.tools.forget(sessionId)),
|
|
35
|
+
);
|
|
36
|
+
const driver = yield* makeTurnDriver(yield* turnRequests(sessionId, options.cwd, settings));
|
|
37
|
+
yield* Effect.forkScoped(server.exited.pipe(Effect.andThen(driver.close)));
|
|
38
|
+
yield* Effect.forkScoped(
|
|
39
|
+
Stream.fromSubscription(forDriver).pipe(
|
|
40
|
+
Stream.runForEach((frame) =>
|
|
41
|
+
Option.match(frameFor(sessionId, frame), {
|
|
42
|
+
onNone: () => Effect.void,
|
|
43
|
+
onSome: driver.track,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
const projection = openSessionProjection(options.model);
|
|
49
|
+
const events: Stream.Stream<AgentEvent> = Stream.make(sessionOpened(route, response, sessionId)).pipe(
|
|
50
|
+
Stream.concat(Stream.fromSubscription(forEvents).pipe(Stream.flatMap(eventsOfFrame(sessionId, projection)))),
|
|
51
|
+
Stream.interruptWhen(server.exited),
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
events,
|
|
55
|
+
interrupt: driver.interrupt,
|
|
56
|
+
nativeRef: Effect.succeed(Option.some(sessionId)),
|
|
57
|
+
queue: (input) => driver.queue(input.parts.map((part) => part.text).join("\n")),
|
|
58
|
+
steer: (input) => driver.steer(input.parts.map((part) => part.text).join("\n")),
|
|
59
|
+
} satisfies SessionHandle;
|
|
60
|
+
});
|
package/src/opencode.ts
CHANGED
|
@@ -1 +1,61 @@
|
|
|
1
|
-
|
|
1
|
+
import { Effect, Layer, Option } from "effect";
|
|
2
|
+
import { FetchHttpClient } from "effect/http";
|
|
3
|
+
import type { ChildProcessSpawner } from "effect/process";
|
|
4
|
+
import { Backend } from "@shivaedev/agent/backend.ts";
|
|
5
|
+
import type { ReadFailure } from "@shivaedev/agent/failures.ts";
|
|
6
|
+
import { makeOpencodeServer } from "#adapters/makeOpencodeServer.ts";
|
|
7
|
+
import { providerAddress } from "#adapters/providerAddress.ts";
|
|
8
|
+
import { makeToolSessions } from "#adapters/toolSessions.ts";
|
|
9
|
+
import { opencodeFailure } from "#failure.ts";
|
|
10
|
+
import { listOpencodeModels } from "#listOpencodeModels.ts";
|
|
11
|
+
import { makeOperations } from "#operations.ts";
|
|
12
|
+
import type { OpencodeOptions } from "#options.ts";
|
|
13
|
+
import { Provider } from "#Provider.ts";
|
|
14
|
+
export function opencode(
|
|
15
|
+
options: OpencodeOptions & {
|
|
16
|
+
readonly url: string;
|
|
17
|
+
},
|
|
18
|
+
): Layer.Layer<Backend, ReadFailure>;
|
|
19
|
+
export function opencode(options: OpencodeOptions): Layer.Layer<Backend, ReadFailure, ChildProcessSpawner.ChildProcessSpawner>;
|
|
20
|
+
export function opencode(options: OpencodeOptions) {
|
|
21
|
+
return Layer.effect(
|
|
22
|
+
Backend,
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const tools = makeToolSessions((options.tools ?? []).map((tool) => tool.name));
|
|
25
|
+
let url = options.url;
|
|
26
|
+
if (url !== undefined && (options.tools?.length || options.sessionTools !== undefined)) {
|
|
27
|
+
return yield* Effect.fail(opencodeFailure("session tool bridging requires a spawned OpenCode server"));
|
|
28
|
+
}
|
|
29
|
+
if (url === undefined) {
|
|
30
|
+
url = yield* providerAddress(options, tools);
|
|
31
|
+
}
|
|
32
|
+
const server = yield* makeOpencodeServer(url, tools);
|
|
33
|
+
const operations = yield* makeOperations(options).pipe(Effect.provideService(Provider, server));
|
|
34
|
+
return {
|
|
35
|
+
backend: "opencode",
|
|
36
|
+
events: operations.events,
|
|
37
|
+
features: {
|
|
38
|
+
imageInput: { reason: "only text input is implemented", support: "unsupported" },
|
|
39
|
+
interruption: {
|
|
40
|
+
implementation: { interrupt: (receipt) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.session.interrupt)) },
|
|
41
|
+
support: "supported",
|
|
42
|
+
},
|
|
43
|
+
models: { implementation: { list: listOpencodeModels().pipe(Effect.provideService(Provider, server)) }, support: "supported" },
|
|
44
|
+
queuedInput: {
|
|
45
|
+
implementation: { queue: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.session.queue(input))) },
|
|
46
|
+
support: "supported",
|
|
47
|
+
},
|
|
48
|
+
sessions: { implementation: { resume: operations.resume }, support: "supported" },
|
|
49
|
+
steering: {
|
|
50
|
+
implementation: { steer: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.session.steer(input))) },
|
|
51
|
+
support: "supported",
|
|
52
|
+
},
|
|
53
|
+
transcript: { reason: "no provider history reader is implemented", support: "unsupported" },
|
|
54
|
+
},
|
|
55
|
+
inspect: operations.inspect,
|
|
56
|
+
reconcile: operations.reconcile,
|
|
57
|
+
start: (request) => operations.open(request, Option.none()),
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
).pipe(Layer.provide(FetchHttpClient.layer));
|
|
61
|
+
}
|