@kylecheng3146/agent-ops 0.5.3 → 0.6.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 (93) hide show
  1. package/README.md +48 -0
  2. package/dist/packages/cli/src/args.js +165 -2
  3. package/dist/packages/cli/src/bin.js +64 -17
  4. package/dist/packages/cli/src/cli.js +33 -6
  5. package/dist/packages/cli/src/commands/advance.js +23 -3
  6. package/dist/packages/cli/src/commands/review-show.js +1 -1
  7. package/dist/packages/cli/src/commands/review.js +100 -9
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +61 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/parallel/finish.js +85 -22
  26. package/dist/runtime/src/parallel/guard.js +5 -1
  27. package/dist/runtime/src/parallel/integrate.js +288 -10
  28. package/dist/runtime/src/parallel/manage.js +4 -1
  29. package/dist/runtime/src/parallel/receipt.js +71 -5
  30. package/dist/runtime/src/parallel/service.js +41 -1
  31. package/dist/runtime/src/review/attestation.js +36 -11
  32. package/dist/runtime/src/review/execute.js +24 -1
  33. package/dist/runtime/src/review/packet.js +35 -1
  34. package/dist/runtime/src/review/runner.js +12 -1
  35. package/dist/runtime/src/review/scope.js +18 -1
  36. package/dist/runtime/src/review/slots.js +35 -10
  37. package/dist/runtime/src/run/authorization.js +60 -0
  38. package/dist/runtime/src/run/budget.js +30 -0
  39. package/dist/runtime/src/run/controls.js +139 -0
  40. package/dist/runtime/src/run/convergence.js +177 -0
  41. package/dist/runtime/src/run/hosts/claude.js +180 -0
  42. package/dist/runtime/src/run/hosts/codex.js +279 -0
  43. package/dist/runtime/src/run/hosts/index.js +4 -0
  44. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  45. package/dist/runtime/src/run/hosts/types.js +1 -0
  46. package/dist/runtime/src/run/hosts/util.js +136 -0
  47. package/dist/runtime/src/run/integration.js +204 -0
  48. package/dist/runtime/src/run/macOS.js +189 -0
  49. package/dist/runtime/src/run/ownership.js +22 -0
  50. package/dist/runtime/src/run/planning.js +25 -0
  51. package/dist/runtime/src/run/policy-transition.js +1076 -0
  52. package/dist/runtime/src/run/policy.js +176 -0
  53. package/dist/runtime/src/run/ratchet.js +71 -0
  54. package/dist/runtime/src/run/scheduler.js +260 -0
  55. package/dist/runtime/src/run/service.js +425 -0
  56. package/dist/runtime/src/run/supervisor.js +635 -0
  57. package/dist/runtime/src/run/transport.js +159 -0
  58. package/dist/runtime/src/run/types.js +1 -0
  59. package/dist/runtime/src/run/usage.js +81 -0
  60. package/dist/runtime/src/run/verification.js +64 -0
  61. package/dist/runtime/src/run/worker-gate.js +48 -0
  62. package/dist/runtime/src/schema/acceptance.js +91 -0
  63. package/dist/runtime/src/schema/validate.js +67 -7
  64. package/dist/runtime/src/task/completion.js +14 -2
  65. package/dist/runtime/src/task/contract.js +17 -0
  66. package/dist/runtime/src/task/pin-finding.js +81 -0
  67. package/dist/runtime/src/task/service.js +316 -13
  68. package/dist/runtime/src/task/store.js +57 -7
  69. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  70. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  71. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  72. package/dist/runtime/src/verify/adapters/index.js +5 -0
  73. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  74. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  75. package/dist/runtime/src/verify/adapters/node.js +254 -0
  76. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  77. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  78. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  79. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  80. package/dist/runtime/src/verify/evidence.js +5 -2
  81. package/dist/runtime/src/verify/service.js +24 -8
  82. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  83. package/dist/runtime/src/verify/spawn.js +13 -2
  84. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  85. package/docs/en/spec/loop-engineering.md +49 -0
  86. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  87. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  88. package/package.json +1 -1
  89. package/schemas/config.schema.json +221 -19
  90. package/schemas/evidence.schema.json +129 -6
  91. package/schemas/task.schema.json +265 -6
  92. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  93. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -0,0 +1,177 @@
