@shivaedev/agent-codex 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.
Files changed (163) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +47 -1
  3. package/dist/adapters/Request.d.ts +4 -0
  4. package/dist/adapters/Request.d.ts.map +1 -0
  5. package/dist/adapters/Request.js +2 -0
  6. package/dist/adapters/Request.js.map +1 -0
  7. package/dist/adapters/UserMessageItem.d.ts +22 -0
  8. package/dist/adapters/UserMessageItem.d.ts.map +1 -0
  9. package/dist/adapters/UserMessageItem.js +24 -0
  10. package/dist/adapters/UserMessageItem.js.map +1 -0
  11. package/dist/adapters/attributed.d.ts +6 -0
  12. package/dist/adapters/attributed.d.ts.map +1 -0
  13. package/dist/adapters/attributed.js +17 -0
  14. package/dist/adapters/attributed.js.map +1 -0
  15. package/dist/adapters/codexFailure.d.ts +3 -0
  16. package/dist/adapters/codexFailure.d.ts.map +1 -0
  17. package/dist/adapters/codexFailure.js +5 -0
  18. package/dist/adapters/codexFailure.js.map +1 -0
  19. package/dist/adapters/connect.d.ts +11 -0
  20. package/dist/adapters/connect.d.ts.map +1 -0
  21. package/dist/adapters/connect.js +100 -0
  22. package/dist/adapters/connect.js.map +1 -0
  23. package/dist/adapters/delivery.d.ts +18 -0
  24. package/dist/adapters/delivery.d.ts.map +1 -0
  25. package/dist/adapters/delivery.js +115 -0
  26. package/dist/adapters/delivery.js.map +1 -0
  27. package/dist/adapters/deliveryState.d.ts +31 -0
  28. package/dist/adapters/deliveryState.d.ts.map +1 -0
  29. package/dist/adapters/deliveryState.js +29 -0
  30. package/dist/adapters/deliveryState.js.map +1 -0
  31. package/dist/adapters/engine.d.ts +10 -0
  32. package/dist/adapters/engine.d.ts.map +1 -0
  33. package/dist/adapters/engine.js +80 -0
  34. package/dist/adapters/engine.js.map +1 -0
  35. package/dist/adapters/features.d.ts +726 -0
  36. package/dist/adapters/features.d.ts.map +1 -0
  37. package/dist/adapters/features.js +46 -0
  38. package/dist/adapters/features.js.map +1 -0
  39. package/dist/adapters/handshake.d.ts +5 -0
  40. package/dist/adapters/handshake.d.ts.map +1 -0
  41. package/dist/adapters/handshake.js +30 -0
  42. package/dist/adapters/handshake.js.map +1 -0
  43. package/dist/adapters/items.d.ts +7 -0
  44. package/dist/adapters/items.d.ts.map +1 -0
  45. package/dist/adapters/items.js +105 -0
  46. package/dist/adapters/items.js.map +1 -0
  47. package/dist/adapters/mapping.d.ts +9 -0
  48. package/dist/adapters/mapping.d.ts.map +1 -0
  49. package/dist/adapters/mapping.js +57 -0
  50. package/dist/adapters/mapping.js.map +1 -0
  51. package/dist/adapters/process.d.ts +15 -0
  52. package/dist/adapters/process.d.ts.map +1 -0
  53. package/dist/adapters/process.js +55 -0
  54. package/dist/adapters/process.js.map +1 -0
  55. package/dist/adapters/protocol.d.ts +178 -0
  56. package/dist/adapters/protocol.d.ts.map +1 -0
  57. package/dist/adapters/protocol.js +97 -0
  58. package/dist/adapters/protocol.js.map +1 -0
  59. package/dist/adapters/protocolItems.d.ts +89 -0
  60. package/dist/adapters/protocolItems.d.ts.map +1 -0
  61. package/dist/adapters/protocolItems.js +100 -0
  62. package/dist/adapters/protocolItems.js.map +1 -0
  63. package/dist/adapters/rateLimits.d.ts +7 -0
  64. package/dist/adapters/rateLimits.d.ts.map +1 -0
  65. package/dist/adapters/rateLimits.js +40 -0
  66. package/dist/adapters/rateLimits.js.map +1 -0
  67. package/dist/adapters/reads.d.ts +324 -0
  68. package/dist/adapters/reads.d.ts.map +1 -0
  69. package/dist/adapters/reads.js +83 -0
  70. package/dist/adapters/reads.js.map +1 -0
  71. package/dist/adapters/recovery.d.ts +324 -0
  72. package/dist/adapters/recovery.d.ts.map +1 -0
  73. package/dist/adapters/recovery.js +39 -0
  74. package/dist/adapters/recovery.js.map +1 -0
  75. package/dist/adapters/residualApproval.d.ts +4 -0
  76. package/dist/adapters/residualApproval.d.ts.map +1 -0
  77. package/dist/adapters/residualApproval.js +21 -0
  78. package/dist/adapters/residualApproval.js.map +1 -0
  79. package/dist/adapters/rpc.d.ts +8 -0
  80. package/dist/adapters/rpc.d.ts.map +1 -0
  81. package/dist/adapters/rpc.js +2 -0
  82. package/dist/adapters/rpc.js.map +1 -0
  83. package/dist/adapters/rpcProtocol.d.ts +11 -0
  84. package/dist/adapters/rpcProtocol.d.ts.map +1 -0
  85. package/dist/adapters/rpcProtocol.js +29 -0
  86. package/dist/adapters/rpcProtocol.js.map +1 -0
  87. package/dist/adapters/server.d.ts +30 -0
  88. package/dist/adapters/server.d.ts.map +1 -0
  89. package/dist/adapters/server.js +68 -0
  90. package/dist/adapters/server.js.map +1 -0
  91. package/dist/adapters/session.d.ts +312 -0
  92. package/dist/adapters/session.d.ts.map +1 -0
  93. package/dist/adapters/session.js +45 -0
  94. package/dist/adapters/session.js.map +1 -0
  95. package/dist/adapters/subagentItems.d.ts +16 -0
  96. package/dist/adapters/subagentItems.d.ts.map +1 -0
  97. package/dist/adapters/subagentItems.js +58 -0
  98. package/dist/adapters/subagentItems.js.map +1 -0
  99. package/dist/adapters/tellCliVersionOnce.d.ts +5 -0
  100. package/dist/adapters/tellCliVersionOnce.d.ts.map +1 -0
  101. package/dist/adapters/tellCliVersionOnce.js +13 -0
  102. package/dist/adapters/tellCliVersionOnce.js.map +1 -0
  103. package/dist/adapters/threadClaims.d.ts +8 -0
  104. package/dist/adapters/threadClaims.d.ts.map +1 -0
  105. package/dist/adapters/threadClaims.js +20 -0
  106. package/dist/adapters/threadClaims.js.map +1 -0
  107. package/dist/adapters/threadSweep.d.ts +15 -0
  108. package/dist/adapters/threadSweep.d.ts.map +1 -0
  109. package/dist/adapters/threadSweep.js +57 -0
  110. package/dist/adapters/threadSweep.js.map +1 -0
  111. package/dist/adapters/threadTree.d.ts +8 -0
  112. package/dist/adapters/threadTree.d.ts.map +1 -0
  113. package/dist/adapters/threadTree.js +125 -0
  114. package/dist/adapters/threadTree.js.map +1 -0
  115. package/dist/adapters/turnRequests.d.ts +13 -0
  116. package/dist/adapters/turnRequests.d.ts.map +1 -0
  117. package/dist/adapters/turnRequests.js +27 -0
  118. package/dist/adapters/turnRequests.js.map +1 -0
  119. package/dist/adapters/userMessageEvent.d.ts +7 -0
  120. package/dist/adapters/userMessageEvent.d.ts.map +1 -0
  121. package/dist/adapters/userMessageEvent.js +19 -0
  122. package/dist/adapters/userMessageEvent.js.map +1 -0
  123. package/dist/codex.d.ts +10 -1
  124. package/dist/codex.d.ts.map +1 -1
  125. package/dist/codex.js +15 -1
  126. package/dist/codex.js.map +1 -1
  127. package/dist/config.d.ts +24 -0
  128. package/dist/config.d.ts.map +1 -0
  129. package/dist/config.js +4 -0
  130. package/dist/config.js.map +1 -0
  131. package/package.json +21 -4
  132. package/src/adapters/Request.ts +3 -0
  133. package/src/adapters/UserMessageItem.ts +28 -0
  134. package/src/adapters/attributed.ts +21 -0
  135. package/src/adapters/codexFailure.ts +4 -0
  136. package/src/adapters/connect.ts +129 -0
  137. package/src/adapters/delivery.ts +137 -0
  138. package/src/adapters/deliveryState.ts +43 -0
  139. package/src/adapters/engine.ts +101 -0
  140. package/src/adapters/features.ts +62 -0
  141. package/src/adapters/handshake.ts +35 -0
  142. package/src/adapters/items.ts +133 -0
  143. package/src/adapters/mapping.ts +66 -0
  144. package/src/adapters/process.ts +69 -0
  145. package/src/adapters/protocol.ts +125 -0
  146. package/src/adapters/protocolItems.ts +113 -0
  147. package/src/adapters/rateLimits.ts +55 -0
  148. package/src/adapters/reads.ts +93 -0
  149. package/src/adapters/recovery.ts +44 -0
  150. package/src/adapters/residualApproval.ts +23 -0
  151. package/src/adapters/rpc.ts +8 -0
  152. package/src/adapters/rpcProtocol.ts +28 -0
  153. package/src/adapters/server.ts +96 -0
  154. package/src/adapters/session.ts +71 -0
  155. package/src/adapters/subagentItems.ts +73 -0
  156. package/src/adapters/tellCliVersionOnce.ts +20 -0
  157. package/src/adapters/threadClaims.ts +26 -0
  158. package/src/adapters/threadSweep.ts +79 -0
  159. package/src/adapters/threadTree.ts +149 -0
  160. package/src/adapters/turnRequests.ts +45 -0
  161. package/src/adapters/userMessageEvent.ts +22 -0
  162. package/src/codex.ts +15 -1
  163. 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
+ }