@shivaedev/agent-opencode 0.1.0 → 0.2.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 +6 -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 +33 -4
- 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,111 @@
|
|
|
1
|
+
import { type Cause, Effect, Option, Queue, Stream } from "effect";
|
|
2
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import { ProtocolFailure, UnknownAcceptance } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import type { Observation, Receipt, StartRequest } from "@shivaedev/agent/run.ts";
|
|
5
|
+
import { opencodeFailure } from "#failure.ts";
|
|
6
|
+
import { openOpencodeSession } from "#openOpencodeSession.ts";
|
|
7
|
+
import type { OpencodeOptions, SessionHandle } from "#options.ts";
|
|
8
|
+
import { Provider } from "#Provider.ts";
|
|
9
|
+
export interface OpenRun {
|
|
10
|
+
readonly events: Stream.Stream<RunEvent>;
|
|
11
|
+
readonly receipt: Receipt;
|
|
12
|
+
readonly session: SessionHandle;
|
|
13
|
+
}
|
|
14
|
+
export const makeOperations = Effect.fnUntraced(function* (options: OpencodeOptions) {
|
|
15
|
+
const server = yield* Provider;
|
|
16
|
+
const runs = new Map<string, OpenRun>();
|
|
17
|
+
const receipts = new Map<string, Receipt>();
|
|
18
|
+
const observation = new Map<string, Observation>();
|
|
19
|
+
function lookup(receipt: Receipt) {
|
|
20
|
+
return Effect.suspend(() => {
|
|
21
|
+
const run = runs.get(receipt.operationId);
|
|
22
|
+
return receipt.backend === "opencode" && receipt.reference.version === 1 && run?.receipt.reference.value === receipt.reference.value
|
|
23
|
+
? Effect.succeed(run)
|
|
24
|
+
: Effect.fail(opencodeFailure("run is not open in this backend scope"));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const open = Effect.fnUntraced(function* (request: StartRequest, resume: Option.Option<string>) {
|
|
28
|
+
if (receipts.has(request.operationId)) {
|
|
29
|
+
return yield* Effect.fail(opencodeFailure("operation already acknowledged; reconcile instead"));
|
|
30
|
+
}
|
|
31
|
+
const session = yield* openOpencodeSession({
|
|
32
|
+
constrainedPrompt: options.constrainedPrompt,
|
|
33
|
+
cwd: request.cwd,
|
|
34
|
+
effort: Option.fromUndefinedOr(request.effort),
|
|
35
|
+
model: request.model,
|
|
36
|
+
resume,
|
|
37
|
+
tools: options.sessionTools?.(request) ?? options.tools ?? [],
|
|
38
|
+
}).pipe(Effect.provideService(Provider, server));
|
|
39
|
+
const reference = yield* session.nativeRef;
|
|
40
|
+
if (Option.isNone(reference)) {
|
|
41
|
+
return yield* Effect.fail(opencodeFailure("session returned no reference"));
|
|
42
|
+
}
|
|
43
|
+
const receipt: Receipt = {
|
|
44
|
+
backend: "opencode",
|
|
45
|
+
operationId: request.operationId,
|
|
46
|
+
reference: { value: reference.value, version: 1 },
|
|
47
|
+
session: { value: reference.value, version: 1 },
|
|
48
|
+
};
|
|
49
|
+
yield* session
|
|
50
|
+
.steer(request.input)
|
|
51
|
+
.pipe(
|
|
52
|
+
Effect.mapError((error) =>
|
|
53
|
+
error._tag === "Rejected"
|
|
54
|
+
? error
|
|
55
|
+
: new UnknownAcceptance({ backend: "opencode", message: error.message, operationId: request.operationId }),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
const output = yield* Queue.unbounded<RunEvent, Cause.Done>();
|
|
59
|
+
observation.set(request.operationId, { state: "accepted", type: "known" });
|
|
60
|
+
function observe(event: RunEvent) {
|
|
61
|
+
return Effect.sync(() => {
|
|
62
|
+
if (event.type === "execution") {
|
|
63
|
+
observation.set(request.operationId, { state: event.state, type: "known" });
|
|
64
|
+
}
|
|
65
|
+
}).pipe(Effect.andThen(Queue.offer(output, event)));
|
|
66
|
+
}
|
|
67
|
+
const ended = Effect.sync(() => {
|
|
68
|
+
const latest = observation.get(request.operationId);
|
|
69
|
+
if (runs.has(request.operationId) && latest?.type === "known" && !["completed", "failed", "interrupted"].includes(latest.state)) {
|
|
70
|
+
observation.set(request.operationId, { reason: "provider event stream ended without terminal evidence", type: "missing" });
|
|
71
|
+
}
|
|
72
|
+
}).pipe(Effect.andThen(Queue.end(output)));
|
|
73
|
+
yield* Effect.forkScoped(session.events.pipe(Stream.runForEach(observe), Effect.ensuring(ended)));
|
|
74
|
+
const events = Stream.fromQueue(output);
|
|
75
|
+
runs.set(request.operationId, { events, receipt, session });
|
|
76
|
+
receipts.set(request.operationId, receipt);
|
|
77
|
+
yield* Effect.addFinalizer(() =>
|
|
78
|
+
Effect.sync(() => {
|
|
79
|
+
runs.delete(request.operationId);
|
|
80
|
+
observation.delete(request.operationId);
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
return receipt;
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
events: (receipt: Receipt) => Stream.unwrap(lookup(receipt).pipe(Effect.map((run) => run.events))),
|
|
87
|
+
inspect: (receipt: Receipt) =>
|
|
88
|
+
Effect.sync(
|
|
89
|
+
(): Observation => observation.get(receipt.operationId) ?? { reason: "no live execution evidence in this backend scope", type: "missing" },
|
|
90
|
+
),
|
|
91
|
+
lookup,
|
|
92
|
+
open,
|
|
93
|
+
reconcile: (operationId: string) =>
|
|
94
|
+
Effect.sync(() => {
|
|
95
|
+
const receipt = receipts.get(operationId);
|
|
96
|
+
return receipt === undefined
|
|
97
|
+
? { operationId, reason: "OpenCode has no operation-id lookup; absence is not proof of refusal", type: "uncertain" as const }
|
|
98
|
+
: { receipt, type: "accepted" as const };
|
|
99
|
+
}),
|
|
100
|
+
resume: (
|
|
101
|
+
reference: {
|
|
102
|
+
readonly value: string;
|
|
103
|
+
readonly version: number;
|
|
104
|
+
},
|
|
105
|
+
request: StartRequest,
|
|
106
|
+
) =>
|
|
107
|
+
reference.version === 1
|
|
108
|
+
? open(request, Option.some(reference.value))
|
|
109
|
+
: Effect.fail(new ProtocolFailure({ backend: "opencode", message: "unsupported session reference version" })),
|
|
110
|
+
};
|
|
111
|
+
});
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Effect, Option, Stream } from "effect";
|
|
2
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import type { Input, StartRequest } from "@shivaedev/agent/run.ts";
|
|
5
|
+
import type { DirectTool } from "#tools.ts";
|
|
6
|
+
export interface OpencodeOptions {
|
|
7
|
+
readonly command?: string | undefined;
|
|
8
|
+
readonly constrainedPrompt?: string | undefined;
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
readonly sessionTools?: (request: StartRequest) => readonly DirectTool[];
|
|
11
|
+
readonly skills?: string | undefined;
|
|
12
|
+
readonly tools?: readonly DirectTool[];
|
|
13
|
+
readonly url?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
export interface OpenSessionOptions {
|
|
16
|
+
readonly constrainedPrompt?: string | undefined;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly effort: Option.Option<string>;
|
|
19
|
+
readonly model?: string | undefined;
|
|
20
|
+
readonly resume: Option.Option<string>;
|
|
21
|
+
readonly tools: readonly DirectTool[];
|
|
22
|
+
}
|
|
23
|
+
export interface SessionHandle {
|
|
24
|
+
readonly events: Stream.Stream<RunEvent>;
|
|
25
|
+
readonly interrupt: Effect.Effect<void, BackendFailure>;
|
|
26
|
+
readonly nativeRef: Effect.Effect<Option.Option<string>>;
|
|
27
|
+
readonly queue: (input: Input) => Effect.Effect<void, BackendFailure>;
|
|
28
|
+
readonly steer: (input: Input) => Effect.Effect<void, BackendFailure>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import { rawOf } from "#adapters/mapping.ts";
|
|
5
|
+
import { SessionResponse } from "#adapters/protocol.ts";
|
|
6
|
+
import { CONSTRAINED_AGENT, wireName } from "#adapters/toolNames.ts";
|
|
7
|
+
import { opencodeFailure } from "#failure.ts";
|
|
8
|
+
import type { OpenSessionOptions } from "#options.ts";
|
|
9
|
+
import { Provider, type ProviderPort } from "#Provider.ts";
|
|
10
|
+
|
|
11
|
+
const decodeSession = Schema.decodeUnknownOption(SessionResponse);
|
|
12
|
+
export interface PromptSettings {
|
|
13
|
+
readonly agent?: string;
|
|
14
|
+
readonly model?: {
|
|
15
|
+
readonly "modelID": string;
|
|
16
|
+
readonly "providerID": string;
|
|
17
|
+
};
|
|
18
|
+
readonly system?: string;
|
|
19
|
+
readonly variant?: string;
|
|
20
|
+
}
|
|
21
|
+
function namedModel(model: string): Effect.Effect<PromptSettings, BackendFailure> {
|
|
22
|
+
const separator = model.indexOf("/");
|
|
23
|
+
return separator === -1
|
|
24
|
+
? Effect.fail(opencodeFailure(`model ${JSON.stringify(model)} names no provider; OpenCode reads a model as provider/model`))
|
|
25
|
+
: Effect.succeed({ model: { "modelID": model.slice(separator + 1), "providerID": model.slice(0, separator) } });
|
|
26
|
+
}
|
|
27
|
+
export function promptSettings(options: OpenSessionOptions): Effect.Effect<PromptSettings, BackendFailure> {
|
|
28
|
+
const constrained: PromptSettings = options.constrainedPrompt === undefined ? {} : { agent: CONSTRAINED_AGENT, system: options.constrainedPrompt };
|
|
29
|
+
const variant = Option.match(options.effort, {
|
|
30
|
+
onNone: (): PromptSettings => constrained,
|
|
31
|
+
onSome: (effort) => ({ ...constrained, variant: effort }),
|
|
32
|
+
});
|
|
33
|
+
return Effect.map(options.model === undefined ? Effect.succeed({}) : namedModel(options.model), (named) => ({ ...named, ...variant }));
|
|
34
|
+
}
|
|
35
|
+
function onlyGivenTools(tools: OpenSessionOptions["tools"]) {
|
|
36
|
+
return [
|
|
37
|
+
{ action: "deny", pattern: "*", permission: "*" },
|
|
38
|
+
...tools.map((tool) => ({ action: "allow", pattern: "*", permission: wireName(tool.name) })),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
function withoutOtherTools(server: ProviderPort, tools: OpenSessionOptions["tools"]) {
|
|
42
|
+
const given = new Set(tools.map((tool) => tool.name));
|
|
43
|
+
return server.tools.names.filter((name) => !given.has(name)).map((name) => ({ action: "deny", pattern: "*", permission: wireName(name) }));
|
|
44
|
+
}
|
|
45
|
+
function permissions(server: ProviderPort, options: OpenSessionOptions) {
|
|
46
|
+
return options.constrainedPrompt === undefined ? withoutOtherTools(server, options.tools) : onlyGivenTools(options.tools);
|
|
47
|
+
}
|
|
48
|
+
export const openSession = Effect.fnUntraced(function* (options: OpenSessionOptions) {
|
|
49
|
+
const server = yield* Provider;
|
|
50
|
+
const query = { directory: options.cwd };
|
|
51
|
+
return yield* Option.match(options.resume, {
|
|
52
|
+
onNone: () =>
|
|
53
|
+
server
|
|
54
|
+
.post({ body: { permission: permissions(server, options) }, path: "/session", query })
|
|
55
|
+
.pipe(Effect.map((response) => ["POST /session", response] as const)),
|
|
56
|
+
onSome: (sessionId) =>
|
|
57
|
+
server
|
|
58
|
+
.get({ body: undefined, path: `/session/${sessionId}`, query })
|
|
59
|
+
.pipe(Effect.map((response) => [`GET /session/${sessionId}`, response] as const)),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
export function sessionIdOf(route: string, response: unknown): Effect.Effect<string, BackendFailure> {
|
|
63
|
+
return Option.match(decodeSession(response), {
|
|
64
|
+
onNone: () => Effect.fail(opencodeFailure(`${route} returned no session`)),
|
|
65
|
+
onSome: ({ id }) => Effect.succeed(id),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function sessionOpened(route: string, response: unknown, sessionId: string): AgentEvent {
|
|
69
|
+
return {
|
|
70
|
+
raw: rawOf(route, response),
|
|
71
|
+
reference: sessionId,
|
|
72
|
+
type: "session.opened",
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Effect } from "effect";
|
|
2
|
+
export interface ToolOutcome {
|
|
3
|
+
readonly ok: boolean;
|
|
4
|
+
readonly text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ToolDefinition {
|
|
7
|
+
readonly description: string;
|
|
8
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DirectTool extends ToolDefinition {
|
|
12
|
+
readonly call: (callId: string, args: unknown) => Effect.Effect<ToolOutcome>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import { Provider } from "#Provider.ts";
|
|
4
|
+
import type { PromptSettings } from "#sessionOpen.ts";
|
|
5
|
+
export interface TurnRequests {
|
|
6
|
+
readonly abort: Effect.Effect<unknown, BackendFailure>;
|
|
7
|
+
readonly prompt: (text: string) => Effect.Effect<unknown, BackendFailure>;
|
|
8
|
+
}
|
|
9
|
+
export const turnRequests = Effect.fnUntraced(function* (sessionId: string, cwd: string, settings: PromptSettings) {
|
|
10
|
+
const server = yield* Provider;
|
|
11
|
+
const query = { directory: cwd };
|
|
12
|
+
return {
|
|
13
|
+
abort: server.post({
|
|
14
|
+
body: {},
|
|
15
|
+
path: `/session/${sessionId}/abort`,
|
|
16
|
+
query,
|
|
17
|
+
}),
|
|
18
|
+
prompt: (text: string) =>
|
|
19
|
+
server.post({
|
|
20
|
+
body: { ...settings, parts: [{ text, type: "text" }] },
|
|
21
|
+
path: `/session/${sessionId}/prompt_async`,
|
|
22
|
+
query,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
package/src/turnState.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Deferred } from "effect";
|
|
2
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import { opencodeFailure } from "#failure.ts";
|
|
4
|
+
export interface PendingText {
|
|
5
|
+
readonly accepted: Deferred.Deferred<void, BackendFailure>;
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OpenTurnState {
|
|
9
|
+
readonly "_tag": "open";
|
|
10
|
+
readonly pending: readonly PendingText[];
|
|
11
|
+
readonly running: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface ClosedTurnState {
|
|
14
|
+
readonly "_tag": "closed";
|
|
15
|
+
}
|
|
16
|
+
export type TurnState = ClosedTurnState | OpenTurnState;
|
|
17
|
+
export const closed: ClosedTurnState = { "_tag": "closed" };
|
|
18
|
+
export const idle: OpenTurnState = {
|
|
19
|
+
"_tag": "open",
|
|
20
|
+
pending: [],
|
|
21
|
+
running: false,
|
|
22
|
+
};
|
|
23
|
+
export const SESSION_CLOSED = opencodeFailure("session closed before delivery reached the provider");
|
|
24
|
+
export function withPending(state: OpenTurnState, text: PendingText): OpenTurnState {
|
|
25
|
+
return { ...state, pending: [...state.pending, text] };
|
|
26
|
+
}
|
|
27
|
+
export function running(state: OpenTurnState): OpenTurnState {
|
|
28
|
+
return {
|
|
29
|
+
...state,
|
|
30
|
+
running: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function rested(state: OpenTurnState): OpenTurnState {
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
running: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function sent(state: TurnState): TurnState {
|
|
40
|
+
return state._tag === "closed" ? state : { ...state, pending: state.pending.slice(1) };
|
|
41
|
+
}
|
package/src/turns.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Deferred, Effect, Ref, Semaphore } from "effect";
|
|
2
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import type { SessionFrame } from "#adapters/sessionFrames.ts";
|
|
4
|
+
import type { TurnRequests } from "#turnRequests.ts";
|
|
5
|
+
import {
|
|
6
|
+
closed,
|
|
7
|
+
idle,
|
|
8
|
+
type OpenTurnState,
|
|
9
|
+
type PendingText,
|
|
10
|
+
rested,
|
|
11
|
+
running,
|
|
12
|
+
SESSION_CLOSED,
|
|
13
|
+
sent,
|
|
14
|
+
type TurnState,
|
|
15
|
+
withPending,
|
|
16
|
+
} from "#turnState.ts";
|
|
17
|
+
|
|
18
|
+
function refuse(next: PendingText, failure: BackendFailure) {
|
|
19
|
+
return Deferred.fail(next.accepted, failure).pipe(Effect.andThen(Effect.logWarning("opencode: queued prompt failed", failure)));
|
|
20
|
+
}
|
|
21
|
+
export interface TurnDriver {
|
|
22
|
+
readonly close: Effect.Effect<void>;
|
|
23
|
+
readonly interrupt: Effect.Effect<void, BackendFailure>;
|
|
24
|
+
readonly queue: (text: string) => Effect.Effect<void, BackendFailure>;
|
|
25
|
+
readonly steer: (text: string) => Effect.Effect<void, BackendFailure>;
|
|
26
|
+
readonly track: (frame: SessionFrame) => Effect.Effect<void>;
|
|
27
|
+
}
|
|
28
|
+
export const makeTurnDriver = Effect.fnUntraced(function* (requests: TurnRequests) {
|
|
29
|
+
const state = yield* Ref.make<TurnState>(idle);
|
|
30
|
+
const gate = yield* Semaphore.make(1);
|
|
31
|
+
function sendNow(text: string) {
|
|
32
|
+
return requests.prompt(text).pipe(Effect.andThen(Ref.update(state, (current) => (current._tag === "closed" ? current : running(current)))));
|
|
33
|
+
}
|
|
34
|
+
function flush(current: OpenTurnState) {
|
|
35
|
+
const [next] = current.pending;
|
|
36
|
+
if (next === undefined) {
|
|
37
|
+
return Effect.void;
|
|
38
|
+
}
|
|
39
|
+
return sendNow(next.text).pipe(
|
|
40
|
+
Effect.matchEffect({
|
|
41
|
+
onFailure: (failure) => Ref.update(state, sent).pipe(Effect.andThen(refuse(next, failure))),
|
|
42
|
+
onSuccess: () => Ref.update(state, sent).pipe(Effect.andThen(Deferred.succeed(next.accepted, undefined))),
|
|
43
|
+
}),
|
|
44
|
+
Effect.asVoid,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function admit(text: string, accepted: PendingText["accepted"]) {
|
|
48
|
+
return Effect.flatMap(Ref.get(state), (current) => {
|
|
49
|
+
if (current._tag === "closed") {
|
|
50
|
+
return Deferred.fail(accepted, SESSION_CLOSED);
|
|
51
|
+
}
|
|
52
|
+
if (current.running) {
|
|
53
|
+
return Ref.set(state, withPending(current, { accepted, text }));
|
|
54
|
+
}
|
|
55
|
+
return sendNow(text).pipe(
|
|
56
|
+
Effect.matchEffect({
|
|
57
|
+
onFailure: (failure) => Deferred.fail(accepted, failure),
|
|
58
|
+
onSuccess: () => Deferred.succeed(accepted, undefined),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const queue = Effect.fnUntraced(function* (text: string) {
|
|
64
|
+
const accepted = yield* Deferred.make<void, BackendFailure>();
|
|
65
|
+
yield* gate.withPermit(admit(text, accepted));
|
|
66
|
+
yield* Deferred.await(accepted);
|
|
67
|
+
});
|
|
68
|
+
function steer(text: string) {
|
|
69
|
+
return Effect.flatMap(Ref.get(state), (current) => (current._tag === "closed" ? Effect.fail(SESSION_CLOSED) : sendNow(text)));
|
|
70
|
+
}
|
|
71
|
+
const settled = Effect.flatMap(Ref.get(state), (current) =>
|
|
72
|
+
current._tag === "closed" ? Effect.void : Ref.set(state, rested(current)).pipe(Effect.andThen(flush(rested(current)))),
|
|
73
|
+
);
|
|
74
|
+
const close = Ref.getAndSet(state, closed).pipe(
|
|
75
|
+
Effect.flatMap((current) =>
|
|
76
|
+
current._tag === "closed"
|
|
77
|
+
? Effect.void
|
|
78
|
+
: Effect.forEach(current.pending, (waiting) => Deferred.fail(waiting.accepted, SESSION_CLOSED)).pipe(Effect.asVoid),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
yield* Effect.addFinalizer(() => close);
|
|
82
|
+
return {
|
|
83
|
+
close,
|
|
84
|
+
interrupt: Effect.asVoid(requests.abort),
|
|
85
|
+
queue,
|
|
86
|
+
steer: (text) => gate.withPermit(steer(text)),
|
|
87
|
+
track: (frame) => (frame.type === "session.idle" ? gate.withPermit(settled) : Effect.void),
|
|
88
|
+
} satisfies TurnDriver;
|
|
89
|
+
});
|