1
+ import { canonicalJson } from "../config/hash.js";
2
+ import { sha256 } from "../fs/hash.js";
3
+ const MAX_ID = 4_096;
4
+ function bounded(value, label) {
5
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_ID || value.includes("\0") || /[\r\n]/u.test(value)) {
6
+ throw new TypeError(`${label} must be a bounded single-line string.`);
7
+ }
8
+ return value;
9
+ }
10
+ function status(value) {
11
+ return value === "PASS" || value === "FAIL" ? value : "UNKNOWN";
12
+ }
13
+ function failureClass(value, current) {
14
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_ID || value.includes("\0") || /[\r\n]/u.test(value)) {
15
+ return current === "PASS" ? "none" : "unknown";
16
+ }
17
+ return value;
18
+ }
19
+ function stablePins(values) {
20
+ return [...new Set((values ?? []).map((value) => bounded(value, "pinId")))].sort();
21
+ }
22
+ function stableIds(values, label) {
23
+ return [...new Set((values ?? []).map((value) => bounded(value, label)))].sort();
24
+ }
25
+ function itemKey(check) {
26
+ return {
27
+ criterionId: check.criterionId,
28
+ runnerId: check.runnerId,
29
+ checkId: check.checkId,
30
+ phase: check.phase,
31
+ pinId: check.pinId
32
+ };
33
+ }
34
+ function identity(check) {
35
+ return {
36
+ ...itemKey(check),
37
+ status: check.status,
38
+ failureClass: check.failureClass
39
+ };
40
+ }
41
+ function sortedChecks(checks) {
42
+ return [...checks].sort((left, right) => canonicalJson(identity(left)).localeCompare(canonicalJson(identity(right))));
43
+ }
44
+ function commandObservation(result) {
45
+ const current = status(result.status);
46
+ return {
47
+ criterionId: null,
48
+ runnerId: null,
49
+ checkId: bounded(result.commandId, "commandId"),
50
+ phase: "verification",
51
+ status: current,
52
+ failureClass: failureClass(result.failureClass, current),
53
+ pinId: null
54
+ };
55
+ }
56
+ function acceptanceObservations(result) {
57
+ const current = status(result.status);
58
+ const criterionId = bounded(result.criterionId, "criterionId");
59
+ const runnerId = bounded(result.runnerId, "runnerId");
60
+ const phase = result.phase === "baseline" || result.phase === "candidate" ? result.phase : "candidate";
61
+ const actual = new Map((result.checks ?? []).map((check) => [bounded(check.checkId, "checkId"), {
62
+ status: status(check.status),
63
+ failureClass: failureClass(check.failureClass, status(check.status))
64
+ }]));
65
+ const required = stableIds(result.requiredCheckIds, "checkId");
66
+ const red = stableIds(result.redCheckIds, "redCheckId");
67
+ const checkIds = [...new Set([...required, ...red, ...actual.keys()])].sort();
68
+ if (checkIds.length === 0) {
69
+ return [{
70
+ criterionId,
71
+ runnerId,
72
+ checkId: criterionId,
73
+ phase,
74
+ status: current,
75
+ failureClass: failureClass(result.failureClass, current),
76
+ pinId: null
77
+ }];
78
+ }
79
+ return checkIds.map((checkId) => {
80
+ const observed = actual.get(checkId);
81
+ // A PASS criterion is a successful contract result. Baseline assertion
82
+ // failures are the intended red proof and must not become repair work.
83
+ if (current === "PASS") {
84
+ return { criterionId, runnerId, checkId, phase, status: "PASS", failureClass: "none", pinId: null };
85
+ }
86
+ if (phase === "baseline" && red.length > 0) {
87
+ if (red.includes(checkId) && observed?.status === "FAIL" && observed.failureClass === "assertion") {
88
+ return { criterionId, runnerId, checkId, phase, status: "PASS", failureClass: "none", pinId: null };
89
+ }
90
+ if (red.includes(checkId)) {
91
+ return {
92
+ criterionId,
93
+ runnerId,
94
+ checkId,
95
+ phase,
96
+ status: "FAIL",
97
+ failureClass: observed === undefined ? "missing-red-check" : observed.status === "PASS" ? "non-discriminating" : observed.failureClass,
98
+ pinId: null
99
+ };
100
+ }
101
+ }
102
+ return {
103
+ criterionId,
104
+ runnerId,
105
+ checkId,
106
+ phase,
107
+ status: observed?.status ?? current,
108
+ failureClass: observed?.failureClass ?? (observed === undefined ? "missing-check" : failureClass(result.failureClass, current)),
109
+ pinId: null
110
+ };
111
+ });
112
+ }
113
+ function pinObservation(pinId) {
114
+ return {
115
+ criterionId: null,
116
+ runnerId: null,
117
+ checkId: pinId,
118
+ phase: "review",
119
+ status: "FAIL",
120
+ failureClass: "pinned-finding",
121
+ pinId
122
+ };
123
+ }
124
+ /**
125
+ * Turn a verifier report and the saved finding ratchet into a stable semantic
126
+ * observation. Raw diagnostics, source commits, timestamps, and task failure
127
+ * fingerprints are deliberately not read by this function.
128
+ */
129
+ export function deriveFailureObservation(report, options = {}) {
130
+ const checks = [
131
+ ...report.results.filter((result) => result.required).map(commandObservation),
132
+ ...(report.acceptance ?? []).flatMap(acceptanceObservations),
133
+ ...stablePins(options.pendingPinIds).map(pinObservation)
134
+ ];
135
+ if (checks.length === 0 && report.status !== undefined && report.status !== "PASS") {
136
+ checks.push({
137
+ criterionId: null,
138
+ runnerId: null,
139
+ checkId: "verification",
140
+ phase: "verification",
141
+ status: status(report.status),
142
+ failureClass: "verification-failed",
143
+ pinId: null
144
+ });
145
+ }
146
+ const ordered = sortedChecks(checks);
147
+ const pendingPinIds = stablePins(options.pendingPinIds);
148
+ const failing = ordered.filter((check) => check.status !== "PASS").map(identity);
149
+ return {
150
+ taskId: bounded(report.taskId, "taskId"),
151
+ checks: ordered,
152
+ pendingPinIds,
153
+ failureKey: failing.length === 0 ? null : sha256(canonicalJson({ taskId: report.taskId, failing })),
154
+ progressDigest: sha256(canonicalJson({
155
+ taskId: report.taskId,
156
+ checks: ordered.map((check) => identity(check)),
157
+ pendingPinIds
158
+ }))
159
+ };
160
+ }
161
+ /** Compare only stable check identities; diagnostic text never participates. */
162
+ export function compareFailureObservations(previous, current) {
163
+ const currentByItem = new Map(current.checks.map((check) => [canonicalJson(itemKey(check)), check]));
164
+ const passed = previous.checks.some((check) => {
165
+ if (check.status === "PASS")
166
+ return false;
167
+ return currentByItem.get(canonicalJson(itemKey(check)))?.status === "PASS";
168
+ });
169
+ const currentPins = new Set(current.pendingPinIds);
170
+ const dischargedPin = previous.pendingPinIds.some((pinId) => !currentPins.has(pinId));
171
+ return {
172
+ sameFailure: previous.failureKey !== null && previous.failureKey === current.failureKey,
173
+ usefulProgress: passed || dischargedPin,
174
+ failureKey: current.failureKey,
175
+ progressDigest: current.progressDigest
176
+ };
177
+ }
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AgentOpsError } from "../../fs/paths.js";
3
+ import { JsonLineStream } from "./protocol.js";
4
+ import { actionResult, assertContext, assertProcessRunning, event, spawnNative, stopNativeProcess } from "./util.js";
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function stringField(value, key) {
9
+ return isRecord(value) && typeof value[key] === "string" ? value[key] : null;
10
+ }
11
+ function goalStatus(value) {
12
+ if (isRecord(value)) {
13
+ const subtype = stringField(value, "subtype");
14
+ if (subtype === "success")
15
+ return "complete";
16
+ if (subtype === "error_max_budget_usd" || subtype === "error_max_turns")
17
+ return "budgetLimited";
18
+ const status = stringField(value, "status");
19
+ if (status === "active" || status === "paused" || status === "blocked" ||
20
+ status === "usageLimited" || status === "budgetLimited" || status === "complete") {
21
+ return status;
22
+ }
23
+ }
24
+ return "unknown";
25
+ }
26
+ function writeMessage(handle, text) {
27
+ handle.process.stdin.write(`${JSON.stringify({
28
+ type: "user",
29
+ message: {
30
+ role: "user",
31
+ content: [{ type: "text", text }]
32
+ }
33
+ })}\n`);
34
+ }
35
+ export class ClaudeGoalHost {
36
+ options;
37
+ kind = "claude";
38
+ streams = new WeakMap();
39
+ now;
40
+ constructor(options = {}) {
41
+ this.options = options;
42
+ this.now = options.now ?? (() => new Date());
43
+ }
44
+ async start(input) {
45
+ return this.open(input, false, null);
46
+ }
47
+ async resume(input, sessionId, objective) {
48
+ if (sessionId.length === 0) {
49
+ throw new AgentOpsError("NATIVE_SESSION_INVALID", "Claude session id is required to resume.");
50
+ }
51
+ const handle = await this.open(input, true, sessionId);
52
+ if (objective !== undefined) {
53
+ await this.activate(handle, objective, input, input.tokenBudget);
54
+ }
55
+ return handle;
56
+ }
57
+ async activate(handle, objective, context, tokenBudget = null) {
58
+ assertContext(handle, context);
59
+ assertProcessRunning(handle.process);
60
+ if (objective.length === 0 || objective.length > 4000) {
61
+ throw new AgentOpsError("NATIVE_GOAL_INVALID", "Claude goal objective must be 1–4000 characters.");
62
+ }
63
+ const condition = tokenBudget === null
64
+ ? `/goal ${objective}`
65
+ : `/goal ${objective} or stop after ${tokenBudget} tokens`;
66
+ writeMessage(handle, condition);
67
+ return actionResult(true, "active", { objective, tokenBudget });
68
+ }
69
+ async update(handle, update) {
70
+ assertContext(handle, update);
71
+ assertProcessRunning(handle.process);
72
+ return this.activate(handle, update.objective, update, update.tokenBudget ?? null);
73
+ }
74
+ async *observe(handle, options = {}) {
75
+ const stream = this.stream(handle);
76
+ while (true) {
77
+ const message = await stream.next(options.signal);
78
+ if (message === null) {
79
+ return;
80
+ }
81
+ const mapped = this.mapEvent(handle, message);
82
+ yield mapped;
83
+ if (mapped.type === "completed" || mapped.type === "error" || mapped.type === "closed") {
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ async interrupt(handle, context) {
89
+ assertContext(handle, context);
90
+ if (handle.process.exitCode === null && handle.process.signalCode === null) {
91
+ handle.process.kill("SIGINT");
92
+ }
93
+ return actionResult(true, "paused", { signal: "SIGINT" });
94
+ }
95
+ async stop(handle, context, signal = "SIGTERM") {
96
+ assertContext(handle, context);
97
+ await stopNativeProcess(handle.process, signal);
98
+ return actionResult(true, "paused", { signal });
99
+ }
100
+ async open(input, resume, sessionId) {
101
+ const requestedSessionId = sessionId ?? input.sessionId ?? randomUUID();
102
+ const defaultArgs = resume
103
+ ? [
104
+ "-p",
105
+ "--resume", requestedSessionId,
106
+ "--input-format", "stream-json",
107
+ "--output-format", "stream-json",
108
+ "--verbose", "--include-partial-messages",
109
+ "--permission-mode", "auto",
110
+ "--permission-prompts", "none"
111
+ ]
112
+ : [
113
+ "-p",
114
+ "--session-id", requestedSessionId,
115
+ "--input-format", "stream-json",
116
+ "--output-format", "stream-json",
117
+ "--verbose", "--include-partial-messages",
118
+ "--permission-mode", "auto",
119
+ "--permission-prompts", "none"
120
+ ];
121
+ const command = this.options.command ?? "claude";
122
+ const args = this.options.args ?? defaultArgs;
123
+ const process = spawnNative(command, args, input.cwd, input.env, this.options);
124
+ const stream = new JsonLineStream(process);
125
+ try {
126
+ if (process.pid !== undefined)
127
+ await input.registerProcess?.(process.pid);
128
+ }
129
+ catch (cause) {
130
+ stream.close(cause);
131
+ if (process.pid !== undefined)
132
+ await stopNativeProcess(process, "SIGKILL");
133
+ throw cause;
134
+ }
135
+ const handle = {
136
+ runId: input.runId,
137
+ workerId: input.workerId,
138
+ generation: input.generation,
139
+ contractHash: input.contractHash,
140
+ kind: this.kind,
141
+ // --session-id and --resume make this identity durable before the first
142
+ // user message. Claude can emit startup hook events before system/init,
143
+ // so start intentionally does not wait for that event.
144
+ sessionId: requestedSessionId,
145
+ threadId: null,
146
+ nativeVersion: input.nativeVersion ?? this.options.nativeVersion ?? null,
147
+ cwd: input.cwd,
148
+ processId: process.pid ?? null,
149
+ startedAt: this.now().toISOString(),
150
+ process,
151
+ goalStatus: "unknown"
152
+ };
153
+ this.streams.set(handle, stream);
154
+ return handle;
155
+ }
156
+ stream(handle) {
157
+ const stream = this.streams.get(handle);
158
+ if (stream === undefined) {
159
+ throw new AgentOpsError("NATIVE_SESSION_INVALID", "Unknown Claude native handle.");
160
+ }
161
+ return stream;
162
+ }
163
+ mapEvent(handle, payload) {
164
+ const type = stringField(payload, "type");
165
+ if (type === "result") {
166
+ const subtype = stringField(payload, "subtype");
167
+ return event(handle, subtype === "success" ? "completed" : "error", goalStatus(payload), payload, this.now);
168
+ }
169
+ if (type === "error" || (type === "system" && stringField(payload, "subtype") === "error")) {
170
+ return event(handle, "error", goalStatus(payload), payload, this.now);
171
+ }
172
+ if (isRecord(payload.usage) || typeof payload.total_cost_usd === "number") {
173
+ return event(handle, "usage", goalStatus(payload), payload, this.now);
174
+ }
175
+ if (type === "system" && stringField(payload, "subtype") === "goal") {
176
+ return event(handle, "goal-updated", goalStatus(payload), payload, this.now);
177
+ }
178
+ return event(handle, "message", goalStatus(payload), payload, this.now);
179
+ }
180
+ }
@@ -0,0 +1,279 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AgentOpsError } from "../../fs/paths.js";
3
+ import { JsonRpcSession } from "./protocol.js";
4
+ import { actionResult, assertContext, commandParts, event, assertProcessRunning, spawnNative, stopNativeProcess } from "./util.js";
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function nestedRecord(value, key) {
9
+ if (!isRecord(value) || !isRecord(value[key])) {
10
+ return null;
11
+ }
12
+ return value[key];
13
+ }
14
+ function stringField(value, key) {
15
+ return isRecord(value) && typeof value[key] === "string" ? value[key] : null;
16
+ }
17
+ function statusField(value) {
18
+ const status = stringField(value, "status");
19
+ return status === "active" || status === "paused" || status === "blocked" ||
20
+ status === "usageLimited" || status === "budgetLimited" || status === "complete"
21
+ ? status
22
+ : "unknown";
23
+ }
24
+ function resultThreadId(result, fallback = null) {
25
+ const thread = nestedRecord(result, "thread");
26
+ const id = stringField(thread, "id");
27
+ if (id === null) {
28
+ if (fallback !== null) {
29
+ return fallback;
30
+ }
31
+ throw new AgentOpsError("NATIVE_PROTOCOL_INVALID", "Codex app server did not return a thread id.");
32
+ }
33
+ return id;
34
+ }
35
+ function resultTurnId(result) {
36
+ const turn = nestedRecord(result, "turn");
37
+ return stringField(turn, "id");
38
+ }
39
+ export class CodexGoalHost {
40
+ options;
41
+ kind = "codex";
42
+ sessions = new WeakMap();
43
+ turns = new WeakMap();
44
+ now;
45
+ constructor(options = {}) {
46
+ this.options = options;
47
+ this.now = options.now ?? (() => new Date());
48
+ }
49
+ async start(input) {
50
+ const handle = await this.open(input, false, null);
51
+ return handle;
52
+ }
53
+ async resume(input, sessionId, objective) {
54
+ if (sessionId.length === 0) {
55
+ throw new AgentOpsError("NATIVE_SESSION_INVALID", "Codex thread id is required to resume.");
56
+ }
57
+ const handle = await this.open(input, true, sessionId);
58
+ if (objective !== undefined) {
59
+ await this.activate(handle, objective, input, input.tokenBudget);
60
+ }
61
+ return handle;
62
+ }
63
+ async activate(handle, objective, context, tokenBudget = null) {
64
+ assertContext(handle, context);
65
+ assertProcessRunning(handle.process);
66
+ if (objective.length === 0 || objective.length > 4000) {
67
+ throw new AgentOpsError("NATIVE_GOAL_INVALID", "Codex goal objective must be 1–4000 characters.");
68
+ }
69
+ const session = this.session(handle);
70
+ const goal = await session.request("thread/goal/set", {
71
+ threadId: handle.threadId,
72
+ objective,
73
+ status: "active",
74
+ tokenBudget
75
+ });
76
+ const turn = await session.request("turn/start", {
77
+ threadId: handle.threadId,
78
+ input: [{ type: "text", text: objective }],
79
+ approvalPolicy: "on-request",
80
+ approvalsReviewer: "auto_review"
81
+ });
82
+ const turnId = resultTurnId(turn);
83
+ if (turnId !== null) {
84
+ this.turns.set(handle, turnId);
85
+ }
86
+ return actionResult(true, statusField(nestedRecord(goal, "goal") ?? goal), goal);
87
+ }
88
+ async update(handle, update) {
89
+ assertContext(handle, update);
90
+ assertProcessRunning(handle.process);
91
+ const session = this.session(handle);
92
+ const goal = await session.request("thread/goal/set", {
93
+ threadId: handle.threadId,
94
+ objective: update.objective,
95
+ status: "active",
96
+ tokenBudget: update.tokenBudget ?? null
97
+ });
98
+ const turnId = this.turns.get(handle);
99
+ let turn;
100
+ if (turnId === undefined) {
101
+ turn = await session.request("turn/start", {
102
+ threadId: handle.threadId,
103
+ input: [{ type: "text", text: update.objective }],
104
+ approvalPolicy: "on-request",
105
+ approvalsReviewer: "auto_review"
106
+ });
107
+ }
108
+ else {
109
+ try {
110
+ turn = await session.request("turn/steer", {
111
+ threadId: handle.threadId,
112
+ expectedTurnId: turnId,
113
+ input: [{ type: "text", text: update.objective }]
114
+ });
115
+ }
116
+ catch (error) {
117
+ if (!(error instanceof AgentOpsError) || error.code !== "NATIVE_REQUEST_FAILED") {
118
+ throw error;
119
+ }
120
+ turn = await session.request("turn/start", {
121
+ threadId: handle.threadId,
122
+ input: [{ type: "text", text: update.objective }],
123
+ approvalPolicy: "on-request",
124
+ approvalsReviewer: "auto_review"
125
+ });
126
+ }
127
+ }
128
+ const nextTurnId = resultTurnId(turn);
129
+ if (nextTurnId !== null) {
130
+ this.turns.set(handle, nextTurnId);
131
+ }
132
+ return actionResult(true, statusField(nestedRecord(goal, "goal") ?? goal), goal);
133
+ }
134
+ async *observe(handle, options = {}) {
135
+ const session = this.session(handle);
136
+ while (true) {
137
+ const message = await session.nextEvent(options.signal);
138
+ if (message === null) {
139
+ return;
140
+ }
141
+ const mapped = this.mapEvent(handle, message);
142
+ yield { ...mapped, ...(typeof message.method === "string" ? { transportMethod: message.method } : {}) };
143
+ if (mapped.type === "completed" || mapped.type === "error" || mapped.type === "closed") {
144
+ return;
145
+ }
146
+ }
147
+ }
148
+ async interrupt(handle, context) {
149
+ assertContext(handle, context);
150
+ const session = this.session(handle);
151
+ const turnId = this.turns.get(handle);
152
+ if (turnId !== undefined) {
153
+ await session.request("turn/interrupt", { threadId: handle.threadId, turnId });
154
+ }
155
+ const result = await session.request("thread/goal/set", {
156
+ threadId: handle.threadId,
157
+ objective: null,
158
+ status: "paused",
159
+ tokenBudget: null
160
+ });
161
+ return actionResult(true, statusField(nestedRecord(result, "goal") ?? result), result);
162
+ }
163
+ async stop(handle, context, signal = "SIGTERM") {
164
+ assertContext(handle, context);
165
+ let payload = null;
166
+ try {
167
+ const result = await this.interrupt(handle, context);
168
+ payload = result.payload;
169
+ }
170
+ catch {
171
+ // A crashed or already closed native process is already stopped.
172
+ }
173
+ await stopNativeProcess(handle.process, signal);
174
+ return actionResult(true, "paused", payload);
175
+ }
176
+ async open(input, resume, threadId) {
177
+ const parts = commandParts(this.options, "codex", ["app-server", "--listen", "stdio://"]);
178
+ const process = spawnNative(parts.command, parts.args, input.cwd, input.env, this.options);
179
+ const channel = new JsonRpcSession(process, input.timeoutMs ?? this.options.requestTimeoutMs ?? 30_000);
180
+ try {
181
+ if (process.pid !== undefined)
182
+ await input.registerProcess?.(process.pid);
183
+ await this.sendInitialize(channel);
184
+ let nativeThreadId;
185
+ if (resume) {
186
+ const result = await channel.request("thread/resume", {
187
+ threadId,
188
+ cwd: input.cwd,
189
+ approvalPolicy: "on-request",
190
+ approvalsReviewer: "auto_review",
191
+ sandbox: "workspace-write",
192
+ excludeTurns: true
193
+ });
194
+ nativeThreadId = resultThreadId(result, threadId);
195
+ }
196
+ else {
197
+ const result = await channel.request("thread/start", {
198
+ cwd: input.cwd,
199
+ approvalPolicy: "on-request",
200
+ approvalsReviewer: "auto_review",
201
+ sandbox: "workspace-write",
202
+ ephemeral: false
203
+ });
204
+ nativeThreadId = resultThreadId(result);
205
+ }
206
+ const handle = {
207
+ runId: input.runId,
208
+ workerId: input.workerId,
209
+ generation: input.generation,
210
+ contractHash: input.contractHash,
211
+ kind: this.kind,
212
+ sessionId: input.sessionId ?? randomUUID(),
213
+ threadId: nativeThreadId,
214
+ nativeVersion: input.nativeVersion ?? this.options.nativeVersion ?? null,
215
+ cwd: input.cwd,
216
+ processId: process.pid ?? null,
217
+ startedAt: this.now().toISOString(),
218
+ process,
219
+ goalStatus: "unknown"
220
+ };
221
+ this.sessions.set(handle, channel);
222
+ return handle;
223
+ }
224
+ catch (cause) {
225
+ channel.close(cause);
226
+ if (process.pid !== undefined)
227
+ await stopNativeProcess(process, "SIGKILL");
228
+ throw cause;
229
+ }
230
+ }
231
+ async sendInitialize(session) {
232
+ // Keep the notification sequence in one helper so callers cannot send a
233
+ // goal before the app-server handshake is complete.
234
+ await session.request("initialize", {
235
+ capabilities: { experimentalApi: true },
236
+ clientInfo: {
237
+ name: "agent-ops",
238
+ title: "agent-ops native goal host",
239
+ version: "0.6.0"
240
+ }
241
+ });
242
+ session.notify("initialized", {});
243
+ }
244
+ session(handle) {
245
+ const session = this.sessions.get(handle);
246
+ if (session === undefined) {
247
+ throw new AgentOpsError("NATIVE_SESSION_INVALID", "Unknown Codex native handle.");
248
+ }
249
+ return session;
250
+ }
251
+ mapEvent(handle, message) {
252
+ const method = typeof message.method === "string" ? message.method : "message";
253
+ const payload = isRecord(message.params) ? message.params : message;
254
+ if (method === "thread/goal/updated") {
255
+ const goal = isRecord(payload.goal) ? payload.goal : payload;
256
+ const status = statusField(goal);
257
+ return event(handle, status === "complete" ? "completed" : "goal-updated", status, payload, this.now);
258
+ }
259
+ if (method === "thread/goal/cleared") {
260
+ return event(handle, "goal-updated", "complete", payload, this.now);
261
+ }
262
+ if (method === "turn/completed") {
263
+ this.turns.delete(handle);
264
+ // A completed turn is not necessarily a completed native goal. Native
265
+ // goals can continue automatically after a turn boundary.
266
+ return event(handle, "message", "unknown", payload, this.now);
267
+ }
268
+ if (method === "turn/started") {
269
+ const turn = nestedRecord(payload, "turn");
270
+ const id = stringField(turn, "id");
271
+ if (id !== null)
272
+ this.turns.set(handle, id);
273
+ }
274
+ if (method.includes("usage") || method.includes("token")) {
275
+ return event(handle, "usage", "unknown", payload, this.now);
276
+ }
277
+ return event(handle, "message", "unknown", payload, this.now);
278
+ }
279
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./protocol.js";
3
+ export * from "./codex.js";
4
+ export * from "./claude.js";