@shivaedev/agent-codex 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 +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/backend.d.ts +5 -0
- package/dist/adapters/backend.d.ts.map +1 -0
- package/dist/adapters/backend.js +23 -0
- package/dist/adapters/backend.js.map +1 -0
- package/dist/adapters/census.d.ts +4 -0
- package/dist/adapters/census.d.ts.map +1 -0
- package/dist/adapters/census.js +10 -0
- package/dist/adapters/census.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/configuredBackend.d.ts +6 -0
- package/dist/adapters/configuredBackend.d.ts.map +1 -0
- package/dist/adapters/configuredBackend.js +3 -0
- package/dist/adapters/configuredBackend.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 +8 -0
- package/dist/adapters/engine.d.ts.map +1 -0
- package/dist/adapters/engine.js +9 -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/initializeCodex.d.ts +770 -0
- package/dist/adapters/initializeCodex.d.ts.map +1 -0
- package/dist/adapters/initializeCodex.js +8 -0
- package/dist/adapters/initializeCodex.js.map +1 -0
- package/dist/adapters/initializeEngine.d.ts +774 -0
- package/dist/adapters/initializeEngine.d.ts.map +1 -0
- package/dist/adapters/initializeEngine.js +63 -0
- package/dist/adapters/initializeEngine.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 +10 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +15 -1
- package/dist/codex.js.map +1 -1
- 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 +23 -6
- package/src/adapters/Request.ts +3 -0
- package/src/adapters/UserMessageItem.ts +28 -0
- package/src/adapters/attributed.ts +21 -0
- package/src/adapters/backend.ts +29 -0
- package/src/adapters/census.ts +17 -0
- package/src/adapters/codexFailure.ts +4 -0
- package/src/adapters/configuredBackend.ts +6 -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 +11 -0
- package/src/adapters/features.ts +62 -0
- package/src/adapters/handshake.ts +35 -0
- package/src/adapters/initializeCodex.ts +8 -0
- package/src/adapters/initializeEngine.ts +75 -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 -1
- package/src/config.ts +17 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { Clock, Context, Deferred, Effect, Exit, Fiber, Option, PubSub, Queue, Schema, type Scope } from "effect";
|
|
3
|
+
import { BackendUnavailable } from "@shivaedev/agent/failures.ts";
|
|
4
|
+
import { connect } from "#adapters/connect.ts";
|
|
5
|
+
import { handshake } from "#adapters/handshake.ts";
|
|
6
|
+
import { type LineProcess, spawnLineProcess } from "#adapters/process.ts";
|
|
7
|
+
import { DynamicToolCallParams } from "#adapters/protocol.ts";
|
|
8
|
+
import { residualApproval } from "#adapters/residualApproval.ts";
|
|
9
|
+
import type { RpcNotification } from "#adapters/rpc.ts";
|
|
10
|
+
import { tellCliVersionOnce } from "#adapters/tellCliVersionOnce.ts";
|
|
11
|
+
import { openThreadClaims } from "#adapters/threadClaims.ts";
|
|
12
|
+
import type { CodexConfiguration } from "#config.ts";
|
|
13
|
+
export const openServer = Effect.fn("Codex.openServer")(function* (config: CodexConfiguration, spawn?: () => LineProcess) {
|
|
14
|
+
const notifications = yield* PubSub.unbounded<RpcNotification>();
|
|
15
|
+
const requests = yield* Queue.unbounded<{ id: number | string; method: string; params: unknown }>();
|
|
16
|
+
const exited = yield* Deferred.make<void>();
|
|
17
|
+
const stderr = yield* Queue.unbounded<string>();
|
|
18
|
+
const child = yield* Effect.acquireRelease(
|
|
19
|
+
Effect.try({
|
|
20
|
+
catch: () => new BackendUnavailable({ backend: "codex", message: "Could not start Codex app-server" }),
|
|
21
|
+
try: () =>
|
|
22
|
+
spawn?.() ?? spawnLineProcess({ args: config.args ?? ["app-server"], command: config.command ?? "codex", cwd: config.cwd ?? process.cwd() }),
|
|
23
|
+
}),
|
|
24
|
+
(processHandle) => Effect.sync(() => processHandle.kill()),
|
|
25
|
+
);
|
|
26
|
+
const rpc = connect(
|
|
27
|
+
child,
|
|
28
|
+
config.requestTimeoutMs ?? 30_000,
|
|
29
|
+
(n) => {
|
|
30
|
+
PubSub.publishUnsafe(notifications, n);
|
|
31
|
+
},
|
|
32
|
+
(id, method, params) => {
|
|
33
|
+
PubSub.publishUnsafe(notifications, { method, params });
|
|
34
|
+
Queue.offerUnsafe(requests, { id, method, params });
|
|
35
|
+
},
|
|
36
|
+
() => {
|
|
37
|
+
Deferred.doneUnsafe(exited, Effect.void);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
child.onStderr((text) => {
|
|
41
|
+
Queue.offerUnsafe(stderr, text);
|
|
42
|
+
});
|
|
43
|
+
yield* Effect.forkScoped(
|
|
44
|
+
Queue.take(stderr).pipe(
|
|
45
|
+
Effect.flatMap((text) => Effect.logWarning("Codex app-server", { stderr: text })),
|
|
46
|
+
Effect.forever,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
const claims = openThreadClaims();
|
|
50
|
+
const registrations = new Map<string, Scope.Scope>();
|
|
51
|
+
const handle = Effect.fn("Codex.handle")(function* (request: { id: number | string; method: string; params: unknown }) {
|
|
52
|
+
if (request.method === "currentTime/read") {
|
|
53
|
+
const millis = yield* Clock.currentTimeMillis;
|
|
54
|
+
yield* Effect.sync(() => rpc.respond(request.id, { currentTimeAt: Math.floor(millis / 1000) }));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const declined = residualApproval(request);
|
|
58
|
+
if (declined._tag === "Some") {
|
|
59
|
+
yield* Effect.sync(() => rpc.respond(request.id, declined.value));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (request.method !== "item/tool/call") {
|
|
63
|
+
yield* Effect.sync(() => rpc.deny(request.id));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const call = Schema.decodeUnknownOption(DynamicToolCallParams)(request.params);
|
|
67
|
+
const tool =
|
|
68
|
+
Option.isSome(call) && registrations.has(call.value.threadId)
|
|
69
|
+
? config.tools?.find((candidate) => candidate.name === call.value.tool)
|
|
70
|
+
: undefined;
|
|
71
|
+
const callScope = Option.isSome(call) ? registrations.get(call.value.threadId) : undefined;
|
|
72
|
+
const outcome =
|
|
73
|
+
tool !== undefined && Option.isSome(call) && callScope !== undefined
|
|
74
|
+
? yield* tool.execute(call.value.arguments, call.value.callId).pipe(
|
|
75
|
+
Effect.forkIn(callScope),
|
|
76
|
+
Effect.flatMap(Fiber.await),
|
|
77
|
+
Effect.map((exit) => (Exit.isSuccess(exit) ? exit.value : { ok: false, text: "The session closed or tool execution failed" })),
|
|
78
|
+
)
|
|
79
|
+
: { ok: false, text: "No registered tool for this thread" };
|
|
80
|
+
yield* Effect.sync(() => rpc.respond(request.id, { contentItems: [{ text: outcome.text, type: "inputText" }], success: outcome.ok }));
|
|
81
|
+
});
|
|
82
|
+
yield* Effect.forkScoped(
|
|
83
|
+
Queue.take(requests).pipe(
|
|
84
|
+
Effect.flatMap((request) => Effect.forkScoped(handle(request))),
|
|
85
|
+
Effect.forever,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
const version = yield* handshake(rpc.request);
|
|
89
|
+
yield* Effect.sync(() => rpc.notify("initialized", {}));
|
|
90
|
+
const tell = yield* tellCliVersionOnce;
|
|
91
|
+
yield* tell(version);
|
|
92
|
+
return { ...rpc, claims, exited: Deferred.await(exited), notifications, registrations };
|
|
93
|
+
});
|
|
94
|
+
export type CodexServer = Effect.Success<ReturnType<typeof openServer>>;
|
|
95
|
+
|
|
96
|
+
export class AppServer extends Context.Service<AppServer, CodexServer>()("@shivaedev/agent-codex/AppServer") {}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Effect, PubSub, Scope, Stream } from "effect";
|
|
2
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import type { StartRequest } from "@shivaedev/agent/run.ts";
|
|
4
|
+
import { delivery } from "#adapters/delivery.ts";
|
|
5
|
+
import { toAgentEvents } from "#adapters/mapping.ts";
|
|
6
|
+
import { ThreadResponse } from "#adapters/protocol.ts";
|
|
7
|
+
import { decode } from "#adapters/reads.ts";
|
|
8
|
+
import { AppServer } from "#adapters/server.ts";
|
|
9
|
+
import { openThreadTree } from "#adapters/threadTree.ts";
|
|
10
|
+
import type { CodexConfiguration } from "#config.ts";
|
|
11
|
+
export const openSession = Effect.fn("Codex.openSession")(function* (
|
|
12
|
+
config: CodexConfiguration,
|
|
13
|
+
request: Omit<StartRequest, "input">,
|
|
14
|
+
resume: string | undefined,
|
|
15
|
+
remember: (operationId: string, threadId: string) => void,
|
|
16
|
+
) {
|
|
17
|
+
const server = yield* AppServer;
|
|
18
|
+
const threadTools = config.tools?.map(({ name, description, inputSchema }) => ({ description, inputSchema, name, type: "function" }));
|
|
19
|
+
const opening = resume === undefined ? { ...(threadTools?.length ? { dynamicTools: threadTools } : {}) } : { threadId: resume };
|
|
20
|
+
const response = yield* server
|
|
21
|
+
.request(
|
|
22
|
+
resume === undefined ? "thread/start" : "thread/resume",
|
|
23
|
+
{
|
|
24
|
+
approvalsReviewer: "auto_review",
|
|
25
|
+
cwd: request.cwd,
|
|
26
|
+
model: request.model,
|
|
27
|
+
sandbox: config.sandbox ?? "workspace-write",
|
|
28
|
+
...(config.baseInstructions === undefined ? {} : { baseInstructions: config.baseInstructions }),
|
|
29
|
+
...opening,
|
|
30
|
+
},
|
|
31
|
+
request.operationId,
|
|
32
|
+
)
|
|
33
|
+
.pipe(Effect.flatMap((value) => decode(ThreadResponse, value)));
|
|
34
|
+
const threadId = response.thread.id;
|
|
35
|
+
const driver = yield* delivery(
|
|
36
|
+
threadId,
|
|
37
|
+
{
|
|
38
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
39
|
+
...(request.effort === undefined ? {} : { effort: request.effort }),
|
|
40
|
+
},
|
|
41
|
+
(operationId) => remember(operationId, threadId),
|
|
42
|
+
);
|
|
43
|
+
const toolScope = yield* Scope.fork(yield* Effect.scope);
|
|
44
|
+
server.registrations.set(threadId, toolScope);
|
|
45
|
+
const events = yield* PubSub.unbounded<RunEvent>();
|
|
46
|
+
const tree = openThreadTree(threadId, server.claims, request.model ?? "");
|
|
47
|
+
const subscription = yield* PubSub.subscribe(server.notifications);
|
|
48
|
+
yield* Effect.forkScoped(
|
|
49
|
+
Stream.fromSubscription(subscription).pipe(
|
|
50
|
+
Stream.interruptWhen(server.exited),
|
|
51
|
+
Stream.runForEach((notification) =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const mapped =
|
|
54
|
+
notification.method === "account/rateLimits/updated" ? toAgentEvents(notification, request.model ?? "") : tree.events(notification);
|
|
55
|
+
for (const event of mapped) {
|
|
56
|
+
yield* PubSub.publish(events, event);
|
|
57
|
+
}
|
|
58
|
+
yield* driver.track(notification);
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
yield* Effect.addFinalizer(() =>
|
|
64
|
+
Effect.sync(() => {
|
|
65
|
+
server.registrations.delete(threadId);
|
|
66
|
+
server.claims.release(threadId);
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
return { driver, events, threadId };
|
|
70
|
+
});
|
|
71
|
+
export type Session = Effect.Success<ReturnType<typeof openSession>>;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
|
|
9
|
+
type Item<T extends KnownItem["type"]> = Extract<KnownItem, { type: T }>;
|
|
10
|
+
|
|
11
|
+
export type SubAgentActivity = Item<"subAgentActivity">;
|
|
12
|
+
export type CollabCall = Item<"collabAgentToolCall">;
|
|
13
|
+
|
|
14
|
+
const decodeItem = Schema.decodeUnknownOption(KnownItem);
|
|
15
|
+
|
|
16
|
+
export function subAgentItem(item: unknown): SubAgentActivity | CollabCall | undefined {
|
|
17
|
+
return Option.match(decodeItem(item), {
|
|
18
|
+
onNone: () => undefined,
|
|
19
|
+
onSome: (known) => (known.type === "subAgentActivity" || known.type === "collabAgentToolCall" ? known : undefined),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function announced(item: SubAgentActivity, threadId: string, spawnedBy: string, raw: RawPayload): AgentEvent {
|
|
24
|
+
return {
|
|
25
|
+
childRef: item.agentThreadId,
|
|
26
|
+
label: item.agentPath,
|
|
27
|
+
parentRef: threadId,
|
|
28
|
+
raw,
|
|
29
|
+
spawnedBy,
|
|
30
|
+
type: "child.opened",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Codex reports forced sub-agent termination as `interrupted`.
|
|
35
|
+
export function interrupted(item: SubAgentActivity, raw: RawPayload): AgentEvent {
|
|
36
|
+
return {
|
|
37
|
+
childRef: item.agentThreadId,
|
|
38
|
+
outcome: "interrupted",
|
|
39
|
+
raw,
|
|
40
|
+
type: "child.ended",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function closedWithoutWord(childRef: string, raw: RawPayload): AgentEvent {
|
|
45
|
+
return {
|
|
46
|
+
childRef,
|
|
47
|
+
outcome: "unknown",
|
|
48
|
+
raw,
|
|
49
|
+
type: "child.ended",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function collabEvents(item: CollabCall, raw: RawPayload, started: boolean): readonly AgentEvent[] {
|
|
54
|
+
return started
|
|
55
|
+
? [
|
|
56
|
+
{
|
|
57
|
+
input: item.prompt ?? "",
|
|
58
|
+
name: item.tool,
|
|
59
|
+
raw,
|
|
60
|
+
toolId: item.id,
|
|
61
|
+
type: "tool.started",
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: [
|
|
65
|
+
{
|
|
66
|
+
ok: item.status === "completed",
|
|
67
|
+
output: item.receiverThreadIds.join("\n"),
|
|
68
|
+
raw,
|
|
69
|
+
toolId: item.id,
|
|
70
|
+
type: "tool.completed",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Effect, Option, Ref } from "effect";
|
|
2
|
+
import { PINNED_CLI_VERSION } from "#adapters/protocol.ts";
|
|
3
|
+
|
|
4
|
+
type TellCliVersion = (version: Option.Option<string>) => Effect.Effect<void>;
|
|
5
|
+
|
|
6
|
+
// Codex has no protocol negotiation; the userAgent it reports at initialize is compared with the pinned CLI version.
|
|
7
|
+
function tell(version: Option.Option<string>): Effect.Effect<void> {
|
|
8
|
+
return Option.match(version, {
|
|
9
|
+
onNone: () => Effect.logWarning("codex: initialize response unrecognised"),
|
|
10
|
+
onSome: (installed) =>
|
|
11
|
+
installed === PINNED_CLI_VERSION
|
|
12
|
+
? Effect.logInfo("codex app-server", { version: installed })
|
|
13
|
+
: Effect.logWarning("codex app-server version differs from the pin", { pinned: PINNED_CLI_VERSION, version: installed }),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const tellCliVersionOnce: Effect.Effect<TellCliVersion> = Effect.map(
|
|
18
|
+
Ref.make(false),
|
|
19
|
+
(told) => (version) => Effect.flatMap(Ref.getAndSet(told, true), (already) => (already ? Effect.void : tell(version))),
|
|
20
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ThreadClaims {
|
|
2
|
+
readonly claim: (rootThreadId: string, threadId: string) => void;
|
|
3
|
+
readonly isNode: (threadId: string) => boolean;
|
|
4
|
+
readonly ownerOf: (threadId: string) => string | undefined;
|
|
5
|
+
readonly release: (rootThreadId: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function openThreadClaims(): ThreadClaims {
|
|
9
|
+
const owners = new Map<string, string>();
|
|
10
|
+
return {
|
|
11
|
+
claim: (rootThreadId, threadId) => {
|
|
12
|
+
if (!owners.has(threadId)) {
|
|
13
|
+
owners.set(threadId, rootThreadId);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
isNode: (threadId) => owners.has(threadId),
|
|
17
|
+
ownerOf: (threadId) => owners.get(threadId),
|
|
18
|
+
release: (rootThreadId) => {
|
|
19
|
+
for (const [threadId, owner] of [...owners]) {
|
|
20
|
+
if (owner === rootThreadId) {
|
|
21
|
+
owners.delete(threadId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { ReadFailure as BackendFailure } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import { codexFailure } from "#adapters/codexFailure.ts";
|
|
4
|
+
import { ThreadListResponse, type ThreadStatus } from "#adapters/protocol.ts";
|
|
5
|
+
|
|
6
|
+
type Request = (method: string, params: unknown) => Effect.Effect<unknown, BackendFailure>;
|
|
7
|
+
|
|
8
|
+
export interface SpawnedChild {
|
|
9
|
+
readonly agentNickname: string | undefined;
|
|
10
|
+
readonly agentPath: string | undefined;
|
|
11
|
+
readonly agentRole: string | undefined;
|
|
12
|
+
readonly parentThreadId: string;
|
|
13
|
+
readonly threadId: string;
|
|
14
|
+
readonly working: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CensusSweep = readonly SpawnedChild[];
|
|
18
|
+
|
|
19
|
+
// Codex caps thread-list pages at 100 rows.
|
|
20
|
+
const PAGE = 100;
|
|
21
|
+
const PAGES = 200;
|
|
22
|
+
|
|
23
|
+
const decodeListing = Schema.decodeUnknownOption(ThreadListResponse);
|
|
24
|
+
|
|
25
|
+
function named(value: string | null | undefined): string | undefined {
|
|
26
|
+
return value === null ? undefined : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isWorking(status: typeof ThreadStatus.Type): boolean {
|
|
30
|
+
return status.type === "active";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function childrenOf(listed: typeof ThreadListResponse.Type): readonly SpawnedChild[] {
|
|
34
|
+
return listed.data.map((thread) => {
|
|
35
|
+
const spawn = thread.source.subAgent.threadSpawn;
|
|
36
|
+
return {
|
|
37
|
+
agentNickname: named(spawn.agentNickname),
|
|
38
|
+
agentPath: named(spawn.agentPath),
|
|
39
|
+
agentRole: named(spawn.agentRole),
|
|
40
|
+
parentThreadId: spawn.parentThreadId,
|
|
41
|
+
threadId: thread.id,
|
|
42
|
+
working: isWorking(thread.status),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Codex thread/list with ancestorThreadId returns the descendant tree without attaching to it.
|
|
48
|
+
export const sweepSpawnedDescendants = Effect.fn("Codex.sweepSpawnedDescendants")(function* (
|
|
49
|
+
request: Request,
|
|
50
|
+
rootThreadId: string,
|
|
51
|
+
): Effect.fn.Return<CensusSweep, BackendFailure> {
|
|
52
|
+
const found = new Map<string, SpawnedChild>();
|
|
53
|
+
const cursors = new Set<string>();
|
|
54
|
+
let cursor: string | undefined;
|
|
55
|
+
for (let page = 0; page < PAGES; page += 1) {
|
|
56
|
+
const response = yield* request("thread/list", {
|
|
57
|
+
ancestorThreadId: rootThreadId,
|
|
58
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
59
|
+
limit: PAGE,
|
|
60
|
+
});
|
|
61
|
+
const decoded = decodeListing(response);
|
|
62
|
+
if (Option.isNone(decoded)) {
|
|
63
|
+
return yield* Effect.fail(codexFailure("it answered in a shape this backend cannot read"));
|
|
64
|
+
}
|
|
65
|
+
for (const child of childrenOf(decoded.value)) {
|
|
66
|
+
found.set(child.threadId, child);
|
|
67
|
+
}
|
|
68
|
+
const nextCursor = named(decoded.value.nextCursor);
|
|
69
|
+
if (nextCursor === undefined) {
|
|
70
|
+
return [...found.values()];
|
|
71
|
+
}
|
|
72
|
+
if (cursors.has(nextCursor)) {
|
|
73
|
+
return yield* codexFailure("Repeated descendant pagination cursor");
|
|
74
|
+
}
|
|
75
|
+
cursors.add(nextCursor);
|
|
76
|
+
cursor = nextCursor;
|
|
77
|
+
}
|
|
78
|
+
return yield* Effect.fail(codexFailure("its listing of this session's threads never ended"));
|
|
79
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Origin as OriginSchema } from "@shivaedev/agent/eventData.ts";
|
|
2
|
+
import type { RunEvent as AgentEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
|
|
4
|
+
type Origin = typeof OriginSchema.Type;
|
|
5
|
+
|
|
6
|
+
import type { Raw as RawSchema } from "@shivaedev/agent/eventData.ts";
|
|
7
|
+
|
|
8
|
+
type RawPayload = typeof RawSchema.Type;
|
|
9
|
+
|
|
10
|
+
import { Option, Schema } from "effect";
|
|
11
|
+
import { attributed } from "#adapters/attributed.ts";
|
|
12
|
+
import { rawOf, toAgentEvents } from "#adapters/mapping.ts";
|
|
13
|
+
import { ItemNotification, SpawnedThread, ThreadScoped, ThreadSettingsUpdatedNotification } from "#adapters/protocol.ts";
|
|
14
|
+
import type { RpcNotification } from "#adapters/rpc.ts";
|
|
15
|
+
import {
|
|
16
|
+
announced,
|
|
17
|
+
type CollabCall,
|
|
18
|
+
closedWithoutWord,
|
|
19
|
+
collabEvents,
|
|
20
|
+
interrupted,
|
|
21
|
+
type SubAgentActivity,
|
|
22
|
+
subAgentItem,
|
|
23
|
+
} from "#adapters/subagentItems.ts";
|
|
24
|
+
import type { ThreadClaims } from "#adapters/threadClaims.ts";
|
|
25
|
+
|
|
26
|
+
const decodeScoped = Schema.decodeUnknownOption(ThreadScoped);
|
|
27
|
+
const decodeSpawned = Schema.decodeUnknownOption(SpawnedThread);
|
|
28
|
+
const decodeItem = Schema.decodeUnknownOption(ItemNotification);
|
|
29
|
+
const decodeSettings = Schema.decodeUnknownOption(ThreadSettingsUpdatedNotification);
|
|
30
|
+
|
|
31
|
+
export interface ThreadTree {
|
|
32
|
+
readonly events: (notification: RpcNotification) => readonly AgentEvent[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Ownership is tracked even when the application has no event subscriber.
|
|
36
|
+
export function openThreadTree(rootThreadId: string, claims: ThreadClaims, sessionModel: string): ThreadTree {
|
|
37
|
+
const spawnCalls = new Map<string, string>();
|
|
38
|
+
const stated = new Set<string>();
|
|
39
|
+
const billing = new Map<string, string>();
|
|
40
|
+
function owns(threadId: string): boolean {
|
|
41
|
+
return threadId === rootThreadId || claims.ownerOf(threadId) === rootThreadId;
|
|
42
|
+
}
|
|
43
|
+
function billedOn(threadId: string): string {
|
|
44
|
+
return billing.get(threadId) ?? sessionModel;
|
|
45
|
+
}
|
|
46
|
+
// A thread spends on what the thread that spawned it was spending on, until something names a model for it.
|
|
47
|
+
function claimed(parent: string, child: string): void {
|
|
48
|
+
claims.claim(rootThreadId, child);
|
|
49
|
+
if (!billing.has(child)) {
|
|
50
|
+
billing.set(child, billedOn(parent));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function once(key: string): boolean {
|
|
54
|
+
if (stated.has(key)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
stated.add(key);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function spawnedThread(params: unknown): readonly AgentEvent[] {
|
|
61
|
+
const spawned = decodeSpawned(params);
|
|
62
|
+
if (Option.isNone(spawned)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const { id, source } = spawned.value.thread;
|
|
66
|
+
const parent = source.subAgent.threadSpawn.parentThreadId;
|
|
67
|
+
if (owns(parent)) {
|
|
68
|
+
claimed(parent, id);
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
function collab(item: CollabCall, threadId: string, raw: RawPayload, started: boolean): readonly AgentEvent[] {
|
|
73
|
+
for (const receiver of item.receiverThreadIds) {
|
|
74
|
+
claimed(threadId, receiver);
|
|
75
|
+
spawnCalls.set(receiver, item.id);
|
|
76
|
+
}
|
|
77
|
+
return collabEvents(item, raw, started);
|
|
78
|
+
}
|
|
79
|
+
function activity(item: SubAgentActivity, threadId: string, raw: RawPayload): readonly AgentEvent[] {
|
|
80
|
+
claimed(threadId, item.agentThreadId);
|
|
81
|
+
const node = item.agentThreadId;
|
|
82
|
+
if (item.kind === "interrupted") {
|
|
83
|
+
return once(`ended/${node}`) ? [interrupted(item, raw)] : [{ raw, type: "raw" }];
|
|
84
|
+
}
|
|
85
|
+
return item.kind !== "started" || !once(`opened/${node}`)
|
|
86
|
+
? [{ raw, type: "raw" }]
|
|
87
|
+
: [announced(item, threadId, spawnCalls.get(node) ?? item.id, raw)];
|
|
88
|
+
}
|
|
89
|
+
function lifecycle(notification: RpcNotification, threadId: string): readonly AgentEvent[] | undefined {
|
|
90
|
+
const decoded = decodeItem(notification.params);
|
|
91
|
+
if (Option.isNone(decoded)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const item = subAgentItem(decoded.value.item);
|
|
95
|
+
if (item === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const raw = rawOf(notification.method, notification.params);
|
|
99
|
+
return item.type === "collabAgentToolCall" ? collab(item, threadId, raw, notification.method === "item/started") : activity(item, threadId, raw);
|
|
100
|
+
}
|
|
101
|
+
function closed(threadId: string, params: unknown): readonly AgentEvent[] {
|
|
102
|
+
const raw = rawOf("thread/closed", params);
|
|
103
|
+
return once(`ended/${threadId}`) ? [closedWithoutWord(threadId, raw)] : [{ raw, type: "raw" }];
|
|
104
|
+
}
|
|
105
|
+
function mapped(notification: RpcNotification, threadId: string, root: boolean): readonly AgentEvent[] {
|
|
106
|
+
return !root && notification.method === "thread/closed"
|
|
107
|
+
? closed(threadId, notification.params)
|
|
108
|
+
: (lifecycle(notification, threadId) ?? toAgentEvents(notification, billedOn(threadId)));
|
|
109
|
+
}
|
|
110
|
+
function rebill(threadId: string, foundEvents: readonly AgentEvent[]): void {
|
|
111
|
+
for (const mappedEvent of foundEvents) {
|
|
112
|
+
if (mappedEvent.type === "model.changed") {
|
|
113
|
+
billing.set(threadId, mappedEvent.model);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function settled(notification: RpcNotification, threadId: string): void {
|
|
118
|
+
if (notification.method !== "thread/settings/updated") {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const settings = decodeSettings(notification.params);
|
|
122
|
+
if (Option.isSome(settings)) {
|
|
123
|
+
billing.set(threadId, settings.value.threadSettings.model);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function events(notification: RpcNotification): readonly AgentEvent[] {
|
|
127
|
+
if (notification.method === "thread/started") {
|
|
128
|
+
return spawnedThread(notification.params);
|
|
129
|
+
}
|
|
130
|
+
const scoped = decodeScoped(notification.params);
|
|
131
|
+
if (Option.isNone(scoped) || !owns(scoped.value.threadId)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const threadId = scoped.value.threadId;
|
|
135
|
+
const root = threadId === rootThreadId;
|
|
136
|
+
settled(notification, threadId);
|
|
137
|
+
const found = mapped(notification, threadId, root);
|
|
138
|
+
rebill(threadId, found);
|
|
139
|
+
if (root) {
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
const origin: Origin = {
|
|
143
|
+
node: threadId,
|
|
144
|
+
spawnedBy: spawnCalls.get(threadId) ?? threadId,
|
|
145
|
+
};
|
|
146
|
+
return found.map((event) => attributed(event, origin));
|
|
147
|
+
}
|
|
148
|
+
return { events };
|
|
149
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { UnknownAcceptance } from "@shivaedev/agent/failures.ts";
|
|
3
|
+
import type { ImageInput, Input } from "@shivaedev/agent/run.ts";
|
|
4
|
+
import { TurnResponse } from "#adapters/protocol.ts";
|
|
5
|
+
import { decode } from "#adapters/reads.ts";
|
|
6
|
+
import { AppServer } from "#adapters/server.ts";
|
|
7
|
+
|
|
8
|
+
function userInput(input: ImageInput) {
|
|
9
|
+
return input.parts.map((part) =>
|
|
10
|
+
part.type === "text" ? { text: part.text, "text_elements": [], type: "text" } : { path: part.path, type: "localImage" },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
export const turnRequests = Effect.fn("Codex.turnRequests")(function* (threadId: string, settings: { model?: string; effort?: string }) {
|
|
14
|
+
const server = yield* AppServer;
|
|
15
|
+
return {
|
|
16
|
+
interrupt: (turnId: string) =>
|
|
17
|
+
server.request("turn/interrupt", { threadId, turnId }, turnId).pipe(
|
|
18
|
+
Effect.as("draining" as const),
|
|
19
|
+
Effect.catchTag("Rejected", (error) => {
|
|
20
|
+
if (error.message.includes("no active turn")) {
|
|
21
|
+
return Effect.succeed("inactive" as const);
|
|
22
|
+
}
|
|
23
|
+
if (error.message.includes("timeout waiting")) {
|
|
24
|
+
return Effect.succeed("draining" as const);
|
|
25
|
+
}
|
|
26
|
+
return Effect.fail(error);
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
start: (input: ImageInput, operationId: string) =>
|
|
30
|
+
server.request("turn/start", { clientUserMessageId: operationId, input: userInput(input), threadId, ...settings }, operationId).pipe(
|
|
31
|
+
Effect.flatMap((value) =>
|
|
32
|
+
decode(TurnResponse, value).pipe(
|
|
33
|
+
Effect.mapError(
|
|
34
|
+
() => new UnknownAcceptance({ backend: "codex", message: "Codex accepted a mutation but returned no readable turn", operationId }),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
Effect.map(({ turn }) => turn.id),
|
|
39
|
+
),
|
|
40
|
+
steer: (expectedTurnId: string, input: Input, operationId: string) =>
|
|
41
|
+
server
|
|
42
|
+
.request("turn/steer", { clientUserMessageId: operationId, expectedTurnId, input: userInput(input), threadId }, operationId)
|
|
43
|
+
.pipe(Effect.asVoid),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Raw } from "@shivaedev/agent/eventData.ts";
|
|
2
|
+
import type { RunEvent } from "@shivaedev/agent/events.ts";
|
|
3
|
+
import type { KnownItem } from "#adapters/protocolItems.ts";
|
|
4
|
+
|
|
5
|
+
function contentPart(part: Extract<KnownItem, { type: "userMessage" }>["content"][number]): Extract<RunEvent, { type: "message" }>["parts"][number] {
|
|
6
|
+
if (part.type === "text") {
|
|
7
|
+
return { text: part.text, type: "text" };
|
|
8
|
+
}
|
|
9
|
+
if (part.type === "localImage") {
|
|
10
|
+
return { mediaType: "application/octet-stream", path: part.path, type: "image" };
|
|
11
|
+
}
|
|
12
|
+
return { kind: "image", payload: JSON.stringify(part), type: "native" };
|
|
13
|
+
}
|
|
14
|
+
export function userMessageEvent(item: Extract<KnownItem, { type: "userMessage" }>, raw: typeof Raw.Type): RunEvent {
|
|
15
|
+
return {
|
|
16
|
+
raw,
|
|
17
|
+
role: "user",
|
|
18
|
+
type: "message",
|
|
19
|
+
...(item.clientId === null || item.clientId === undefined ? {} : { inputId: item.clientId }),
|
|
20
|
+
parts: item.content.map(contentPart),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/codex.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Backend } from "@shivaedev/agent/backend.ts";
|
|
3
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
|
+
import { backend } from "#adapters/backend.ts";
|
|
5
|
+
import { census } from "#adapters/census.ts";
|
|
6
|
+
import { configuredBackend } from "#adapters/configuredBackend.ts";
|
|
7
|
+
import { initializeCodex } from "#adapters/initializeCodex.ts";
|
|
8
|
+
import { CodexConfig } from "#config.ts";
|
|
9
|
+
export const Codex = defineService({
|
|
10
|
+
id: "@shivaedev/agent-codex/Codex",
|
|
11
|
+
initialize: initializeCodex,
|
|
12
|
+
methods: (state) => ({ backend: backend(state), census: census(state) }),
|
|
13
|
+
requires: [CodexConfig],
|
|
14
|
+
});
|
|
15
|
+
export const codexLayer = Layer.effect(Backend)(Effect.flatMap(Codex, configuredBackend)).pipe(Layer.provideMerge(Codex.layer));
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
export interface DynamicTool {
|
|
3
|
+
readonly description: string;
|
|
4
|
+
readonly execute: (input: unknown, callId: string) => Effect.Effect<{ readonly ok: boolean; readonly text: string }>;
|
|
5
|
+
readonly inputSchema: unknown;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CodexConfiguration {
|
|
9
|
+
readonly args?: readonly string[];
|
|
10
|
+
readonly baseInstructions?: string;
|
|
11
|
+
readonly command?: string;
|
|
12
|
+
readonly cwd?: string;
|
|
13
|
+
readonly requestTimeoutMs?: number;
|
|
14
|
+
readonly sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
15
|
+
readonly tools?: readonly DynamicTool[];
|
|
16
|
+
}
|
|
17
|
+
export class CodexConfig extends Context.Service<CodexConfig, CodexConfiguration>()("@shivaedev/agent-codex/CodexConfig") {}
|