@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.
Files changed (188) hide show
  1. package/CHANGELOG.md +12 -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/backend.d.ts +5 -0
  16. package/dist/adapters/backend.d.ts.map +1 -0
  17. package/dist/adapters/backend.js +23 -0
  18. package/dist/adapters/backend.js.map +1 -0
  19. package/dist/adapters/census.d.ts +4 -0
  20. package/dist/adapters/census.d.ts.map +1 -0
  21. package/dist/adapters/census.js +10 -0
  22. package/dist/adapters/census.js.map +1 -0
  23. package/dist/adapters/codexFailure.d.ts +3 -0
  24. package/dist/adapters/codexFailure.d.ts.map +1 -0
  25. package/dist/adapters/codexFailure.js +5 -0
  26. package/dist/adapters/codexFailure.js.map +1 -0
  27. package/dist/adapters/configuredBackend.d.ts +6 -0
  28. package/dist/adapters/configuredBackend.d.ts.map +1 -0
  29. package/dist/adapters/configuredBackend.js +3 -0
  30. package/dist/adapters/configuredBackend.js.map +1 -0
  31. package/dist/adapters/connect.d.ts +11 -0
  32. package/dist/adapters/connect.d.ts.map +1 -0
  33. package/dist/adapters/connect.js +100 -0
  34. package/dist/adapters/connect.js.map +1 -0
  35. package/dist/adapters/delivery.d.ts +18 -0
  36. package/dist/adapters/delivery.d.ts.map +1 -0
  37. package/dist/adapters/delivery.js +115 -0
  38. package/dist/adapters/delivery.js.map +1 -0
  39. package/dist/adapters/deliveryState.d.ts +31 -0
  40. package/dist/adapters/deliveryState.d.ts.map +1 -0
  41. package/dist/adapters/deliveryState.js +29 -0
  42. package/dist/adapters/deliveryState.js.map +1 -0
  43. package/dist/adapters/engine.d.ts +8 -0
  44. package/dist/adapters/engine.d.ts.map +1 -0
  45. package/dist/adapters/engine.js +9 -0
  46. package/dist/adapters/engine.js.map +1 -0
  47. package/dist/adapters/features.d.ts +726 -0
  48. package/dist/adapters/features.d.ts.map +1 -0
  49. package/dist/adapters/features.js +46 -0
  50. package/dist/adapters/features.js.map +1 -0
  51. package/dist/adapters/handshake.d.ts +5 -0
  52. package/dist/adapters/handshake.d.ts.map +1 -0
  53. package/dist/adapters/handshake.js +30 -0
  54. package/dist/adapters/handshake.js.map +1 -0
  55. package/dist/adapters/initializeCodex.d.ts +770 -0
  56. package/dist/adapters/initializeCodex.d.ts.map +1 -0
  57. package/dist/adapters/initializeCodex.js +8 -0
  58. package/dist/adapters/initializeCodex.js.map +1 -0
  59. package/dist/adapters/initializeEngine.d.ts +774 -0
  60. package/dist/adapters/initializeEngine.d.ts.map +1 -0
  61. package/dist/adapters/initializeEngine.js +63 -0
  62. package/dist/adapters/initializeEngine.js.map +1 -0
  63. package/dist/adapters/items.d.ts +7 -0
  64. package/dist/adapters/items.d.ts.map +1 -0
  65. package/dist/adapters/items.js +105 -0
  66. package/dist/adapters/items.js.map +1 -0
  67. package/dist/adapters/mapping.d.ts +9 -0
  68. package/dist/adapters/mapping.d.ts.map +1 -0
  69. package/dist/adapters/mapping.js +57 -0
  70. package/dist/adapters/mapping.js.map +1 -0
  71. package/dist/adapters/process.d.ts +15 -0
  72. package/dist/adapters/process.d.ts.map +1 -0
  73. package/dist/adapters/process.js +55 -0
  74. package/dist/adapters/process.js.map +1 -0
  75. package/dist/adapters/protocol.d.ts +178 -0
  76. package/dist/adapters/protocol.d.ts.map +1 -0
  77. package/dist/adapters/protocol.js +97 -0
  78. package/dist/adapters/protocol.js.map +1 -0
  79. package/dist/adapters/protocolItems.d.ts +89 -0
  80. package/dist/adapters/protocolItems.d.ts.map +1 -0
  81. package/dist/adapters/protocolItems.js +100 -0
  82. package/dist/adapters/protocolItems.js.map +1 -0
  83. package/dist/adapters/rateLimits.d.ts +7 -0
  84. package/dist/adapters/rateLimits.d.ts.map +1 -0
  85. package/dist/adapters/rateLimits.js +40 -0
  86. package/dist/adapters/rateLimits.js.map +1 -0
  87. package/dist/adapters/reads.d.ts +324 -0
  88. package/dist/adapters/reads.d.ts.map +1 -0
  89. package/dist/adapters/reads.js +83 -0
  90. package/dist/adapters/reads.js.map +1 -0
  91. package/dist/adapters/recovery.d.ts +324 -0
  92. package/dist/adapters/recovery.d.ts.map +1 -0
  93. package/dist/adapters/recovery.js +39 -0
  94. package/dist/adapters/recovery.js.map +1 -0
  95. package/dist/adapters/residualApproval.d.ts +4 -0
  96. package/dist/adapters/residualApproval.d.ts.map +1 -0
  97. package/dist/adapters/residualApproval.js +21 -0
  98. package/dist/adapters/residualApproval.js.map +1 -0
  99. package/dist/adapters/rpc.d.ts +8 -0
  100. package/dist/adapters/rpc.d.ts.map +1 -0
  101. package/dist/adapters/rpc.js +2 -0
  102. package/dist/adapters/rpc.js.map +1 -0
  103. package/dist/adapters/rpcProtocol.d.ts +11 -0
  104. package/dist/adapters/rpcProtocol.d.ts.map +1 -0
  105. package/dist/adapters/rpcProtocol.js +29 -0
  106. package/dist/adapters/rpcProtocol.js.map +1 -0
  107. package/dist/adapters/server.d.ts +30 -0
  108. package/dist/adapters/server.d.ts.map +1 -0
  109. package/dist/adapters/server.js +68 -0
  110. package/dist/adapters/server.js.map +1 -0
  111. package/dist/adapters/session.d.ts +312 -0
  112. package/dist/adapters/session.d.ts.map +1 -0
  113. package/dist/adapters/session.js +45 -0
  114. package/dist/adapters/session.js.map +1 -0
  115. package/dist/adapters/subagentItems.d.ts +16 -0
  116. package/dist/adapters/subagentItems.d.ts.map +1 -0
  117. package/dist/adapters/subagentItems.js +58 -0
  118. package/dist/adapters/subagentItems.js.map +1 -0
  119. package/dist/adapters/tellCliVersionOnce.d.ts +5 -0
  120. package/dist/adapters/tellCliVersionOnce.d.ts.map +1 -0
  121. package/dist/adapters/tellCliVersionOnce.js +13 -0
  122. package/dist/adapters/tellCliVersionOnce.js.map +1 -0
  123. package/dist/adapters/threadClaims.d.ts +8 -0
  124. package/dist/adapters/threadClaims.d.ts.map +1 -0
  125. package/dist/adapters/threadClaims.js +20 -0
  126. package/dist/adapters/threadClaims.js.map +1 -0
  127. package/dist/adapters/threadSweep.d.ts +15 -0
  128. package/dist/adapters/threadSweep.d.ts.map +1 -0
  129. package/dist/adapters/threadSweep.js +57 -0
  130. package/dist/adapters/threadSweep.js.map +1 -0
  131. package/dist/adapters/threadTree.d.ts +8 -0
  132. package/dist/adapters/threadTree.d.ts.map +1 -0
  133. package/dist/adapters/threadTree.js +125 -0
  134. package/dist/adapters/threadTree.js.map +1 -0
  135. package/dist/adapters/turnRequests.d.ts +13 -0
  136. package/dist/adapters/turnRequests.d.ts.map +1 -0
  137. package/dist/adapters/turnRequests.js +27 -0
  138. package/dist/adapters/turnRequests.js.map +1 -0
  139. package/dist/adapters/userMessageEvent.d.ts +7 -0
  140. package/dist/adapters/userMessageEvent.d.ts.map +1 -0
  141. package/dist/adapters/userMessageEvent.js +19 -0
  142. package/dist/adapters/userMessageEvent.js.map +1 -0
  143. package/dist/codex.d.ts +10 -1
  144. package/dist/codex.d.ts.map +1 -1
  145. package/dist/codex.js +15 -1
  146. package/dist/codex.js.map +1 -1
  147. package/dist/config.d.ts +24 -0
  148. package/dist/config.d.ts.map +1 -0
  149. package/dist/config.js +4 -0
  150. package/dist/config.js.map +1 -0
  151. package/package.json +23 -6
  152. package/src/adapters/Request.ts +3 -0
  153. package/src/adapters/UserMessageItem.ts +28 -0
  154. package/src/adapters/attributed.ts +21 -0
  155. package/src/adapters/backend.ts +29 -0
  156. package/src/adapters/census.ts +17 -0
  157. package/src/adapters/codexFailure.ts +4 -0
  158. package/src/adapters/configuredBackend.ts +6 -0
  159. package/src/adapters/connect.ts +129 -0
  160. package/src/adapters/delivery.ts +137 -0
  161. package/src/adapters/deliveryState.ts +43 -0
  162. package/src/adapters/engine.ts +11 -0
  163. package/src/adapters/features.ts +62 -0
  164. package/src/adapters/handshake.ts +35 -0
  165. package/src/adapters/initializeCodex.ts +8 -0
  166. package/src/adapters/initializeEngine.ts +75 -0
  167. package/src/adapters/items.ts +133 -0
  168. package/src/adapters/mapping.ts +66 -0
  169. package/src/adapters/process.ts +69 -0
  170. package/src/adapters/protocol.ts +125 -0
  171. package/src/adapters/protocolItems.ts +113 -0
  172. package/src/adapters/rateLimits.ts +55 -0
  173. package/src/adapters/reads.ts +93 -0
  174. package/src/adapters/recovery.ts +44 -0
  175. package/src/adapters/residualApproval.ts +23 -0
  176. package/src/adapters/rpc.ts +8 -0
  177. package/src/adapters/rpcProtocol.ts +28 -0
  178. package/src/adapters/server.ts +96 -0
  179. package/src/adapters/session.ts +71 -0
  180. package/src/adapters/subagentItems.ts +73 -0
  181. package/src/adapters/tellCliVersionOnce.ts +20 -0
  182. package/src/adapters/threadClaims.ts +26 -0
  183. package/src/adapters/threadSweep.ts +79 -0
  184. package/src/adapters/threadTree.ts +149 -0
  185. package/src/adapters/turnRequests.ts +45 -0
  186. package/src/adapters/userMessageEvent.ts +22 -0
  187. package/src/codex.ts +15 -1
  188. package/src/config.ts +17 -0
