@shivaedev/agent-codex 0.0.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +47 -1
- package/dist/adapters/Request.d.ts +4 -0
- package/dist/adapters/Request.d.ts.map +1 -0
- package/dist/adapters/Request.js +2 -0
- package/dist/adapters/Request.js.map +1 -0
- package/dist/adapters/UserMessageItem.d.ts +22 -0
- package/dist/adapters/UserMessageItem.d.ts.map +1 -0
- package/dist/adapters/UserMessageItem.js +24 -0
- package/dist/adapters/UserMessageItem.js.map +1 -0
- package/dist/adapters/attributed.d.ts +6 -0
- package/dist/adapters/attributed.d.ts.map +1 -0
- package/dist/adapters/attributed.js +17 -0
- package/dist/adapters/attributed.js.map +1 -0
- package/dist/adapters/codexFailure.d.ts +3 -0
- package/dist/adapters/codexFailure.d.ts.map +1 -0
- package/dist/adapters/codexFailure.js +5 -0
- package/dist/adapters/codexFailure.js.map +1 -0
- package/dist/adapters/connect.d.ts +11 -0
- package/dist/adapters/connect.d.ts.map +1 -0
- package/dist/adapters/connect.js +100 -0
- package/dist/adapters/connect.js.map +1 -0
- package/dist/adapters/delivery.d.ts +18 -0
- package/dist/adapters/delivery.d.ts.map +1 -0
- package/dist/adapters/delivery.js +115 -0
- package/dist/adapters/delivery.js.map +1 -0
- package/dist/adapters/deliveryState.d.ts +31 -0
- package/dist/adapters/deliveryState.d.ts.map +1 -0
- package/dist/adapters/deliveryState.js +29 -0
- package/dist/adapters/deliveryState.js.map +1 -0
- package/dist/adapters/engine.d.ts +10 -0
- package/dist/adapters/engine.d.ts.map +1 -0
- package/dist/adapters/engine.js +80 -0
- package/dist/adapters/engine.js.map +1 -0
- package/dist/adapters/features.d.ts +726 -0
- package/dist/adapters/features.d.ts.map +1 -0
- package/dist/adapters/features.js +46 -0
- package/dist/adapters/features.js.map +1 -0
- package/dist/adapters/handshake.d.ts +5 -0
- package/dist/adapters/handshake.d.ts.map +1 -0
- package/dist/adapters/handshake.js +30 -0
- package/dist/adapters/handshake.js.map +1 -0
- package/dist/adapters/items.d.ts +7 -0
- package/dist/adapters/items.d.ts.map +1 -0
- package/dist/adapters/items.js +105 -0
- package/dist/adapters/items.js.map +1 -0
- package/dist/adapters/mapping.d.ts +9 -0
- package/dist/adapters/mapping.d.ts.map +1 -0
- package/dist/adapters/mapping.js +57 -0
- package/dist/adapters/mapping.js.map +1 -0
- package/dist/adapters/process.d.ts +15 -0
- package/dist/adapters/process.d.ts.map +1 -0
- package/dist/adapters/process.js +55 -0
- package/dist/adapters/process.js.map +1 -0
- package/dist/adapters/protocol.d.ts +178 -0
- package/dist/adapters/protocol.d.ts.map +1 -0
- package/dist/adapters/protocol.js +97 -0
- package/dist/adapters/protocol.js.map +1 -0
- package/dist/adapters/protocolItems.d.ts +89 -0
- package/dist/adapters/protocolItems.d.ts.map +1 -0
- package/dist/adapters/protocolItems.js +100 -0
- package/dist/adapters/protocolItems.js.map +1 -0
- package/dist/adapters/rateLimits.d.ts +7 -0
- package/dist/adapters/rateLimits.d.ts.map +1 -0
- package/dist/adapters/rateLimits.js +40 -0
- package/dist/adapters/rateLimits.js.map +1 -0
- package/dist/adapters/reads.d.ts +324 -0
- package/dist/adapters/reads.d.ts.map +1 -0
- package/dist/adapters/reads.js +83 -0
- package/dist/adapters/reads.js.map +1 -0
- package/dist/adapters/recovery.d.ts +324 -0
- package/dist/adapters/recovery.d.ts.map +1 -0
- package/dist/adapters/recovery.js +39 -0
- package/dist/adapters/recovery.js.map +1 -0
- package/dist/adapters/residualApproval.d.ts +4 -0
- package/dist/adapters/residualApproval.d.ts.map +1 -0
- package/dist/adapters/residualApproval.js +21 -0
- package/dist/adapters/residualApproval.js.map +1 -0
- package/dist/adapters/rpc.d.ts +8 -0
- package/dist/adapters/rpc.d.ts.map +1 -0
- package/dist/adapters/rpc.js +2 -0
- package/dist/adapters/rpc.js.map +1 -0
- package/dist/adapters/rpcProtocol.d.ts +11 -0
- package/dist/adapters/rpcProtocol.d.ts.map +1 -0
- package/dist/adapters/rpcProtocol.js +29 -0
- package/dist/adapters/rpcProtocol.js.map +1 -0
- package/dist/adapters/server.d.ts +30 -0
- package/dist/adapters/server.d.ts.map +1 -0
- package/dist/adapters/server.js +68 -0
- package/dist/adapters/server.js.map +1 -0
- package/dist/adapters/session.d.ts +312 -0
- package/dist/adapters/session.d.ts.map +1 -0
- package/dist/adapters/session.js +45 -0
- package/dist/adapters/session.js.map +1 -0
- package/dist/adapters/subagentItems.d.ts +16 -0
- package/dist/adapters/subagentItems.d.ts.map +1 -0
- package/dist/adapters/subagentItems.js +58 -0
- package/dist/adapters/subagentItems.js.map +1 -0
- package/dist/adapters/tellCliVersionOnce.d.ts +5 -0
- package/dist/adapters/tellCliVersionOnce.d.ts.map +1 -0
- package/dist/adapters/tellCliVersionOnce.js +13 -0
- package/dist/adapters/tellCliVersionOnce.js.map +1 -0
- package/dist/adapters/threadClaims.d.ts +8 -0
- package/dist/adapters/threadClaims.d.ts.map +1 -0
- package/dist/adapters/threadClaims.js +20 -0
- package/dist/adapters/threadClaims.js.map +1 -0
- package/dist/adapters/threadSweep.d.ts +15 -0
- package/dist/adapters/threadSweep.d.ts.map +1 -0
- package/dist/adapters/threadSweep.js +57 -0
- package/dist/adapters/threadSweep.js.map +1 -0
- package/dist/adapters/threadTree.d.ts +8 -0
- package/dist/adapters/threadTree.d.ts.map +1 -0
- package/dist/adapters/threadTree.js +125 -0
- package/dist/adapters/threadTree.js.map +1 -0
- package/dist/adapters/turnRequests.d.ts +13 -0
- package/dist/adapters/turnRequests.d.ts.map +1 -0
- package/dist/adapters/turnRequests.js +27 -0
- package/dist/adapters/turnRequests.js.map +1 -0
- package/dist/adapters/userMessageEvent.d.ts +7 -0
- package/dist/adapters/userMessageEvent.d.ts.map +1 -0
- package/dist/adapters/userMessageEvent.js +19 -0
- package/dist/adapters/userMessageEvent.js.map +1 -0
- package/dist/codex.d.ts +11 -0
- package/dist/codex.d.ts.map +1 -0
- package/dist/codex.js +16 -0
- package/dist/codex.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -0
- package/package.json +63 -4
- package/src/adapters/Request.ts +3 -0
- package/src/adapters/UserMessageItem.ts +28 -0
- package/src/adapters/attributed.ts +21 -0
- package/src/adapters/codexFailure.ts +4 -0
- package/src/adapters/connect.ts +129 -0
- package/src/adapters/delivery.ts +137 -0
- package/src/adapters/deliveryState.ts +43 -0
- package/src/adapters/engine.ts +101 -0
- package/src/adapters/features.ts +62 -0
- package/src/adapters/handshake.ts +35 -0
- package/src/adapters/items.ts +133 -0
- package/src/adapters/mapping.ts +66 -0
- package/src/adapters/process.ts +69 -0
- package/src/adapters/protocol.ts +125 -0
- package/src/adapters/protocolItems.ts +113 -0
- package/src/adapters/rateLimits.ts +55 -0
- package/src/adapters/reads.ts +93 -0
- package/src/adapters/recovery.ts +44 -0
- package/src/adapters/residualApproval.ts +23 -0
- package/src/adapters/rpc.ts +8 -0
- package/src/adapters/rpcProtocol.ts +28 -0
- package/src/adapters/server.ts +96 -0
- package/src/adapters/session.ts +71 -0
- package/src/adapters/subagentItems.ts +73 -0
- package/src/adapters/tellCliVersionOnce.ts +20 -0
- package/src/adapters/threadClaims.ts +26 -0
- package/src/adapters/threadSweep.ts +79 -0
- package/src/adapters/threadTree.ts +149 -0
- package/src/adapters/turnRequests.ts +45 -0
- package/src/adapters/userMessageEvent.ts +22 -0
- package/src/codex.ts +15 -0
- package/src/config.ts +17 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Deferred, Effect, Schema, Semaphore } from "effect";
|
|
2
|
+
import { type BackendFailure, BackendUnavailable, Rejected, UnknownAcceptance } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import type { ImageInput, Input } from "@shivaedev/agent/run.ts";
|
|
4
|
+
import { deliveryState } from "#adapters/deliveryState.ts";
|
|
5
|
+
import { TurnNotification } from "#adapters/protocol.ts";
|
|
6
|
+
import type { RpcNotification } from "#adapters/rpc.ts";
|
|
7
|
+
import { AppServer } from "#adapters/server.ts";
|
|
8
|
+
import { turnRequests } from "#adapters/turnRequests.ts";
|
|
9
|
+
export const delivery = Effect.fn("Codex.delivery")(function* (
|
|
10
|
+
threadId: string,
|
|
11
|
+
settings: { model?: string; effort?: string },
|
|
12
|
+
remember: (operationId: string) => void,
|
|
13
|
+
) {
|
|
14
|
+
const server = yield* AppServer;
|
|
15
|
+
const { state, gate, close, queue } = yield* deliveryState;
|
|
16
|
+
const requests = yield* turnRequests(threadId, settings);
|
|
17
|
+
function closedError() {
|
|
18
|
+
return new BackendUnavailable({ backend: "codex", message: "Session closed before delivery reached Codex" });
|
|
19
|
+
}
|
|
20
|
+
const start = Effect.fn("Codex.start")(function* (input: ImageInput, operationId: string) {
|
|
21
|
+
if (state.closed) {
|
|
22
|
+
return yield* closedError();
|
|
23
|
+
}
|
|
24
|
+
if (state.uncertain !== undefined) {
|
|
25
|
+
return yield* new UnknownAcceptance({
|
|
26
|
+
backend: "codex",
|
|
27
|
+
message: "Reconcile the previous submission before sending another turn",
|
|
28
|
+
operationId: state.uncertain,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
remember(operationId);
|
|
32
|
+
state.active = yield* requests.start(input, operationId).pipe(
|
|
33
|
+
Effect.tapError((error) =>
|
|
34
|
+
Effect.sync(() => {
|
|
35
|
+
if (error._tag !== "Rejected") {
|
|
36
|
+
state.uncertain = operationId;
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!state.finished.has(state.active)) {
|
|
43
|
+
state.finished.set(state.active, yield* Deferred.make<void, BackendFailure>());
|
|
44
|
+
}
|
|
45
|
+
return state.active;
|
|
46
|
+
});
|
|
47
|
+
const flush = Effect.gen(function* () {
|
|
48
|
+
if (state.active !== undefined || state.uncertain !== undefined || state.closed) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const next = state.pending.shift();
|
|
52
|
+
if (next === undefined) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
yield* start(next.input, next.input.id ?? crypto.randomUUID()).pipe(
|
|
56
|
+
Effect.matchEffect({
|
|
57
|
+
onFailure: (error) => Deferred.fail(next.accepted, error),
|
|
58
|
+
onSuccess: () => Deferred.succeed(next.accepted, undefined),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
const settle = Effect.fn("Codex.settle")(function* (turnId: string) {
|
|
63
|
+
if (state.active !== turnId) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
state.active = undefined;
|
|
67
|
+
const signal = state.finished.get(turnId);
|
|
68
|
+
if (signal !== undefined) {
|
|
69
|
+
yield* Deferred.succeed(signal, undefined);
|
|
70
|
+
}
|
|
71
|
+
yield* flush;
|
|
72
|
+
});
|
|
73
|
+
const track = Effect.fn("Codex.track")(function* (notification: RpcNotification) {
|
|
74
|
+
const value = Schema.decodeUnknownOption(TurnNotification)(notification.params);
|
|
75
|
+
if (value._tag === "None" || value.value.threadId !== threadId) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const id = value.value.turn.id;
|
|
79
|
+
if (notification.method === "turn/completed") {
|
|
80
|
+
yield* settle(id);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (notification.method !== "turn/started") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
state.active = id;
|
|
87
|
+
if (!state.finished.has(id)) {
|
|
88
|
+
state.finished.set(id, yield* Deferred.make<void, BackendFailure>());
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
yield* Effect.addFinalizer(() => close);
|
|
93
|
+
yield* Effect.forkScoped(server.exited.pipe(Effect.andThen(close)));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
interrupt: (turnId: string) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
if (state.active !== turnId) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const outcome = yield* requests.interrupt(turnId);
|
|
102
|
+
if (outcome === "inactive") {
|
|
103
|
+
yield* Semaphore.withPermit(gate)(settle(turnId));
|
|
104
|
+
}
|
|
105
|
+
const signal = state.finished.get(turnId);
|
|
106
|
+
if (signal !== undefined) {
|
|
107
|
+
yield* Deferred.await(signal);
|
|
108
|
+
}
|
|
109
|
+
}),
|
|
110
|
+
queue: (input: Input) => queue(input, flush),
|
|
111
|
+
recovered: (turnId: string, running: boolean) =>
|
|
112
|
+
Semaphore.withPermit(gate)(
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
state.active = running ? turnId : undefined;
|
|
115
|
+
state.uncertain = undefined;
|
|
116
|
+
if (running && !state.finished.has(turnId)) {
|
|
117
|
+
state.finished.set(turnId, yield* Deferred.make<void, BackendFailure>());
|
|
118
|
+
}
|
|
119
|
+
yield* flush;
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
start: (input: ImageInput, operationId: string) => Semaphore.withPermit(gate)(start(input, operationId)),
|
|
123
|
+
steer: (expected: string, input: Input) =>
|
|
124
|
+
Semaphore.withPermit(gate)(
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
if (state.active !== expected) {
|
|
127
|
+
return yield* new Rejected({ backend: "codex", message: "The expected turn is no longer active" });
|
|
128
|
+
}
|
|
129
|
+
const operationId = input.id ?? crypto.randomUUID();
|
|
130
|
+
remember(operationId);
|
|
131
|
+
yield* requests.steer(expected, input, operationId);
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
track: (notification: RpcNotification) => Semaphore.withPermit(gate)(track(notification)),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
export type Delivery = Effect.Success<ReturnType<typeof delivery>>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Deferred, Effect, Semaphore } from "effect";
|
|
2
|
+
import { type BackendFailure, BackendUnavailable } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import type { Input } from "@shivaedev/agent/run.ts";
|
|
4
|
+
|
|
5
|
+
interface Pending {
|
|
6
|
+
readonly accepted: Deferred.Deferred<void, BackendFailure>;
|
|
7
|
+
readonly input: Input;
|
|
8
|
+
}
|
|
9
|
+
interface State {
|
|
10
|
+
active: string | undefined;
|
|
11
|
+
closed: boolean;
|
|
12
|
+
finished: Map<string, Deferred.Deferred<void, BackendFailure>>;
|
|
13
|
+
pending: Pending[];
|
|
14
|
+
uncertain: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
export const deliveryState = Effect.gen(function* () {
|
|
17
|
+
const gate = yield* Semaphore.make(1);
|
|
18
|
+
const state: State = { active: undefined, closed: false, finished: new Map(), pending: [], uncertain: undefined };
|
|
19
|
+
const close = Effect.gen(function* () {
|
|
20
|
+
state.closed = true;
|
|
21
|
+
const error = new BackendUnavailable({ backend: "codex", message: "Session closed before delivery reached Codex" });
|
|
22
|
+
for (const next of state.pending.splice(0)) {
|
|
23
|
+
yield* Deferred.fail(next.accepted, error);
|
|
24
|
+
}
|
|
25
|
+
for (const signal of state.finished.values()) {
|
|
26
|
+
yield* Deferred.fail(signal, error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const queue = Effect.fn("Codex.queue")(function* (input: Input, flush: Effect.Effect<void>) {
|
|
30
|
+
const accepted = yield* Deferred.make<void, BackendFailure>();
|
|
31
|
+
yield* Semaphore.withPermit(gate)(
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
if (state.closed) {
|
|
34
|
+
return yield* new BackendUnavailable({ backend: "codex", message: "Session closed before delivery reached Codex" });
|
|
35
|
+
}
|
|
36
|
+
state.pending.push({ accepted, input });
|
|
37
|
+
yield* flush;
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
yield* Deferred.await(accepted);
|
|
41
|
+
});
|
|
42
|
+
return { close, gate, queue, state };
|
|
43
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Effect, PubSub, Semaphore, Stream } from "effect";
|
|
2
|
+
import type { ConfiguredBackend } from "@shivaedev/agent/backend.ts";
|
|
3
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
4
|
+
import { Rejected, UnknownAcceptance } from "@shivaedev/agent/failures.ts";
|
|
5
|
+
import type { ImageInput, StartRequest } from "@shivaedev/agent/run.ts";
|
|
6
|
+
import { features } from "#adapters/features.ts";
|
|
7
|
+
import type { LineProcess } from "#adapters/process.ts";
|
|
8
|
+
import { observe, readRequest } from "#adapters/reads.ts";
|
|
9
|
+
import { receiptOf, recovery } from "#adapters/recovery.ts";
|
|
10
|
+
import { AppServer, openServer } from "#adapters/server.ts";
|
|
11
|
+
import { openSession, type Session } from "#adapters/session.ts";
|
|
12
|
+
import { sweepSpawnedDescendants } from "#adapters/threadSweep.ts";
|
|
13
|
+
import type { CodexConfiguration } from "#config.ts";
|
|
14
|
+
export const engine = Effect.fn("Codex.engine")(function* (config: CodexConfiguration, spawn?: () => LineProcess) {
|
|
15
|
+
const server = yield* openServer(config, spawn);
|
|
16
|
+
const sessions = new Map<string, Session>();
|
|
17
|
+
const { operations, refusals, reconcile } = yield* recovery(sessions).pipe(Effect.provideService(AppServer, server));
|
|
18
|
+
const submissionGate = yield* Semaphore.make(1);
|
|
19
|
+
const submit = Effect.fn("Codex.submit")(function* (
|
|
20
|
+
request: Omit<StartRequest, "input"> & { readonly input: ImageInput },
|
|
21
|
+
reference?: { value: string; version: number },
|
|
22
|
+
) {
|
|
23
|
+
if (operations.has(request.operationId)) {
|
|
24
|
+
const result = yield* reconcile(request.operationId);
|
|
25
|
+
if (result.type === "accepted") {
|
|
26
|
+
return result.receipt;
|
|
27
|
+
}
|
|
28
|
+
if (result.type === "not-accepted") {
|
|
29
|
+
return yield* new Rejected({ backend: "codex", message: result.evidence });
|
|
30
|
+
}
|
|
31
|
+
return yield* new UnknownAcceptance({
|
|
32
|
+
backend: "codex",
|
|
33
|
+
message: result.reason,
|
|
34
|
+
operationId: request.operationId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (reference !== undefined && (reference.version !== 1 || server.claims.isNode(reference.value))) {
|
|
38
|
+
return yield* new Rejected({ backend: "codex", message: "Child threads are observed passively and cannot be resumed" });
|
|
39
|
+
}
|
|
40
|
+
operations.set(request.operationId, reference?.value);
|
|
41
|
+
const session = yield* openSession(config, request, reference?.value, (operationId, threadId) => {
|
|
42
|
+
operations.set(operationId, threadId);
|
|
43
|
+
}).pipe(Effect.provideService(AppServer, server));
|
|
44
|
+
operations.set(request.operationId, session.threadId);
|
|
45
|
+
sessions.set(session.threadId, session);
|
|
46
|
+
yield* Effect.addFinalizer(() =>
|
|
47
|
+
Effect.sync(() => {
|
|
48
|
+
if (sessions.get(session.threadId) === session) {
|
|
49
|
+
sessions.delete(session.threadId);
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
const turnId = yield* session.driver.start(request.input, request.operationId);
|
|
54
|
+
return receiptOf(request.operationId, session.threadId, turnId);
|
|
55
|
+
});
|
|
56
|
+
function start(request: Omit<StartRequest, "input"> & { readonly input: ImageInput }, session?: { value: string; version: number }) {
|
|
57
|
+
return Semaphore.withPermit(submissionGate)(submit(request, session)).pipe(
|
|
58
|
+
Effect.tapError((error) =>
|
|
59
|
+
Effect.sync(() => {
|
|
60
|
+
if (error._tag === "Rejected") {
|
|
61
|
+
refusals.set(request.operationId, error.message);
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const backend: ConfiguredBackend = {
|
|
68
|
+
backend: "codex",
|
|
69
|
+
events: (receipt) =>
|
|
70
|
+
Stream.unwrap(
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const session = sessions.get(receipt.session?.value ?? "");
|
|
73
|
+
if (session === undefined) {
|
|
74
|
+
return Stream.empty;
|
|
75
|
+
}
|
|
76
|
+
const subscription = yield* PubSub.subscribe(session.events);
|
|
77
|
+
return Stream.fromIterable<RunEvent>([
|
|
78
|
+
{ receipt, type: "acknowledged" },
|
|
79
|
+
{ reference: session.threadId, type: "session.opened" },
|
|
80
|
+
]).pipe(Stream.concat(Stream.fromSubscription(subscription)), Stream.interruptWhen(server.exited));
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
features: yield* features(sessions, start).pipe(Effect.provideService(AppServer, server)),
|
|
84
|
+
inspect: (receipt) => observe(receipt).pipe(Effect.provideService(AppServer, server)),
|
|
85
|
+
reconcile,
|
|
86
|
+
start,
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
backend: () => Effect.succeed(backend),
|
|
90
|
+
census: (rootThreadId: string) =>
|
|
91
|
+
sweepSpawnedDescendants((method, params) => readRequest(method, params).pipe(Effect.provideService(AppServer, server)), rootThreadId).pipe(
|
|
92
|
+
Effect.tap((children) =>
|
|
93
|
+
Effect.sync(() => {
|
|
94
|
+
for (const child of children) {
|
|
95
|
+
server.claims.claim(rootThreadId, child.threadId);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Scope } from "effect";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import type { ConfiguredFeatures, FeatureImplementations } from "@shivaedev/agent/backend.ts";
|
|
4
|
+
import type { BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
5
|
+
import { Rejected } from "@shivaedev/agent/failures.ts";
|
|
6
|
+
import type { ImageInput, Receipt, StartRequest } from "@shivaedev/agent/run.ts";
|
|
7
|
+
import { ModelListResponse } from "#adapters/protocol.ts";
|
|
8
|
+
import { decode, readRequest, transcript } from "#adapters/reads.ts";
|
|
9
|
+
import { AppServer } from "#adapters/server.ts";
|
|
10
|
+
import type { Session } from "#adapters/session.ts";
|
|
11
|
+
export const features = Effect.fn("Codex.features")(function* (
|
|
12
|
+
sessions: Map<string, Session>,
|
|
13
|
+
start: (
|
|
14
|
+
request: Omit<StartRequest, "input"> & { readonly input: ImageInput },
|
|
15
|
+
session?: { value: string; version: number },
|
|
16
|
+
) => Effect.Effect<Receipt, BackendFailure, Scope.Scope>,
|
|
17
|
+
) {
|
|
18
|
+
const server = yield* AppServer;
|
|
19
|
+
function driverFor(receipt: Receipt) {
|
|
20
|
+
return Effect.suspend(() => {
|
|
21
|
+
const session = sessions.get(receipt.session?.value ?? "");
|
|
22
|
+
return session === undefined
|
|
23
|
+
? Effect.fail(new Rejected({ backend: "codex", message: "No live session for this receipt" }))
|
|
24
|
+
: Effect.succeed(session.driver);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const models: FeatureImplementations["models"] = {
|
|
28
|
+
list: readRequest("model/list", {}).pipe(
|
|
29
|
+
Effect.flatMap((value) => decode(ModelListResponse, value)),
|
|
30
|
+
Effect.map(({ data }) =>
|
|
31
|
+
data.map((model) => ({
|
|
32
|
+
defaultEffort: model.defaultReasoningEffort,
|
|
33
|
+
efforts: model.supportedReasoningEfforts.map((effort) => effort.reasoningEffort),
|
|
34
|
+
id: model.model,
|
|
35
|
+
isDefault: model.isDefault,
|
|
36
|
+
name: model.displayName,
|
|
37
|
+
})),
|
|
38
|
+
),
|
|
39
|
+
Effect.provideService(AppServer, server),
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
imageInput: { implementation: { start }, support: "supported" },
|
|
44
|
+
interruption: {
|
|
45
|
+
implementation: { interrupt: (receipt) => driverFor(receipt).pipe(Effect.flatMap((driver) => driver.interrupt(receipt.reference.value))) },
|
|
46
|
+
support: "supported",
|
|
47
|
+
},
|
|
48
|
+
models: { implementation: models, support: "supported" },
|
|
49
|
+
queuedInput: {
|
|
50
|
+
implementation: { queue: (receipt, input) => driverFor(receipt).pipe(Effect.flatMap((driver) => driver.queue(input))) },
|
|
51
|
+
support: "supported",
|
|
52
|
+
},
|
|
53
|
+
sessions: { implementation: { resume: (session, request) => start(request, session) }, support: "supported" },
|
|
54
|
+
steering: {
|
|
55
|
+
implementation: {
|
|
56
|
+
steer: (receipt, input) => driverFor(receipt).pipe(Effect.flatMap((driver) => driver.steer(receipt.reference.value, input))),
|
|
57
|
+
},
|
|
58
|
+
support: "supported",
|
|
59
|
+
},
|
|
60
|
+
transcript: { implementation: { read: (receipt) => transcript(receipt).pipe(Effect.provideService(AppServer, server)) }, support: "supported" },
|
|
61
|
+
} satisfies ConfiguredFeatures;
|
|
62
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { InitializeResponse } from "#adapters/protocol.ts";
|
|
3
|
+
import type { Request } from "#adapters/Request.ts";
|
|
4
|
+
|
|
5
|
+
const CLIENT_INFO = { name: "platform_agent_codex", title: "Platform Codex", version: "0.0.0" };
|
|
6
|
+
|
|
7
|
+
export const MUTED_NOTIFICATIONS = [
|
|
8
|
+
"item/agentMessage/delta",
|
|
9
|
+
"item/commandExecution/outputDelta",
|
|
10
|
+
"item/fileChange/outputDelta",
|
|
11
|
+
"item/fileChange/patchUpdated",
|
|
12
|
+
"item/plan/delta",
|
|
13
|
+
"item/reasoning/summaryPartAdded",
|
|
14
|
+
"item/reasoning/summaryTextDelta",
|
|
15
|
+
"item/reasoning/textDelta",
|
|
16
|
+
"turn/diff/updated",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const decodeInitialize = Schema.decodeUnknownOption(InitializeResponse);
|
|
20
|
+
|
|
21
|
+
function versionOf(response: unknown): Option.Option<string> {
|
|
22
|
+
return Option.map(decodeInitialize(response), ({ userAgent }) => userAgent.split(" ")[0]?.split("/")[1] ?? "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Codex requires one initialize followed by initialized, with experimentalApi enabled for dynamic tools.
|
|
26
|
+
export function handshake(request: Request) {
|
|
27
|
+
return request("initialize", {
|
|
28
|
+
capabilities: {
|
|
29
|
+
experimentalApi: true,
|
|
30
|
+
optOutNotificationMethods: MUTED_NOTIFICATIONS,
|
|
31
|
+
requestAttestation: false,
|
|
32
|
+
},
|
|
33
|
+
clientInfo: CLIENT_INFO,
|
|
34
|
+
}).pipe(Effect.map(versionOf));
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Raw as RawSchema } from "@shivaedev/agent/eventData.ts";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
|
|
4
|
+
type RawPayload = typeof RawSchema.Type;
|
|
5
|
+
|
|
6
|
+
import { Option, Schema } from "effect";
|
|
7
|
+
import { KnownItem } from "#adapters/protocolItems.ts";
|
|
8
|
+
import { userMessageEvent } from "#adapters/userMessageEvent.ts";
|
|
9
|
+
|
|
10
|
+
type Item<T extends KnownItem["type"]> = Extract<KnownItem, { type: T }>;
|
|
11
|
+
|
|
12
|
+
const decodeItem = Schema.decodeUnknownOption(KnownItem);
|
|
13
|
+
|
|
14
|
+
type Started = Extract<AgentEvent, { type: "tool.started" }>;
|
|
15
|
+
|
|
16
|
+
function toolIdentity(item: KnownItem): Pick<Started, "name" | "providerName"> {
|
|
17
|
+
if (item.type === "mcpToolCall") {
|
|
18
|
+
return { name: `${item.server}: ${item.tool}`, providerName: `${item.server}/${item.tool}` };
|
|
19
|
+
}
|
|
20
|
+
return item.type === "dynamicToolCall" ? { name: item.tool } : { name: item.type };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toolInput(item: KnownItem): string {
|
|
24
|
+
switch (item.type) {
|
|
25
|
+
case "commandExecution":
|
|
26
|
+
return item.command;
|
|
27
|
+
case "dynamicToolCall":
|
|
28
|
+
return JSON.stringify(item.arguments);
|
|
29
|
+
case "fileChange":
|
|
30
|
+
return item.changes.map((change) => change.path).join("\n");
|
|
31
|
+
case "mcpToolCall":
|
|
32
|
+
return JSON.stringify(item.arguments);
|
|
33
|
+
case "webSearch":
|
|
34
|
+
return item.query;
|
|
35
|
+
default:
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTool(
|
|
41
|
+
item: KnownItem,
|
|
42
|
+
): item is Item<"commandExecution"> | Item<"dynamicToolCall"> | Item<"fileChange"> | Item<"mcpToolCall"> | Item<"webSearch"> {
|
|
43
|
+
return (
|
|
44
|
+
item.type === "commandExecution"
|
|
45
|
+
|| item.type === "dynamicToolCall"
|
|
46
|
+
|| item.type === "fileChange"
|
|
47
|
+
|| item.type === "mcpToolCall"
|
|
48
|
+
|| item.type === "webSearch"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function commandOk(item: Item<"commandExecution">): boolean {
|
|
53
|
+
return item.status === "completed" && (item.exitCode ?? 0) === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function dynamicOutcome(item: Item<"dynamicToolCall">) {
|
|
57
|
+
return {
|
|
58
|
+
ok: item.success ?? item.status === "completed",
|
|
59
|
+
output: (item.contentItems ?? []).map((part) => part.text ?? "").join("\n"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toolOutcome(item: Item<"commandExecution" | "dynamicToolCall" | "fileChange" | "mcpToolCall" | "webSearch">): {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
output: string;
|
|
66
|
+
} {
|
|
67
|
+
switch (item.type) {
|
|
68
|
+
case "commandExecution":
|
|
69
|
+
return { ok: commandOk(item), output: item.aggregatedOutput ?? "" };
|
|
70
|
+
case "dynamicToolCall":
|
|
71
|
+
return dynamicOutcome(item);
|
|
72
|
+
case "fileChange":
|
|
73
|
+
return {
|
|
74
|
+
ok: item.status === "completed",
|
|
75
|
+
output: item.changes.map((change) => change.diff).join("\n"),
|
|
76
|
+
};
|
|
77
|
+
case "mcpToolCall":
|
|
78
|
+
return {
|
|
79
|
+
ok: item.status === "completed",
|
|
80
|
+
output: JSON.stringify(item.error ?? item.result ?? null),
|
|
81
|
+
};
|
|
82
|
+
case "webSearch":
|
|
83
|
+
return { ok: true, output: "" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function reasoningText(item: Item<"reasoning">): string {
|
|
88
|
+
return [...(item.summary ?? []), ...(item.content ?? [])].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function knownStarted(raw: RawPayload, item: KnownItem): AgentEvent[] {
|
|
92
|
+
if (isTool(item)) {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
input: toolInput(item),
|
|
96
|
+
...toolIdentity(item),
|
|
97
|
+
raw,
|
|
98
|
+
toolId: item.id,
|
|
99
|
+
type: "tool.started",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
return item.type === "agentMessage" || item.type === "reasoning" || item.type === "userMessage" ? [] : [{ raw, type: "raw" }];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function knownCompleted(raw: RawPayload, item: KnownItem): AgentEvent[] {
|
|
107
|
+
if (isTool(item)) {
|
|
108
|
+
return [{ ...toolOutcome(item), raw, toolId: item.id, type: "tool.completed" }];
|
|
109
|
+
}
|
|
110
|
+
switch (item.type) {
|
|
111
|
+
case "agentMessage":
|
|
112
|
+
return [{ parts: [{ text: item.text, type: "text" }], raw, role: "agent", type: "message" }];
|
|
113
|
+
case "userMessage":
|
|
114
|
+
return [userMessageEvent(item, raw)];
|
|
115
|
+
case "reasoning": {
|
|
116
|
+
const text = reasoningText(item);
|
|
117
|
+
return text.length === 0 ? [] : [{ raw, text, type: "thinking" }];
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return [{ raw, type: "raw" }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function projectItem(project: (raw: RawPayload, item: KnownItem) => AgentEvent[]) {
|
|
125
|
+
return (raw: RawPayload, item: unknown): AgentEvent[] =>
|
|
126
|
+
Option.match(decodeItem(item), {
|
|
127
|
+
onNone: () => [{ raw, type: "raw" }],
|
|
128
|
+
onSome: (known) => project(raw, known),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const itemStarted = projectItem(knownStarted);
|
|
133
|
+
export const itemCompleted = projectItem(knownCompleted);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import { itemCompleted, itemStarted } from "#adapters/items.ts";
|
|
4
|
+
import {
|
|
5
|
+
ItemNotification,
|
|
6
|
+
ModelReroutedNotification,
|
|
7
|
+
ThreadStatusNotification,
|
|
8
|
+
TokenUsageNotification,
|
|
9
|
+
TurnNotification,
|
|
10
|
+
} from "#adapters/protocol.ts";
|
|
11
|
+
import { RATE_LIMITS_METHOD, rateLimitEvents } from "#adapters/rateLimits.ts";
|
|
12
|
+
import type { RpcNotification } from "#adapters/rpc.ts";
|
|
13
|
+
export function rawOf(kind: string, payload: unknown) {
|
|
14
|
+
return { kind, payload: JSON.stringify(payload), source: "codex" };
|
|
15
|
+
}
|
|
16
|
+
function mapped<A, E>(schema: Schema.Codec<A, E>, notification: RpcNotification, project: (value: A) => RunEvent[]): RunEvent[] {
|
|
17
|
+
return Option.match(Schema.decodeUnknownOption(schema)(notification.params), {
|
|
18
|
+
onNone: () => [{ raw: rawOf(notification.method, notification.params), type: "raw" }],
|
|
19
|
+
onSome: project,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function toAgentEvents(notification: RpcNotification, model: string): RunEvent[] {
|
|
23
|
+
const { method, params } = notification;
|
|
24
|
+
const raw = rawOf(method, params);
|
|
25
|
+
switch (method) {
|
|
26
|
+
case RATE_LIMITS_METHOD:
|
|
27
|
+
return rateLimitEvents(raw, params);
|
|
28
|
+
case "item/started":
|
|
29
|
+
return mapped(ItemNotification, notification, ({ item }) => itemStarted(raw, item));
|
|
30
|
+
case "item/completed":
|
|
31
|
+
return mapped(ItemNotification, notification, ({ item }) => itemCompleted(raw, item));
|
|
32
|
+
case "turn/started":
|
|
33
|
+
return mapped(TurnNotification, notification, () => [{ raw, state: "running", type: "execution" }]);
|
|
34
|
+
case "turn/completed":
|
|
35
|
+
return mapped(TurnNotification, notification, ({ turn }) => [
|
|
36
|
+
{ raw, state: turn.status === "inProgress" ? "running" : turn.status, type: "execution" },
|
|
37
|
+
]);
|
|
38
|
+
case "thread/status/changed":
|
|
39
|
+
return mapped(ThreadStatusNotification, notification, ({ status }) =>
|
|
40
|
+
status.type === "active"
|
|
41
|
+
? [{ raw, state: status.activeFlags.length > 0 ? "awaiting-input" : "running", type: "execution" }]
|
|
42
|
+
: [{ raw, type: "raw" }],
|
|
43
|
+
);
|
|
44
|
+
case "thread/tokenUsage/updated":
|
|
45
|
+
return mapped(TokenUsageNotification, notification, ({ tokenUsage }) => {
|
|
46
|
+
const tokens = tokenUsage.total;
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
cacheReadTokens: tokens.cachedInputTokens,
|
|
50
|
+
inputTokens: tokens.inputTokens,
|
|
51
|
+
outputTokens: tokens.outputTokens,
|
|
52
|
+
scope: "cumulative",
|
|
53
|
+
type: "usage",
|
|
54
|
+
unit: "session",
|
|
55
|
+
...(model.length === 0 ? {} : { model }),
|
|
56
|
+
...(tokens.cacheWriteInputTokens === undefined ? {} : { cacheWriteTokens: tokens.cacheWriteInputTokens }),
|
|
57
|
+
raw,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
});
|
|
61
|
+
case "model/rerouted":
|
|
62
|
+
return mapped(ModelReroutedNotification, notification, ({ toModel, reason }) => [{ model: toModel, raw, reason, type: "model.changed" }]);
|
|
63
|
+
default:
|
|
64
|
+
return [{ raw, type: "raw" }];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface LineProcess {
|
|
4
|
+
readonly kill: () => void;
|
|
5
|
+
readonly onExit: (listener: (code: number | null) => void) => void;
|
|
6
|
+
readonly onLine: (listener: (line: string) => void) => void;
|
|
7
|
+
readonly onStderr: (listener: (text: string) => void) => void;
|
|
8
|
+
readonly write: (line: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SpawnLineProcessOptions {
|
|
12
|
+
readonly args: readonly string[];
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly cwd: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Codex app-server uses newline-delimited JSON over stdio.
|
|
18
|
+
export function spawnLineProcess(options: SpawnLineProcessOptions): LineProcess {
|
|
19
|
+
const child = spawn(options.command, [...options.args], {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
+
});
|
|
23
|
+
let lineListener: ((line: string) => void) | null = null;
|
|
24
|
+
let stderrListener: ((text: string) => void) | null = null;
|
|
25
|
+
const exitListeners: ((code: number | null) => void)[] = [];
|
|
26
|
+
function emitExit(code: number | null): void {
|
|
27
|
+
for (const listener of exitListeners) {
|
|
28
|
+
listener(code);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let buffer = "";
|
|
32
|
+
child.stdout.setEncoding("utf8");
|
|
33
|
+
child.stdout.on("data", (chunk: string) => {
|
|
34
|
+
buffer += chunk;
|
|
35
|
+
let newline = buffer.indexOf("\n");
|
|
36
|
+
while (newline >= 0) {
|
|
37
|
+
const line = buffer.slice(0, newline).trim();
|
|
38
|
+
buffer = buffer.slice(newline + 1);
|
|
39
|
+
if (line.length > 0) {
|
|
40
|
+
lineListener?.(line);
|
|
41
|
+
}
|
|
42
|
+
newline = buffer.indexOf("\n");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
child.stderr.setEncoding("utf8");
|
|
46
|
+
child.stderr.on("data", (chunk: string) => stderrListener?.(chunk));
|
|
47
|
+
child.on("exit", (code) => emitExit(code));
|
|
48
|
+
child.on("error", () => emitExit(null));
|
|
49
|
+
child.stdin.on("error", () => emitExit(null));
|
|
50
|
+
return {
|
|
51
|
+
// Closing stdin lets app-server exit cleanly; SIGTERM handles a child that remains alive.
|
|
52
|
+
kill: () => {
|
|
53
|
+
child.stdin.end();
|
|
54
|
+
child.kill("SIGTERM");
|
|
55
|
+
},
|
|
56
|
+
onExit: (listener) => {
|
|
57
|
+
exitListeners.push(listener);
|
|
58
|
+
},
|
|
59
|
+
onLine: (listener) => {
|
|
60
|
+
lineListener = listener;
|
|
61
|
+
},
|
|
62
|
+
onStderr: (listener) => {
|
|
63
|
+
stderrListener = listener;
|
|
64
|
+
},
|
|
65
|
+
write: (line) => {
|
|
66
|
+
child.stdin.write(`${line}\n`);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|