@opencode-ai/simulation 0.0.0-dev-17880
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/dist/backend/index.d.ts +17 -0
- package/dist/backend/index.js +29 -0
- package/dist/backend/network.d.ts +33 -0
- package/dist/backend/network.js +44 -0
- package/dist/backend/openai.d.ts +4 -0
- package/dist/backend/openai.js +101 -0
- package/dist/backend/simulated-provider.d.ts +27 -0
- package/dist/backend/simulated-provider.js +485 -0
- package/dist/control-server.d.ts +27 -0
- package/dist/control-server.js +99 -0
- package/dist/frontend/actions.d.ts +90 -0
- package/dist/frontend/actions.js +154 -0
- package/dist/frontend/renderer.d.ts +18 -0
- package/dist/frontend/renderer.js +38 -0
- package/dist/frontend/semantics.d.ts +8 -0
- package/dist/frontend/semantics.js +7 -0
- package/dist/frontend/server.d.ts +6 -0
- package/dist/frontend/server.js +64 -0
- package/dist/frontend/simulation.d.ts +5 -0
- package/dist/frontend/simulation.js +20 -0
- package/dist/manifest.d.ts +27 -0
- package/dist/manifest.js +61 -0
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.js +10 -0
- package/dist/recording.d.ts +65 -0
- package/dist/recording.js +105 -0
- package/package.json +65 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LayerNode } from "@opencode-ai/util/effect/layer-node";
|
|
2
|
+
import { Effect, FileSystem } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Layer replacements applied when the server is built in simulation mode.
|
|
5
|
+
*
|
|
6
|
+
* The server merges these into the app node build when simulation is enabled
|
|
7
|
+
* is enabled, via a dynamic import so this module is never loaded eagerly.
|
|
8
|
+
*
|
|
9
|
+
* - Network: all outbound HTTP resolves against the simulated route table;
|
|
10
|
+
* unknown destinations are denied. The driver-answered OpenAI endpoint is
|
|
11
|
+
* registered here as the first route.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
export declare const simulationReplacements: (app: {
|
|
15
|
+
readonly version: string;
|
|
16
|
+
}) => Effect.Effect<LayerNode.Replacements, Error, FileSystem.FileSystem>;
|
|
17
|
+
export * as Simulation from "./index";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LayerNode } from "@opencode-ai/util/effect/layer-node";
|
|
2
|
+
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node";
|
|
3
|
+
import { httpClient } from "@opencode-ai/util/effect/app-node-platform";
|
|
4
|
+
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk";
|
|
5
|
+
import { Config, Effect, FileSystem, Layer } from "effect";
|
|
6
|
+
import { HttpClient } from "effect/unstable/http";
|
|
7
|
+
import { DriveManifest } from "../manifest";
|
|
8
|
+
import { SimulationNetwork } from "./network";
|
|
9
|
+
import { SimulationOpenAI } from "./openai";
|
|
10
|
+
import { SimulatedProvider } from "./simulated-provider";
|
|
11
|
+
export const simulationReplacements = Effect.fn("Simulation.replacements")(function* (app) {
|
|
12
|
+
const models = SimulationNetwork.json("GET", "https://models.opencode.ai/api.json", {});
|
|
13
|
+
if (!(yield* Config.string("OPENCODE_DRIVE").pipe(Config.withDefault(void 0))))
|
|
14
|
+
return [[httpClient, SimulationNetwork.layer([models])]];
|
|
15
|
+
const manifest = yield* DriveManifest.resolve(), networkLayer = Layer.effect(HttpClient.HttpClient, Effect.gen(function* () {
|
|
16
|
+
const provider = yield* SimulatedProvider.Service;
|
|
17
|
+
return (yield* SimulationNetwork.make([SimulationOpenAI.route(provider), models])).client;
|
|
18
|
+
})).pipe(Layer.provide(SimulatedProvider.layerDrive({
|
|
19
|
+
endpoint: manifest.endpoints.backend,
|
|
20
|
+
version: app.version
|
|
21
|
+
}))), networkNode = makeGlobalNode({
|
|
22
|
+
service: HttpClient.HttpClient,
|
|
23
|
+
layer: networkLayer,
|
|
24
|
+
deps: [SdkPlugins.node]
|
|
25
|
+
});
|
|
26
|
+
return [[httpClient, networkNode]];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export * as Simulation from "./index";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientResponse, type HttpMethod } from "effect/unstable/http";
|
|
3
|
+
import { HttpClientError } from "effect/unstable/http/HttpClientError";
|
|
4
|
+
import type { HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import { SimulationProtocol } from "../protocol";
|
|
6
|
+
/**
|
|
7
|
+
* Simulated network.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the `HttpClient.HttpClient` platform node in simulation mode. All
|
|
10
|
+
* outbound HTTP resolves against an in-memory route table; unknown
|
|
11
|
+
* destinations fail loudly with a transport error so no simulation run can
|
|
12
|
+
* silently reach the real network. The scripted LLM is one registered route,
|
|
13
|
+
* not a separate mechanism.
|
|
14
|
+
*
|
|
15
|
+
* Each acquired run owns its routes and request log.
|
|
16
|
+
*/
|
|
17
|
+
export interface Route {
|
|
18
|
+
/** Return a response effect to claim the request, undefined to pass. */
|
|
19
|
+
readonly match: (request: HttpClientRequest.HttpClientRequest, url: URL) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError> | undefined;
|
|
20
|
+
}
|
|
21
|
+
export type LogEntry = SimulationProtocol.Backend.NetworkLogEntry;
|
|
22
|
+
/** Static JSON route: exact method + origin/path match answered with a fixed body. */
|
|
23
|
+
export declare function json(method: HttpMethod.HttpMethod, url: string, body: unknown): Route;
|
|
24
|
+
export interface Run {
|
|
25
|
+
readonly client: HttpClient.HttpClient;
|
|
26
|
+
readonly log: () => Effect.Effect<readonly LogEntry[]>;
|
|
27
|
+
}
|
|
28
|
+
export declare const make: (routes?: readonly Route[] | undefined) => Effect.Effect<{
|
|
29
|
+
client: HttpClient.HttpClient;
|
|
30
|
+
log: () => Effect.Effect<readonly SimulationProtocol.Backend.NetworkLogEntry[], never, never>;
|
|
31
|
+
}, never, never>;
|
|
32
|
+
export declare const layer: (routes?: readonly Route[]) => Layer.Layer<HttpClient.HttpClient, never, never>;
|
|
33
|
+
export * as SimulationNetwork from "./network";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Clock, Effect, Layer, Ref } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError";
|
|
4
|
+
import { SimulationProtocol } from "../protocol";
|
|
5
|
+
const LOG_LIMIT = 1000;
|
|
6
|
+
export function json(method, url, body) {
|
|
7
|
+
return {
|
|
8
|
+
match: (request, requestUrl) => {
|
|
9
|
+
if (request.method !== method)
|
|
10
|
+
return;
|
|
11
|
+
if (requestUrl.origin + requestUrl.pathname !== url)
|
|
12
|
+
return;
|
|
13
|
+
return Effect.sync(() => HttpClientResponse.fromWeb(request, new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } })));
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export const make = Effect.fn("SimulationNetwork.make")(function* (routes = []) {
|
|
18
|
+
const log = yield* Ref.make([]);
|
|
19
|
+
return { client: HttpClient.make((request, url) => Effect.gen(function* () {
|
|
20
|
+
let matched;
|
|
21
|
+
for (const route of routes) {
|
|
22
|
+
matched = route.match(request, url);
|
|
23
|
+
if (matched)
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const entry = {
|
|
27
|
+
time: yield* Clock.currentTimeMillis,
|
|
28
|
+
method: request.method,
|
|
29
|
+
url: url.toString(),
|
|
30
|
+
matched: matched !== void 0
|
|
31
|
+
};
|
|
32
|
+
yield* Ref.update(log, (entries) => [...entries, entry].slice(-LOG_LIMIT));
|
|
33
|
+
if (matched)
|
|
34
|
+
return yield* matched;
|
|
35
|
+
return yield* Effect.fail(new HttpClientError({
|
|
36
|
+
reason: new TransportError({
|
|
37
|
+
request,
|
|
38
|
+
description: `Simulation denied unregistered network destination: ${request.method} ${url}`
|
|
39
|
+
})
|
|
40
|
+
}));
|
|
41
|
+
})), log: () => Ref.get(log) };
|
|
42
|
+
}), layer = (routes = []) => Layer.effect(HttpClient.HttpClient, make(routes).pipe(Effect.map((run) => run.client)));
|
|
43
|
+
|
|
44
|
+
export * as SimulationNetwork from "./network";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError";
|
|
4
|
+
import { OpenAIChatEvent, DEFAULT_BASE_URL, PATH } from "@opencode-ai/ai/protocols/openai-chat";
|
|
5
|
+
import { SimulationNetwork } from "./network";
|
|
6
|
+
import { SimulatedProvider } from "./simulated-provider";
|
|
7
|
+
const encodeChunk = Schema.encodeUnknownSync(OpenAIChatEvent), encoder = new TextEncoder, decodeBody = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
8
|
+
function chunkOf(item) {
|
|
9
|
+
if (item.type === "textDelta")
|
|
10
|
+
return { choices: [{ delta: { content: item.text } }] };
|
|
11
|
+
if (item.type === "reasoningDelta")
|
|
12
|
+
return { choices: [{ delta: { reasoning_content: item.text } }] };
|
|
13
|
+
if (item.type === "toolInputStart")
|
|
14
|
+
return {
|
|
15
|
+
choices: [
|
|
16
|
+
{
|
|
17
|
+
delta: {
|
|
18
|
+
tool_calls: [
|
|
19
|
+
{
|
|
20
|
+
index: item.index,
|
|
21
|
+
id: item.id,
|
|
22
|
+
function: { name: item.name, arguments: "" }
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
if (item.type === "toolInputDelta")
|
|
30
|
+
return {
|
|
31
|
+
choices: [
|
|
32
|
+
{
|
|
33
|
+
delta: {
|
|
34
|
+
tool_calls: [{ index: item.index, function: { arguments: item.text } }]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
if (item.type === "toolCall")
|
|
40
|
+
return {
|
|
41
|
+
choices: [
|
|
42
|
+
{
|
|
43
|
+
delta: {
|
|
44
|
+
tool_calls: [
|
|
45
|
+
{
|
|
46
|
+
index: item.index,
|
|
47
|
+
id: item.id,
|
|
48
|
+
function: { name: item.name, arguments: JSON.stringify(item.input) }
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
return item.chunk;
|
|
56
|
+
}
|
|
57
|
+
const finishReasonWire = {
|
|
58
|
+
stop: "stop",
|
|
59
|
+
"tool-calls": "tool_calls",
|
|
60
|
+
length: "length",
|
|
61
|
+
"content-filter": "content_filter"
|
|
62
|
+
};
|
|
63
|
+
function frame(payload) {
|
|
64
|
+
return encoder.encode(`data: ${JSON.stringify(payload)}
|
|
65
|
+
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
function sseBody(events) {
|
|
69
|
+
return events.pipe(Stream.map((event) => {
|
|
70
|
+
if (event.type === "finish")
|
|
71
|
+
return frame(encodeChunk({ choices: [{ delta: {}, finish_reason: finishReasonWire[event.reason] }] }));
|
|
72
|
+
if (event.type === "raw")
|
|
73
|
+
return frame(event.chunk);
|
|
74
|
+
return frame(encodeChunk(chunkOf(event)));
|
|
75
|
+
}), Stream.concat(Stream.make(encoder.encode(`data: [DONE]
|
|
76
|
+
|
|
77
|
+
`))));
|
|
78
|
+
}
|
|
79
|
+
export const route = (provider) => ({
|
|
80
|
+
match: (request, url) => {
|
|
81
|
+
if (request.method !== "POST")
|
|
82
|
+
return;
|
|
83
|
+
if (url.origin + url.pathname !== DEFAULT_BASE_URL + PATH)
|
|
84
|
+
return;
|
|
85
|
+
return Effect.gen(function* () {
|
|
86
|
+
const body = request.body._tag === "Uint8Array" ? yield* decodeBody(new TextDecoder().decode(request.body.body)).pipe(Effect.mapError((cause) => new HttpClientError({
|
|
87
|
+
reason: new TransportError({
|
|
88
|
+
request,
|
|
89
|
+
cause,
|
|
90
|
+
description: "Simulation received an invalid OpenAI request body"
|
|
91
|
+
})
|
|
92
|
+
}))) : {};
|
|
93
|
+
return HttpClientResponse.fromWeb(request, new Response(Stream.toReadableStream(sseBody(provider.stream({ url: url.toString(), body }))), {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { "content-type": "text/event-stream" }
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export * as SimulationOpenAI from "./openai";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk";
|
|
2
|
+
import { Cause, Context, Layer, Schema, Stream } from "effect";
|
|
3
|
+
import { SimulationProtocol } from "../protocol";
|
|
4
|
+
export interface ProviderRequest {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type ProviderResponseEvent = SimulationProtocol.Backend.Item | {
|
|
9
|
+
readonly type: "finish";
|
|
10
|
+
readonly reason: SimulationProtocol.Backend.FinishReason;
|
|
11
|
+
};
|
|
12
|
+
declare const ProviderDisconnectedError_base: Schema.Class<ProviderDisconnectedError, Schema.TaggedStruct<"SimulatedProvider.ProviderDisconnectedError", {
|
|
13
|
+
readonly message: Schema.String;
|
|
14
|
+
}>, Cause.YieldableError>;
|
|
15
|
+
export declare class ProviderDisconnectedError extends ProviderDisconnectedError_base {
|
|
16
|
+
}
|
|
17
|
+
export interface Interface {
|
|
18
|
+
readonly stream: (request: ProviderRequest) => Stream.Stream<ProviderResponseEvent, ProviderDisconnectedError>;
|
|
19
|
+
}
|
|
20
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/simulation/SimulatedProvider", Interface>;
|
|
21
|
+
export declare class Service extends Service_base {
|
|
22
|
+
}
|
|
23
|
+
export declare const layerDrive: (options: {
|
|
24
|
+
readonly endpoint: string;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
}) => Layer.Layer<Service, unknown, SdkPlugins.Service>;
|
|
27
|
+
export * as SimulatedProvider from "./simulated-provider";
|