@@ -0,0 +1,129 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { type BackendFailure, BackendUnavailable, ProtocolFailure, UnknownAcceptance } from "@shivaedev/agent/failures.ts";
3
+ import type { LineProcess } from "#adapters/process.ts";
4
+ import type { RpcNotification } from "#adapters/rpc.ts";
5
+ import { Envelope, rejected } from "#adapters/rpcProtocol.ts";
6
+
7
+ interface Pending {
8
+ readonly complete: (effect: Effect.Effect<unknown, BackendFailure>) => void;
9
+ readonly operationId: string | undefined;
10
+ }
11
+ export function connect(
12
+ child: LineProcess,
13
+ timeoutMs: number,
14
+ notified: (notification: RpcNotification) => void,
15
+ serverRequest: (id: number | string, method: string, params: unknown) => void,
16
+ exited: () => void,
17
+ ) {
18
+ const pending = new Map<number, Pending>();
19
+ let closed = false;
20
+ let nextId = 0;
21
+ function send(message: unknown) {
22
+ return child.write(JSON.stringify(message));
23
+ }
24
+ function unavailable() {
25
+ return new BackendUnavailable({ backend: "codex", message: "Codex app-server exited" });
26
+ }
27
+ function ambiguous(operationId: string, message: string) {
28
+ return new UnknownAcceptance({ backend: "codex", message, operationId });
29
+ }
30
+ child.onExit(() => {
31
+ closed = true;
32
+ for (const entry of pending.values()) {
33
+ entry.complete(
34
+ Effect.fail(
35
+ entry.operationId === undefined
36
+ ? unavailable()
37
+ : ambiguous(entry.operationId, "Codex disconnected after submission; reconcile before retrying"),
38
+ ),
39
+ );
40
+ }
41
+ pending.clear();
42
+ exited();
43
+ });
44
+ child.onLine((line) => {
45
+ let envelope: typeof Envelope.Type;
46
+ try {
47
+ envelope = Schema.decodeUnknownSync(Envelope)(JSON.parse(line));
48
+ } catch {
49
+ for (const entry of pending.values()) {
50
+ entry.complete(
51
+ Effect.fail(
52
+ entry.operationId === undefined
53
+ ? new ProtocolFailure({ backend: "codex", message: "Invalid Codex RPC envelope" })
54
+ : ambiguous(entry.operationId, "Invalid response after mutation"),
55
+ ),
56
+ );
57
+ }
58
+ pending.clear();
59
+ return;
60
+ }
61
+ dispatch(envelope);
62
+ });
63
+ function dispatch(envelope: typeof Envelope.Type) {
64
+ if (envelope.method !== undefined) {
65
+ if (envelope.id === undefined) {
66
+ notified({ method: envelope.method, params: envelope.params });
67
+ } else {
68
+ serverRequest(envelope.id, envelope.method, envelope.params);
69
+ }
70
+ return;
71
+ }
72
+ if (typeof envelope.id !== "number") {
73
+ return;
74
+ }
75
+ const entry = pending.get(envelope.id);
76
+ pending.delete(envelope.id);
77
+ if (entry === undefined) {
78
+ return;
79
+ }
80
+ if (envelope.error === undefined) {
81
+ entry.complete(Effect.succeed(envelope.result));
82
+ return;
83
+ }
84
+ const error = rejected(envelope.error);
85
+ entry.complete(
86
+ Effect.fail(
87
+ error._tag === "ProtocolFailure" && entry.operationId !== undefined
88
+ ? ambiguous(entry.operationId, "Invalid rejection after mutation")
89
+ : error,
90
+ ),
91
+ );
92
+ }
93
+
94
+ function request(method: string, params: unknown, operationId?: string) {
95
+ return Effect.callback<unknown, BackendFailure>((complete) => {
96
+ if (closed) {
97
+ complete(Effect.fail(unavailable()));
98
+ return;
99
+ }
100
+ nextId += 1;
101
+ const id = nextId;
102
+ pending.set(id, { complete, operationId });
103
+ try {
104
+ send({ id, method, params });
105
+ } catch {
106
+ pending.delete(id);
107
+ complete(Effect.fail(operationId === undefined ? unavailable() : ambiguous(operationId, "Codex write failed after submission")));
108
+ }
109
+ return Effect.sync(() => {
110
+ pending.delete(id);
111
+ });
112
+ }).pipe(
113
+ Effect.timeout(`${timeoutMs} millis`),
114
+ Effect.catchTag("TimeoutError", () =>
115
+ Effect.fail(
116
+ operationId === undefined
117
+ ? new BackendUnavailable({ backend: "codex", message: `Codex ${method} timed out` })
118
+ : ambiguous(operationId, `Codex ${method} timed out; reconcile before retrying`),
119
+ ),
120
+ ),
121
+ );
122
+ }
123
+ return {
124
+ deny: (id: number | string) => send({ error: { code: -32_601, message: "No approval handler configured" }, id }),
125
+ notify: (method: string, params: unknown) => send({ method, params }),
126
+ request,
127
+ respond: (id: number | string, result: unknown) => send({ id, result }),
128
+ };
129
+ }
@@ -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,11 @@
1
+ import { Effect } from "effect";
2
+ import { backend } from "#adapters/backend.ts";
3
+ import { census } from "#adapters/census.ts";
4
+ import { initializeEngine } from "#adapters/initializeEngine.ts";
5
+ import type { LineProcess } from "#adapters/process.ts";
6
+ import type { CodexConfiguration } from "#config.ts";
7
+
8
+ export const engine = Effect.fn("Codex.engine")(function* (config: CodexConfiguration, spawn?: () => LineProcess) {
9
+ const state = yield* initializeEngine(config, spawn);
10
+ return { backend: backend(state), census: census(state) };
11
+ });
@@ -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,8 @@
1
+ import { Effect } from "effect";
2
+ import { initializeEngine } from "#adapters/initializeEngine.ts";
3
+ import { CodexConfig } from "#config.ts";
4
+
5
+ export const initializeCodex = Effect.gen(function* () {
6
+ const config = yield* CodexConfig;
7
+ return yield* initializeEngine(config);
8
+ });
@@ -0,0 +1,75 @@
1
+ import { Effect, Semaphore } from "effect";
2
+ import { Rejected, UnknownAcceptance } from "@shivaedev/agent/failures.ts";
3
+ import type { ImageInput, Receipt, StartRequest } from "@shivaedev/agent/run.ts";
4
+ import { features } from "#adapters/features.ts";
5
+ import type { LineProcess } from "#adapters/process.ts";
6
+ import { observe, readRequest } from "#adapters/reads.ts";
7
+ import { receiptOf, recovery } from "#adapters/recovery.ts";
8
+ import { AppServer, openServer } from "#adapters/server.ts";
9
+ import { openSession, type Session } from "#adapters/session.ts";
10
+ import type { CodexConfiguration } from "#config.ts";
11
+ export const initializeEngine = Effect.fn("Codex.initializeEngine")(function* (config: CodexConfiguration, spawn?: () => LineProcess) {
12
+ const server = yield* openServer(config, spawn);
13
+ const sessions = new Map<string, Session>();
14
+ const { operations, refusals, reconcile } = yield* recovery(sessions).pipe(Effect.provideService(AppServer, server));
15
+ const submissionGate = yield* Semaphore.make(1);
16
+ const submit = Effect.fn("Codex.submit")(function* (
17
+ request: Omit<StartRequest, "input"> & { readonly input: ImageInput },
18
+ reference?: { value: string; version: number },
19
+ ) {
20
+ if (operations.has(request.operationId)) {
21
+ const result = yield* reconcile(request.operationId);
22
+ if (result.type === "accepted") {
23
+ return result.receipt;
24
+ }
25
+ if (result.type === "not-accepted") {
26
+ return yield* new Rejected({ backend: "codex", message: result.evidence });
27
+ }
28
+ return yield* new UnknownAcceptance({
29
+ backend: "codex",
30
+ message: result.reason,
31
+ operationId: request.operationId,
32
+ });
33
+ }
34
+ if (reference !== undefined && (reference.version !== 1 || server.claims.isNode(reference.value))) {
35
+ return yield* new Rejected({ backend: "codex", message: "Child threads are observed passively and cannot be resumed" });
36
+ }
37
+ operations.set(request.operationId, reference?.value);
38
+ const session = yield* openSession(config, request, reference?.value, (operationId, threadId) => {
39
+ operations.set(operationId, threadId);
40
+ }).pipe(Effect.provideService(AppServer, server));
41
+ operations.set(request.operationId, session.threadId);
42
+ sessions.set(session.threadId, session);
43
+ yield* Effect.addFinalizer(() =>
44
+ Effect.sync(() => {
45
+ if (sessions.get(session.threadId) === session) {
46
+ sessions.delete(session.threadId);
47
+ }
48
+ }),
49
+ );
50
+ const turnId = yield* session.driver.start(request.input, request.operationId);
51
+ return receiptOf(request.operationId, session.threadId, turnId);
52
+ });
53
+ function start(request: Omit<StartRequest, "input"> & { readonly input: ImageInput }, session?: { value: string; version: number }) {
54
+ return Semaphore.withPermit(submissionGate)(submit(request, session)).pipe(
55
+ Effect.tapError((error) =>
56
+ Effect.sync(() => {
57
+ if (error._tag === "Rejected") {
58
+ refusals.set(request.operationId, error.message);
59
+ }
60
+ }),
61
+ ),
62
+ );
63
+ }
64
+ return {
65
+ claims: server.claims,
66
+ exited: server.exited,
67
+ features: yield* features(sessions, start).pipe(Effect.provideService(AppServer, server)),
68
+ inspect: (receipt: Receipt) => observe(receipt).pipe(Effect.provideService(AppServer, server)),
69
+ read: (method: string, params: unknown) => readRequest(method, params).pipe(Effect.provideService(AppServer, server)),
70
+ reconcile,
71
+ sessions,
72
+ start,
73
+ };
74
+ });
75
+ export type EngineState = Effect.Success<ReturnType<typeof initializeEngine>>;
@@ -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);