@titan-design/agent 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.
- package/README.md +104 -0
- package/dist/index.d.ts +337 -1
- package/dist/index.js +1473 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -24,8 +24,8 @@ var STRIPPED_PROXY_VARS = [
|
|
|
24
24
|
var DEFAULT_MAX_RETRIES = "3";
|
|
25
25
|
var AuthMisconfiguredError = class extends Error {
|
|
26
26
|
hint;
|
|
27
|
-
constructor(
|
|
28
|
-
super(
|
|
27
|
+
constructor(message2, hint) {
|
|
28
|
+
super(message2);
|
|
29
29
|
this.name = "AuthMisconfiguredError";
|
|
30
30
|
this.hint = hint;
|
|
31
31
|
}
|
|
@@ -214,10 +214,10 @@ var Session = class {
|
|
|
214
214
|
const elapsed = () => (this.deps.now ?? Date.now)() - startedAt;
|
|
215
215
|
try {
|
|
216
216
|
this.stream = run({ prompt: this.config.prompt, options: this.buildOptions() });
|
|
217
|
-
for await (const
|
|
217
|
+
for await (const message2 of this.stream) {
|
|
218
218
|
this.resetWatchdog();
|
|
219
|
-
this.config.onMessage?.(
|
|
220
|
-
this.absorb(
|
|
219
|
+
this.config.onMessage?.(message2);
|
|
220
|
+
this.absorb(message2);
|
|
221
221
|
if (this.endReason || this.authError) break;
|
|
222
222
|
}
|
|
223
223
|
} catch (error) {
|
|
@@ -253,21 +253,21 @@ var Session = class {
|
|
|
253
253
|
}
|
|
254
254
|
return options;
|
|
255
255
|
}
|
|
256
|
-
absorb(
|
|
257
|
-
if ("session_id" in
|
|
258
|
-
if (
|
|
256
|
+
absorb(message2) {
|
|
257
|
+
if ("session_id" in message2 && typeof message2.session_id === "string") this.sessionId = message2.session_id;
|
|
258
|
+
if (message2.type === "system" && message2.subtype === "init") {
|
|
259
259
|
this.init = {
|
|
260
|
-
apiKeySource:
|
|
261
|
-
model:
|
|
262
|
-
tools:
|
|
263
|
-
permissionMode:
|
|
264
|
-
claudeCodeVersion:
|
|
260
|
+
apiKeySource: message2.apiKeySource,
|
|
261
|
+
model: message2.model,
|
|
262
|
+
tools: message2.tools ?? [],
|
|
263
|
+
permissionMode: message2.permissionMode,
|
|
264
|
+
claudeCodeVersion: message2.claude_code_version
|
|
265
265
|
};
|
|
266
|
-
if (!this.config.allowApiKeyBilling) this.checkApiKeySource(
|
|
266
|
+
if (!this.config.allowApiKeyBilling) this.checkApiKeySource(message2.apiKeySource);
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
|
-
if (
|
|
270
|
-
if (
|
|
269
|
+
if (message2.type === "rate_limit_event") this.rateLimitResetsAt = message2.rate_limit_info.resetsAt;
|
|
270
|
+
if (message2.type === "result") this.result = message2;
|
|
271
271
|
}
|
|
272
272
|
checkApiKeySource(observed) {
|
|
273
273
|
try {
|
|
@@ -345,18 +345,1475 @@ function endFailure(reason) {
|
|
|
345
345
|
function describeEnd(reason) {
|
|
346
346
|
return reason.kind === "aborted" ? reason.reason : `no message for ${reason.timeoutMs}ms`;
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
// src/harness-contracts.ts
|
|
350
|
+
var CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
|
|
351
|
+
var CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
|
|
352
|
+
var EXECUTION_CAPABILITIES = [
|
|
353
|
+
"fresh_run",
|
|
354
|
+
"resume",
|
|
355
|
+
"fork",
|
|
356
|
+
"structured_output",
|
|
357
|
+
"interactive_approvals",
|
|
358
|
+
"external_cancellation",
|
|
359
|
+
"semantic_interrupt",
|
|
360
|
+
"active_turn_steering",
|
|
361
|
+
"idle_turn_submission",
|
|
362
|
+
"persisted_transcript",
|
|
363
|
+
"process_reattachment",
|
|
364
|
+
"token_reporting"
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
// src/preflight.ts
|
|
368
|
+
import { conversationRef } from "@titan-design/agent-protocol";
|
|
369
|
+
function preflightHarnessRun(request, descriptor) {
|
|
370
|
+
if (request.harness !== descriptor.harness) {
|
|
371
|
+
return mismatch(`request harness ${request.harness} does not match adapter harness ${descriptor.harness}`);
|
|
372
|
+
}
|
|
373
|
+
if (request.target.kind === "resume" && request.target.conversation.harness !== request.harness) {
|
|
374
|
+
return mismatch(`resume conversation harness ${request.target.conversation.harness} does not match ${request.harness}`);
|
|
375
|
+
}
|
|
376
|
+
const identityFailure = invalidIdentity(request);
|
|
377
|
+
if (identityFailure) return identityFailure;
|
|
378
|
+
const invalid2 = invalidNumbers(request);
|
|
379
|
+
if (invalid2) return invalid2;
|
|
380
|
+
const nativeFailure = invalidNativeOptions(request);
|
|
381
|
+
if (nativeFailure) return nativeFailure;
|
|
382
|
+
if (request.signal?.aborted) return { kind: "aborted", reason: String(request.signal.reason ?? "aborted by caller") };
|
|
383
|
+
const capabilityFailure = checkCapabilities(request, descriptor);
|
|
384
|
+
if (capabilityFailure) return capabilityFailure;
|
|
385
|
+
return checkLimits(request, descriptor);
|
|
386
|
+
}
|
|
387
|
+
function invalidIdentity(request) {
|
|
388
|
+
if (request.target.kind === "fresh") {
|
|
389
|
+
return nonempty(request.target.namespace) ? void 0 : invalid("fresh target namespace must be a nonempty string");
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
conversationRef(request.target.conversation);
|
|
393
|
+
return void 0;
|
|
394
|
+
} catch {
|
|
395
|
+
return invalid("resume conversation identity components must be nonempty strings");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
var CLAUDE_ONLY = ["allowedTools", "disallowedTools", "permissionMode", "agents", "mcpServers", "hooks", "settingSources", "allowApiKeyBilling"];
|
|
399
|
+
var CODEX_ONLY = ["reasoningEffort", "sandbox", "approvalPolicy"];
|
|
400
|
+
var CLAUDE_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
|
|
401
|
+
var CLAUDE_SETTING_SOURCES = ["user", "project", "local"];
|
|
402
|
+
function invalidNativeOptions(request) {
|
|
403
|
+
if (request.native === void 0) return void 0;
|
|
404
|
+
if (!record(request.native)) return invalid("native options must be an object");
|
|
405
|
+
const native = request.native;
|
|
406
|
+
const forbidden = request.harness === "codex" ? CLAUDE_ONLY : CODEX_ONLY;
|
|
407
|
+
const field = forbidden.find((name) => name in native);
|
|
408
|
+
if (field) return invalid(`${field} is not a ${request.harness} native option`);
|
|
409
|
+
const options = native;
|
|
410
|
+
return request.harness === "codex" ? invalidCodexOptions(options) : invalidClaudeOptions(options);
|
|
411
|
+
}
|
|
412
|
+
function invalidCodexOptions(options) {
|
|
413
|
+
const invalidModel = optionalNonempty("model", options.model);
|
|
414
|
+
if (invalidModel) return invalidModel;
|
|
415
|
+
const invalidEffort = optionalNonempty("reasoningEffort", options.reasoningEffort);
|
|
416
|
+
if (invalidEffort) return invalidEffort;
|
|
417
|
+
const enums = [
|
|
418
|
+
["sandbox", options.sandbox, CODEX_SANDBOXES],
|
|
419
|
+
["approvalPolicy", options.approvalPolicy, CODEX_APPROVAL_POLICIES]
|
|
420
|
+
];
|
|
421
|
+
for (const [name, value, allowed] of enums) {
|
|
422
|
+
if (value !== void 0 && !allowed.includes(value)) return invalid(`${name} is not a recognized Codex value`);
|
|
423
|
+
}
|
|
424
|
+
if (options.outputSchema === void 0) return void 0;
|
|
425
|
+
if (!record(options.outputSchema)) return invalid("Codex outputSchema must be an object");
|
|
426
|
+
if (!record(options.outputSchema.jsonSchema)) return invalid("Codex outputSchema.jsonSchema must be an object");
|
|
427
|
+
return typeof options.outputSchema.parse === "function" ? void 0 : invalid("Codex outputSchema.parse must be callable");
|
|
428
|
+
}
|
|
429
|
+
function invalidClaudeOptions(options) {
|
|
430
|
+
const invalidModel = optionalNonempty("model", options.model);
|
|
431
|
+
if (invalidModel) return invalidModel;
|
|
432
|
+
if (options.permissionMode !== void 0 && !CLAUDE_PERMISSION_MODES.includes(options.permissionMode)) {
|
|
433
|
+
return invalid("permissionMode is not a recognized Claude Code value");
|
|
434
|
+
}
|
|
435
|
+
for (const name of ["allowedTools", "disallowedTools"]) {
|
|
436
|
+
if (options[name] !== void 0 && !stringArray(options[name])) return invalid(`${name} must be an array of strings`);
|
|
437
|
+
}
|
|
438
|
+
if (options.settingSources !== void 0 && !enumArray(options.settingSources, CLAUDE_SETTING_SOURCES)) {
|
|
439
|
+
return invalid("settingSources must contain recognized Claude Code values");
|
|
440
|
+
}
|
|
441
|
+
if (options.allowApiKeyBilling !== void 0 && typeof options.allowApiKeyBilling !== "boolean") {
|
|
442
|
+
return invalid("allowApiKeyBilling must be boolean");
|
|
443
|
+
}
|
|
444
|
+
if (options.outputSchema !== void 0 && (!record(options.outputSchema) || typeof options.outputSchema.safeParse !== "function")) {
|
|
445
|
+
return invalid("Claude Code outputSchema must be a Zod schema");
|
|
446
|
+
}
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
async function dispatchHarnessRun(request, adapter) {
|
|
450
|
+
const failure = preflightHarnessRun(request, adapter.descriptor);
|
|
451
|
+
if (failure) return { ok: false, harness: request.harness, failure, usage: [] };
|
|
452
|
+
try {
|
|
453
|
+
return await adapter.run(request);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
harness: request.harness,
|
|
458
|
+
failure: { kind: "runtime_error", reason: messageOf2(error), native: error },
|
|
459
|
+
usage: []
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function invalidNumbers(request) {
|
|
464
|
+
if (!positiveFinite(request.wallTimeMs)) return invalid("wallTimeMs must be a positive finite number");
|
|
465
|
+
for (const limit of request.limits ?? []) {
|
|
466
|
+
if (!positiveFinite(limit.value)) return invalid(`${limit.unit} limit must be a positive finite number`);
|
|
467
|
+
if (limit.unit !== "usd" && !Number.isInteger(limit.value)) {
|
|
468
|
+
return invalid(`${limit.unit} limit must be a positive integer`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
function checkCapabilities(request, descriptor) {
|
|
474
|
+
for (const capability of requiredCapabilities(request)) {
|
|
475
|
+
const assessment = descriptor.capabilities[capability] ?? {
|
|
476
|
+
status: "unverified",
|
|
477
|
+
reason: "adapter did not declare this capability"
|
|
478
|
+
};
|
|
479
|
+
const failure = rejectAssessment(assessment, { kind: "capability", capability });
|
|
480
|
+
if (failure) return failure;
|
|
481
|
+
}
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
function requiredCapabilities(request) {
|
|
485
|
+
const required = new Set(request.requires ?? []);
|
|
486
|
+
required.add(request.target.kind === "fresh" ? "fresh_run" : "resume");
|
|
487
|
+
if (request.native?.outputSchema !== void 0) required.add("structured_output");
|
|
488
|
+
if (request.signal) required.add("external_cancellation");
|
|
489
|
+
return [...required];
|
|
490
|
+
}
|
|
491
|
+
function checkLimits(request, descriptor) {
|
|
492
|
+
const wallTime = {
|
|
493
|
+
unit: "milliseconds",
|
|
494
|
+
value: request.wallTimeMs,
|
|
495
|
+
scope: "execution",
|
|
496
|
+
enforcement: "hard"
|
|
497
|
+
};
|
|
498
|
+
for (const limit of [wallTime, ...request.limits ?? []]) {
|
|
499
|
+
const declared = descriptor.limits.find((candidate) => sameLimit(candidate, limit));
|
|
500
|
+
const assessment = declared?.assessment ?? { status: "unverified", reason: "adapter did not declare this limit" };
|
|
501
|
+
const requirement = { kind: "limit", limit: { ...limit, assessment } };
|
|
502
|
+
const failure = rejectAssessment(assessment, requirement);
|
|
503
|
+
if (failure) return failure;
|
|
504
|
+
}
|
|
505
|
+
return void 0;
|
|
506
|
+
}
|
|
507
|
+
function sameLimit(capability, limit) {
|
|
508
|
+
return capability.unit === limit.unit && capability.scope === limit.scope && capability.enforcement === limit.enforcement;
|
|
509
|
+
}
|
|
510
|
+
function rejectAssessment(assessment, requirement) {
|
|
511
|
+
if (assessment.status === "supported") return void 0;
|
|
512
|
+
const name = requirement.kind === "capability" ? requirement.capability : describeLimit(requirement.limit);
|
|
513
|
+
return { kind: "unsupported_requirement", reason: `${name} is ${assessment.status}: ${assessment.reason}`, requirement, status: assessment.status };
|
|
514
|
+
}
|
|
515
|
+
function describeLimit(limit) {
|
|
516
|
+
return `${limit.enforcement} ${limit.scope} limit in ${limit.unit}`;
|
|
517
|
+
}
|
|
518
|
+
function positiveFinite(value) {
|
|
519
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
520
|
+
}
|
|
521
|
+
function nonempty(value) {
|
|
522
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
523
|
+
}
|
|
524
|
+
function optionalNonempty(name, value) {
|
|
525
|
+
return value === void 0 || nonempty(value) ? void 0 : invalid(`${name} must be a nonempty string`);
|
|
526
|
+
}
|
|
527
|
+
function record(value) {
|
|
528
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
529
|
+
}
|
|
530
|
+
function stringArray(value) {
|
|
531
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
532
|
+
}
|
|
533
|
+
function enumArray(value, allowed) {
|
|
534
|
+
return stringArray(value) && value.every((item) => allowed.includes(item));
|
|
535
|
+
}
|
|
536
|
+
function messageOf2(error) {
|
|
537
|
+
return error instanceof Error ? error.message : String(error);
|
|
538
|
+
}
|
|
539
|
+
function invalid(reason) {
|
|
540
|
+
return { kind: "invalid_request", reason };
|
|
541
|
+
}
|
|
542
|
+
function mismatch(reason) {
|
|
543
|
+
return { kind: "harness_mismatch", reason };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/codex-exec.ts
|
|
547
|
+
import { randomUUID } from "crypto";
|
|
548
|
+
import { isAbsolute } from "path";
|
|
549
|
+
import { performance } from "perf_hooks";
|
|
550
|
+
|
|
551
|
+
// src/codex-exec-session.ts
|
|
552
|
+
import { conversationItemRef } from "@titan-design/agent-protocol";
|
|
553
|
+
var DEFAULT_CODEX_KILL_GRACE_MS = 2e3;
|
|
554
|
+
async function runCodexExecSession(request, processHandle, executionId, now, killGraceMs = DEFAULT_CODEX_KILL_GRACE_MS, processWallTimeMs = request.wallTimeMs, cleanup) {
|
|
555
|
+
return new CodexExecSession(request, processHandle, executionId, now, killGraceMs, processWallTimeMs, cleanup).run();
|
|
556
|
+
}
|
|
557
|
+
var CodexExecSession = class {
|
|
558
|
+
constructor(request, processHandle, executionId, now, killGraceMs, processWallTimeMs, cleanup) {
|
|
559
|
+
this.request = request;
|
|
560
|
+
this.processHandle = processHandle;
|
|
561
|
+
this.executionId = executionId;
|
|
562
|
+
this.now = now;
|
|
563
|
+
this.killGraceMs = killGraceMs;
|
|
564
|
+
this.processWallTimeMs = processWallTimeMs;
|
|
565
|
+
this.cleanup = cleanup;
|
|
566
|
+
}
|
|
567
|
+
request;
|
|
568
|
+
processHandle;
|
|
569
|
+
executionId;
|
|
570
|
+
now;
|
|
571
|
+
killGraceMs;
|
|
572
|
+
processWallTimeMs;
|
|
573
|
+
cleanup;
|
|
574
|
+
conversation;
|
|
575
|
+
finalText;
|
|
576
|
+
usageRaw;
|
|
577
|
+
nativeTurnId;
|
|
578
|
+
turnCompleted = false;
|
|
579
|
+
nativeTerminalObserved = false;
|
|
580
|
+
streamError;
|
|
581
|
+
stderr = "";
|
|
582
|
+
stopReason;
|
|
583
|
+
timer;
|
|
584
|
+
forceTimer;
|
|
585
|
+
onAbort = () => this.stop("abort");
|
|
586
|
+
async run() {
|
|
587
|
+
try {
|
|
588
|
+
this.request.signal?.addEventListener("abort", this.onAbort);
|
|
589
|
+
this.timer = setTimeout(() => this.stop("timeout"), this.processWallTimeMs);
|
|
590
|
+
this.timer.unref?.();
|
|
591
|
+
this.emit({ kind: "execution_started", execution: { executionId: this.executionId } });
|
|
592
|
+
const [, , exit] = await Promise.all([this.readStdout(), this.readStderr(), this.processHandle.completed]);
|
|
593
|
+
let result = this.settle(exit);
|
|
594
|
+
result = await this.finishCleanup(result);
|
|
595
|
+
return this.finish(result);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
this.terminate();
|
|
598
|
+
await this.ignoreExitFailure();
|
|
599
|
+
let result = this.failure({ kind: "runtime_error", reason: messageOf3(error), native: error });
|
|
600
|
+
result = await this.finishCleanup(result);
|
|
601
|
+
return this.finish(result);
|
|
602
|
+
} finally {
|
|
603
|
+
this.dispose();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async readStdout() {
|
|
607
|
+
for await (const line of this.processHandle.stdout) {
|
|
608
|
+
if (!line.trim()) continue;
|
|
609
|
+
let event;
|
|
610
|
+
try {
|
|
611
|
+
event = JSON.parse(line);
|
|
612
|
+
} catch {
|
|
613
|
+
this.streamError = { kind: "runtime_error", reason: "Codex emitted malformed JSONL", native: { line } };
|
|
614
|
+
this.terminate();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.absorb(event);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async readStderr() {
|
|
621
|
+
for await (const line of this.processHandle.stderr) this.stderr = `${this.stderr}${line}
|
|
622
|
+
`.slice(-65536);
|
|
623
|
+
}
|
|
624
|
+
absorb(value) {
|
|
625
|
+
if (!record2(value) || typeof value.type !== "string") return;
|
|
626
|
+
if (value.type === "thread.started" && typeof value.thread_id === "string") this.identify(value.thread_id);
|
|
627
|
+
if (value.type === "turn.started") this.nativeTurnId = turnId(value) ?? this.nativeTurnId;
|
|
628
|
+
if (isAgentMessage(value)) {
|
|
629
|
+
this.finalText = value.item.text;
|
|
630
|
+
this.emit({ kind: "assistant_output", executionId: this.executionId, text: value.item.text });
|
|
631
|
+
}
|
|
632
|
+
if (value.type === "turn.completed") {
|
|
633
|
+
this.turnCompleted = true;
|
|
634
|
+
this.nativeTerminalObserved = true;
|
|
635
|
+
this.nativeTurnId = turnId(value) ?? this.nativeTurnId;
|
|
636
|
+
this.usageRaw = value.usage;
|
|
637
|
+
}
|
|
638
|
+
if (value.type === "turn.failed") this.nativeTerminalObserved = true;
|
|
639
|
+
if (value.type === "turn.failed" || value.type === "error") {
|
|
640
|
+
this.streamError = { kind: "runtime_error", reason: eventMessage(value), native: value };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
identify(nativeId) {
|
|
644
|
+
if (!nativeId.trim()) return this.failStream("Codex emitted an empty native thread ID");
|
|
645
|
+
if (this.request.target.kind === "resume" && nativeId !== this.request.target.conversation.nativeId) {
|
|
646
|
+
return this.failStream("Codex resumed a different native conversation");
|
|
647
|
+
}
|
|
648
|
+
const namespace = this.request.target.kind === "fresh" ? this.request.target.namespace : this.request.target.conversation.namespace;
|
|
649
|
+
this.conversation = { harness: "codex", namespace, nativeId };
|
|
650
|
+
this.emit({ kind: "conversation_identified", executionId: this.executionId, conversation: this.conversation });
|
|
651
|
+
}
|
|
652
|
+
failStream(reason) {
|
|
653
|
+
this.streamError = { kind: "runtime_error", reason };
|
|
654
|
+
this.terminate();
|
|
655
|
+
}
|
|
656
|
+
settle(exit) {
|
|
657
|
+
if (this.stopReason && !this.nativeTerminalObserved) return this.failure(cancelledUnknown(this.stopReason, this.request, exit));
|
|
658
|
+
if (this.stopReason === "timeout") return this.failure(timeoutFailure(this.request.wallTimeMs));
|
|
659
|
+
if (this.stopReason === "abort") return this.failure(abortFailure(this.request.signal));
|
|
660
|
+
if (this.streamError) return this.failure(this.streamError);
|
|
661
|
+
if (exit.exitCode !== 0) return this.failure(exitFailure(exit, this.stderr));
|
|
662
|
+
return this.success();
|
|
663
|
+
}
|
|
664
|
+
success() {
|
|
665
|
+
if (!this.conversation) return this.failure({ kind: "runtime_error", reason: "Codex completed without a thread.started event" });
|
|
666
|
+
if (!this.turnCompleted) return this.failure({ kind: "runtime_error", reason: "Codex completed without a turn.completed event" });
|
|
667
|
+
if (this.finalText === void 0) return this.failure({ kind: "runtime_error", reason: "Codex completed without a final agent message" });
|
|
668
|
+
const output = parseOutput(this.request, this.finalText);
|
|
669
|
+
if ("failure" in output) return this.failure(output.failure);
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
harness: "codex",
|
|
673
|
+
execution: { executionId: this.executionId, conversation: this.conversation },
|
|
674
|
+
conversation: this.conversation,
|
|
675
|
+
output: output.output,
|
|
676
|
+
usage: this.usage(),
|
|
677
|
+
transcript: { format: "codex-rollout-jsonl", namespace: this.conversation.namespace }
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
usage() {
|
|
681
|
+
if (!this.conversation || !record2(this.usageRaw)) return [];
|
|
682
|
+
const measurement = {
|
|
683
|
+
kind: "snapshot",
|
|
684
|
+
scope: "turn",
|
|
685
|
+
scopeId: this.turnScopeId(),
|
|
686
|
+
epoch: this.executionId,
|
|
687
|
+
sequence: 0,
|
|
688
|
+
model: this.request.native?.model ?? null,
|
|
689
|
+
tokens: tokenCounts(this.usageRaw),
|
|
690
|
+
cost: null,
|
|
691
|
+
source: "codex-exec.turn.completed.usage"
|
|
692
|
+
};
|
|
693
|
+
return [measurement];
|
|
694
|
+
}
|
|
695
|
+
turnScopeId() {
|
|
696
|
+
if (this.conversation && this.nativeTurnId) return conversationItemRef(this.conversation, "turn", this.nativeTurnId);
|
|
697
|
+
return `execution:${encodeURIComponent(this.executionId)}:turn`;
|
|
698
|
+
}
|
|
699
|
+
failure(failure) {
|
|
700
|
+
const execution = {
|
|
701
|
+
executionId: this.executionId,
|
|
702
|
+
...this.conversation ? { conversation: this.conversation } : {}
|
|
703
|
+
};
|
|
704
|
+
const result = {
|
|
705
|
+
ok: false,
|
|
706
|
+
harness: "codex",
|
|
707
|
+
execution,
|
|
708
|
+
failure,
|
|
709
|
+
usage: this.usage()
|
|
710
|
+
};
|
|
711
|
+
if (this.conversation) result.conversation = this.conversation;
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
stop(reason) {
|
|
715
|
+
if (this.stopReason) return;
|
|
716
|
+
this.stopReason = reason;
|
|
717
|
+
this.terminate();
|
|
718
|
+
}
|
|
719
|
+
terminate() {
|
|
720
|
+
this.processHandle.terminate("SIGTERM");
|
|
721
|
+
if (this.forceTimer) return;
|
|
722
|
+
this.forceTimer = setTimeout(() => this.processHandle.terminate("SIGKILL"), this.killGraceMs);
|
|
723
|
+
this.forceTimer.unref?.();
|
|
724
|
+
}
|
|
725
|
+
async ignoreExitFailure() {
|
|
726
|
+
try {
|
|
727
|
+
await this.processHandle.completed;
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
finish(result) {
|
|
732
|
+
for (const measurement of result.usage) {
|
|
733
|
+
this.emit({ kind: "usage", executionId: this.executionId, measurement });
|
|
734
|
+
}
|
|
735
|
+
if (this.streamError && result.ok) result = this.failure(this.streamError);
|
|
736
|
+
const cancelled = !result.ok && (result.failure.kind === "aborted" || result.failure.kind === "cancelled_unknown");
|
|
737
|
+
const outcome = result.ok ? "succeeded" : cancelled ? "cancelled" : "failed";
|
|
738
|
+
this.emit({ kind: "execution_finished", executionId: this.executionId, outcome });
|
|
739
|
+
return this.streamError && result.ok ? this.failure(this.streamError) : result;
|
|
740
|
+
}
|
|
741
|
+
emit(progress) {
|
|
742
|
+
if (!this.request.onProgress || this.streamError) return;
|
|
743
|
+
try {
|
|
744
|
+
this.request.onProgress({ ...progress, harness: "codex", atMs: this.now() });
|
|
745
|
+
} catch (error) {
|
|
746
|
+
this.streamError = { kind: "runtime_error", reason: `Codex progress callback failed: ${messageOf3(error)}`, native: error };
|
|
747
|
+
this.terminate();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async finishCleanup(result) {
|
|
751
|
+
if (!this.cleanup) return result;
|
|
752
|
+
try {
|
|
753
|
+
await this.cleanup();
|
|
754
|
+
return result;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
return this.failure({ kind: "runtime_error", reason: messageOf3(error), native: error });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
dispose() {
|
|
760
|
+
if (this.timer) clearTimeout(this.timer);
|
|
761
|
+
if (this.forceTimer) clearTimeout(this.forceTimer);
|
|
762
|
+
this.request.signal?.removeEventListener("abort", this.onAbort);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function parseOutput(request, text) {
|
|
766
|
+
const schema = request.native?.outputSchema;
|
|
767
|
+
if (!schema) return { output: { kind: "text", text } };
|
|
768
|
+
try {
|
|
769
|
+
return { output: { kind: "structured", value: schema.parse(JSON.parse(text)) } };
|
|
770
|
+
} catch (error) {
|
|
771
|
+
const reason = `structured output validation failed: ${messageOf3(error)}`;
|
|
772
|
+
return { failure: { kind: "output_invalid", reason, native: { text, error } } };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function tokenCounts(usage) {
|
|
776
|
+
const input = count(usage.input_tokens);
|
|
777
|
+
const output = count(usage.output_tokens);
|
|
778
|
+
return {
|
|
779
|
+
input,
|
|
780
|
+
output,
|
|
781
|
+
cachedInput: count(usage.cached_input_tokens),
|
|
782
|
+
cacheWriteInput: null,
|
|
783
|
+
reasoningOutput: count(usage.reasoning_output_tokens),
|
|
784
|
+
total: input === null || output === null ? null : input + output
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function count(value) {
|
|
788
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
789
|
+
}
|
|
790
|
+
function isAgentMessage(event) {
|
|
791
|
+
return event.type === "item.completed" && record2(event.item) && event.item.type === "agent_message" && typeof event.item.text === "string";
|
|
792
|
+
}
|
|
793
|
+
function turnId(event) {
|
|
794
|
+
if (typeof event.turn_id === "string" && event.turn_id.trim()) return event.turn_id;
|
|
795
|
+
if (record2(event.turn) && typeof event.turn.id === "string" && event.turn.id.trim()) return event.turn.id;
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
function timeoutFailure(wallTimeMs) {
|
|
799
|
+
return { kind: "wall_time_exceeded", reason: "Codex execution exceeded its wall deadline", wallTimeMs };
|
|
800
|
+
}
|
|
801
|
+
function abortFailure(signal) {
|
|
802
|
+
return { kind: "aborted", reason: String(signal?.reason ?? "aborted by caller") };
|
|
803
|
+
}
|
|
804
|
+
function cancelledUnknown(cause, request, exit) {
|
|
805
|
+
const requested = cause === "timeout" ? { kind: "wall_time_exceeded", wallTimeMs: request.wallTimeMs } : { kind: "external_abort", reason: String(request.signal?.reason ?? "aborted by caller") };
|
|
806
|
+
return {
|
|
807
|
+
kind: "cancelled_unknown",
|
|
808
|
+
reason: `Codex process stopped after ${cause} without a native terminal turn event`,
|
|
809
|
+
native: { cause, requested, processExit: exit }
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function exitFailure(exit, stderr) {
|
|
813
|
+
return {
|
|
814
|
+
kind: "runtime_error",
|
|
815
|
+
reason: stderr.trim() || `Codex exited with status ${String(exit.exitCode)}`,
|
|
816
|
+
native: exit
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function eventMessage(event) {
|
|
820
|
+
if (typeof event.message === "string") return event.message;
|
|
821
|
+
if (record2(event.error) && typeof event.error.message === "string") return event.error.message;
|
|
822
|
+
return `Codex emitted ${String(event.type)}`;
|
|
823
|
+
}
|
|
824
|
+
function record2(value) {
|
|
825
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
826
|
+
}
|
|
827
|
+
function messageOf3(error) {
|
|
828
|
+
return error instanceof Error ? error.message : String(error);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/codex-process.ts
|
|
832
|
+
import { execFile, spawn } from "child_process";
|
|
833
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
834
|
+
import { tmpdir } from "os";
|
|
835
|
+
import { join } from "path";
|
|
836
|
+
import { createInterface } from "readline";
|
|
837
|
+
async function inspectCodexVersion(executablePath, env, options) {
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
execFile(
|
|
840
|
+
executablePath,
|
|
841
|
+
["--version"],
|
|
842
|
+
{
|
|
843
|
+
env,
|
|
844
|
+
encoding: "utf8",
|
|
845
|
+
signal: options.signal,
|
|
846
|
+
timeout: Math.max(1, Math.ceil(options.timeoutMs)),
|
|
847
|
+
killSignal: "SIGKILL"
|
|
848
|
+
},
|
|
849
|
+
(error, stdout) => {
|
|
850
|
+
if (error) reject(error);
|
|
851
|
+
else resolve(stdout);
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function startCodexProcess(input) {
|
|
857
|
+
const child = spawn(input.executablePath, input.args, {
|
|
858
|
+
cwd: input.cwd,
|
|
859
|
+
env: input.env,
|
|
860
|
+
detached: process.platform !== "win32",
|
|
861
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
862
|
+
});
|
|
863
|
+
return {
|
|
864
|
+
stdout: lines(child.stdout),
|
|
865
|
+
stderr: lines(child.stderr),
|
|
866
|
+
completed: completionOf(child),
|
|
867
|
+
terminate: (signal) => terminateProcessGroup(child, signal)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
async function createCodexSchemaFile(schema) {
|
|
871
|
+
const directory = await mkdtemp(join(tmpdir(), "titan-codex-schema-"));
|
|
872
|
+
const path = join(directory, "output-schema.json");
|
|
873
|
+
try {
|
|
874
|
+
await writeFile(path, JSON.stringify(schema), { encoding: "utf8", mode: 384 });
|
|
875
|
+
return { path, dispose: () => rm(directory, { recursive: true, force: true }) };
|
|
876
|
+
} catch (error) {
|
|
877
|
+
await rm(directory, { recursive: true, force: true });
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function* lines(stream) {
|
|
882
|
+
const reader = createInterface({ input: stream });
|
|
883
|
+
for await (const line of reader) yield line;
|
|
884
|
+
}
|
|
885
|
+
function completionOf(child) {
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
child.once("error", reject);
|
|
888
|
+
child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function terminateProcessGroup(child, signal) {
|
|
892
|
+
if (child.pid && process.platform !== "win32") {
|
|
893
|
+
try {
|
|
894
|
+
process.kill(-child.pid, signal);
|
|
895
|
+
return;
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
child.kill(signal);
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/codex-exec.ts
|
|
906
|
+
var DEFAULT_CODEX_EXECUTABLE = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
907
|
+
var SUPPORTED_CODEX_EXEC_VERSION = "0.154.0-alpha.6.1";
|
|
908
|
+
var STRIPPED_CODEX_AUTH_VARS = ["CODEX_API_KEY", "OPENAI_API_KEY", "OPENAI_ACCESS_TOKEN"];
|
|
909
|
+
function createCodexExecAdapter(options, deps = {}) {
|
|
910
|
+
validateOptions(options);
|
|
911
|
+
const executablePath = options.executablePath ?? DEFAULT_CODEX_EXECUTABLE;
|
|
912
|
+
const env = prepareCodexEnv(options.env ?? process.env);
|
|
913
|
+
const descriptor = codexExecCapabilities();
|
|
914
|
+
return {
|
|
915
|
+
descriptor,
|
|
916
|
+
async run(request) {
|
|
917
|
+
const preflight = preflightHarnessRun(request, descriptor);
|
|
918
|
+
if (preflight) return failed(preflight);
|
|
919
|
+
const invalid2 = validateAdapterRequest(request);
|
|
920
|
+
if (invalid2) return failed(invalid2);
|
|
921
|
+
try {
|
|
922
|
+
return await runSupervised(request, { executablePath, env, killGraceMs: options.killGraceMs }, deps);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
return failed({ kind: "runtime_error", reason: messageOf4(error), native: error });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function prepareCodexEnv(env) {
|
|
930
|
+
const prepared = {};
|
|
931
|
+
for (const [name, value] of Object.entries(env)) {
|
|
932
|
+
if (value !== void 0 && !STRIPPED_CODEX_AUTH_VARS.includes(name)) prepared[name] = value;
|
|
933
|
+
}
|
|
934
|
+
return prepared;
|
|
935
|
+
}
|
|
936
|
+
function codexExecCapabilities() {
|
|
937
|
+
const supported = (evidence) => ({ status: "supported", evidence });
|
|
938
|
+
const unsupported = (reason) => ({ status: "unsupported", reason });
|
|
939
|
+
const capabilities = Object.fromEntries(
|
|
940
|
+
EXECUTION_CAPABILITIES.map((capability) => [
|
|
941
|
+
capability,
|
|
942
|
+
unsupported("not implemented by the supervised exec adapter")
|
|
943
|
+
])
|
|
944
|
+
);
|
|
945
|
+
capabilities.fresh_run = supported("codex exec starts a persisted thread");
|
|
946
|
+
capabilities.resume = supported("codex exec resume uses an explicit native thread ID");
|
|
947
|
+
capabilities.structured_output = supported("native output schema plus caller parser validation");
|
|
948
|
+
capabilities.external_cancellation = supported("owned local process group receives termination signals");
|
|
949
|
+
capabilities.persisted_transcript = supported("adapter never passes --ephemeral");
|
|
950
|
+
capabilities.token_reporting = supported("turn.completed usage is normalized with source provenance");
|
|
951
|
+
const limits = limitCapabilities(supported, unsupported);
|
|
952
|
+
return {
|
|
953
|
+
harness: "codex",
|
|
954
|
+
adapter: { name: "codex-exec-json", version: SUPPORTED_CODEX_EXEC_VERSION },
|
|
955
|
+
capabilities,
|
|
956
|
+
limits
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function limitCapabilities(supported, unsupported) {
|
|
960
|
+
const limits = [
|
|
961
|
+
{
|
|
962
|
+
unit: "milliseconds",
|
|
963
|
+
scope: "execution",
|
|
964
|
+
enforcement: "hard",
|
|
965
|
+
assessment: supported("local process deadline")
|
|
966
|
+
}
|
|
967
|
+
];
|
|
968
|
+
for (const unit of ["usd", "model_requests", "agent_iterations", "tokens"]) {
|
|
969
|
+
for (const scope of ["execution", "conversation"]) {
|
|
970
|
+
limits.push({
|
|
971
|
+
unit,
|
|
972
|
+
scope,
|
|
973
|
+
enforcement: "hard",
|
|
974
|
+
assessment: unsupported("codex exec has no verified hard cap in this unit")
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return limits;
|
|
979
|
+
}
|
|
980
|
+
function validateOptions(options) {
|
|
981
|
+
if (options.auth !== "cached-cli") throw new TypeError("Codex exec adapter supports only cached-cli authentication");
|
|
982
|
+
if (options.executablePath !== void 0) {
|
|
983
|
+
if (options.executablePath.trim().length === 0) throw new TypeError("executablePath must be a nonempty string");
|
|
984
|
+
if (!isAbsolute(options.executablePath)) throw new TypeError("executablePath must be absolute");
|
|
985
|
+
}
|
|
986
|
+
if (options.killGraceMs !== void 0 && (!Number.isFinite(options.killGraceMs) || options.killGraceMs < 0)) {
|
|
987
|
+
throw new TypeError("killGraceMs must be a finite nonnegative number");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function validateAdapterRequest(request) {
|
|
991
|
+
if (!request.prompt.trim()) return { kind: "invalid_request", reason: "Codex exec requires a nonempty prompt" };
|
|
992
|
+
if (!isAbsolute(request.cwd)) return { kind: "invalid_request", reason: "Codex exec requires an absolute cwd" };
|
|
993
|
+
if (!request.native?.model) return { kind: "invalid_request", reason: "Codex exec requires an explicit model" };
|
|
994
|
+
if (request.native.approvalPolicy && request.native.approvalPolicy !== "never") {
|
|
995
|
+
return { kind: "invalid_request", reason: "Codex exec supports only the noninteractive never approval policy" };
|
|
996
|
+
}
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
async function checkVersion(inspect, expected) {
|
|
1000
|
+
try {
|
|
1001
|
+
const actual = parseVersion(await inspect());
|
|
1002
|
+
if (actual === expected) return void 0;
|
|
1003
|
+
return { kind: "runtime_error", reason: `unsupported Codex CLI version ${actual}; expected ${expected}` };
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
const reason = `could not verify Codex CLI version: ${messageOf4(error)}`;
|
|
1006
|
+
return { kind: "runtime_error", reason, native: error };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function parseVersion(output) {
|
|
1010
|
+
const match = /^codex-cli\s+(\S+)/m.exec(output.trim());
|
|
1011
|
+
if (!match?.[1]) throw new Error(`unrecognized version output: ${output.trim()}`);
|
|
1012
|
+
return match[1];
|
|
1013
|
+
}
|
|
1014
|
+
async function runSupervised(request, config, deps) {
|
|
1015
|
+
const deadlineNow = deps.deadlineNow ?? performance.now.bind(performance);
|
|
1016
|
+
const startedAt = deadlineNow();
|
|
1017
|
+
const remaining = () => Math.max(0, request.wallTimeMs - (deadlineNow() - startedAt));
|
|
1018
|
+
let schemaFile;
|
|
1019
|
+
try {
|
|
1020
|
+
const versionOutput = await setupStep(
|
|
1021
|
+
(deps.inspectVersion ?? inspectCodexVersion)(config.executablePath, config.env, {
|
|
1022
|
+
signal: request.signal,
|
|
1023
|
+
timeoutMs: remaining()
|
|
1024
|
+
}),
|
|
1025
|
+
request.signal,
|
|
1026
|
+
remaining()
|
|
1027
|
+
);
|
|
1028
|
+
const versionFailure = await checkVersion(async () => versionOutput, SUPPORTED_CODEX_EXEC_VERSION);
|
|
1029
|
+
if (versionFailure) return failed(versionFailure);
|
|
1030
|
+
if (request.native?.outputSchema) {
|
|
1031
|
+
schemaFile = await setupStep(
|
|
1032
|
+
(deps.createSchemaFile ?? createCodexSchemaFile)(request.native.outputSchema.jsonSchema),
|
|
1033
|
+
request.signal,
|
|
1034
|
+
remaining(),
|
|
1035
|
+
(lateFile) => lateFile.dispose()
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if (request.signal?.aborted) return failed(abortFailure2(request.signal));
|
|
1039
|
+
const processWallTimeMs = remaining();
|
|
1040
|
+
if (processWallTimeMs <= 0) return failed(timeoutFailure2(request.wallTimeMs));
|
|
1041
|
+
const input = {
|
|
1042
|
+
executablePath: config.executablePath,
|
|
1043
|
+
args: buildCodexExecArgs(request, schemaFile?.path),
|
|
1044
|
+
cwd: request.cwd,
|
|
1045
|
+
env: config.env
|
|
1046
|
+
};
|
|
1047
|
+
const executionId = (deps.executionId ?? randomUUID)();
|
|
1048
|
+
const processHandle = (deps.start ?? startCodexProcess)(input);
|
|
1049
|
+
const cleanup = async () => {
|
|
1050
|
+
const file = schemaFile;
|
|
1051
|
+
schemaFile = void 0;
|
|
1052
|
+
await file?.dispose();
|
|
1053
|
+
};
|
|
1054
|
+
return await runCodexExecSession(
|
|
1055
|
+
request,
|
|
1056
|
+
processHandle,
|
|
1057
|
+
executionId,
|
|
1058
|
+
deps.now ?? Date.now,
|
|
1059
|
+
config.killGraceMs,
|
|
1060
|
+
processWallTimeMs,
|
|
1061
|
+
cleanup
|
|
1062
|
+
);
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
if (request.signal?.aborted) return failed(abortFailure2(request.signal));
|
|
1065
|
+
if (error instanceof SetupTimeoutError || isProcessTimeout(error)) {
|
|
1066
|
+
return failed(timeoutFailure2(request.wallTimeMs));
|
|
1067
|
+
}
|
|
1068
|
+
return failed({ kind: "runtime_error", reason: messageOf4(error), native: error });
|
|
1069
|
+
} finally {
|
|
1070
|
+
await schemaFile?.dispose();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function buildCodexExecArgs(request, schemaPath) {
|
|
1074
|
+
const native = request.native;
|
|
1075
|
+
const args = ["exec"];
|
|
1076
|
+
if (request.target.kind === "resume") args.push("resume");
|
|
1077
|
+
args.push("--json", "--strict-config", "--ignore-user-config", "--model", native.model);
|
|
1078
|
+
args.push("--config", `approval_policy=${JSON.stringify(native.approvalPolicy ?? "never")}`);
|
|
1079
|
+
args.push("--config", `sandbox_mode=${JSON.stringify(native.sandbox ?? "read-only")}`);
|
|
1080
|
+
if (native.reasoningEffort) {
|
|
1081
|
+
args.push("--config", `model_reasoning_effort=${JSON.stringify(native.reasoningEffort)}`);
|
|
1082
|
+
}
|
|
1083
|
+
if (request.target.kind === "fresh") args.push("--cd", request.cwd);
|
|
1084
|
+
if (schemaPath) args.push("--output-schema", schemaPath);
|
|
1085
|
+
if (request.target.kind === "resume") args.push(request.target.conversation.nativeId);
|
|
1086
|
+
args.push("--", request.prompt);
|
|
1087
|
+
return args;
|
|
1088
|
+
}
|
|
1089
|
+
function failed(failure) {
|
|
1090
|
+
return { ok: false, harness: "codex", failure, usage: [] };
|
|
1091
|
+
}
|
|
1092
|
+
function messageOf4(error) {
|
|
1093
|
+
return error instanceof Error ? error.message : String(error);
|
|
1094
|
+
}
|
|
1095
|
+
function timeoutFailure2(wallTimeMs) {
|
|
1096
|
+
return { kind: "wall_time_exceeded", reason: "Codex execution exceeded its wall deadline", wallTimeMs };
|
|
1097
|
+
}
|
|
1098
|
+
function abortFailure2(signal) {
|
|
1099
|
+
return { kind: "aborted", reason: String(signal.reason ?? "aborted by caller") };
|
|
1100
|
+
}
|
|
1101
|
+
var SetupTimeoutError = class extends Error {
|
|
1102
|
+
};
|
|
1103
|
+
function setupStep(work, signal, timeoutMs, disposeLateValue) {
|
|
1104
|
+
return new Promise((resolve, reject) => {
|
|
1105
|
+
let settled = false;
|
|
1106
|
+
const finish = (callback) => {
|
|
1107
|
+
if (settled) return false;
|
|
1108
|
+
settled = true;
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1111
|
+
callback();
|
|
1112
|
+
return true;
|
|
1113
|
+
};
|
|
1114
|
+
const onAbort = () => finish(() => reject(signal?.reason ?? new Error("aborted by caller")));
|
|
1115
|
+
const timer = setTimeout(
|
|
1116
|
+
() => finish(() => reject(new SetupTimeoutError("Codex setup exceeded its wall deadline"))),
|
|
1117
|
+
Math.max(0, timeoutMs)
|
|
1118
|
+
);
|
|
1119
|
+
timer.unref?.();
|
|
1120
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1121
|
+
if (signal?.aborted) onAbort();
|
|
1122
|
+
work.then(
|
|
1123
|
+
(value) => {
|
|
1124
|
+
if (!finish(() => resolve(value)) && disposeLateValue) {
|
|
1125
|
+
void Promise.resolve(disposeLateValue(value)).catch(() => void 0);
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
(error) => finish(() => reject(error))
|
|
1129
|
+
);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function isProcessTimeout(error) {
|
|
1133
|
+
if (typeof error !== "object" || error === null) return false;
|
|
1134
|
+
const value = error;
|
|
1135
|
+
return value.code === "ETIMEDOUT" || value.killed === true && value.signal === "SIGKILL";
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/durable-dispatcher.ts
|
|
1139
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1140
|
+
import {
|
|
1141
|
+
isTerminalExecutionPhase as isTerminalExecutionPhase2
|
|
1142
|
+
} from "@titan-design/agent-protocol";
|
|
1143
|
+
|
|
1144
|
+
// src/durable-result.ts
|
|
1145
|
+
function terminalFromHarnessResult(result) {
|
|
1146
|
+
if (result.ok) {
|
|
1147
|
+
return {
|
|
1148
|
+
outcome: "succeeded",
|
|
1149
|
+
result: {
|
|
1150
|
+
harness: result.harness,
|
|
1151
|
+
adapterExecution: result.execution,
|
|
1152
|
+
conversation: result.conversation,
|
|
1153
|
+
output: result.output,
|
|
1154
|
+
usage: result.usage,
|
|
1155
|
+
...result.transcript ? { transcript: result.transcript } : {}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const reason = `${result.failure.kind}: ${result.failure.reason}`;
|
|
1160
|
+
if (result.failure.kind === "aborted" || result.failure.kind === "cancelled_unknown" || result.failure.kind === "wall_time_exceeded") {
|
|
1161
|
+
return { outcome: "cancellation_unknown", reason, evidence: cancellationEvidence(result.failure) };
|
|
1162
|
+
}
|
|
1163
|
+
return { outcome: "failed", reason, retryable: false };
|
|
1164
|
+
}
|
|
1165
|
+
function cancellationEvidence(failure) {
|
|
1166
|
+
if (failure.kind === "wall_time_exceeded") return "wall deadline elapsed after durable dispatch without a native terminal acknowledgement";
|
|
1167
|
+
return failure.kind === "cancelled_unknown" ? "adapter reported cancellation without a native terminal acknowledgement" : "adapter was aborted after durable dispatch without a native terminal acknowledgement";
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/durable-dispatcher-support.ts
|
|
1171
|
+
function failureKind(kind) {
|
|
1172
|
+
return kind === "ownership_lost" || kind === "revision_conflict" ? "ownership_lost" : "recovery_required";
|
|
1173
|
+
}
|
|
1174
|
+
function positive(name, value) {
|
|
1175
|
+
if (!Number.isFinite(value) || value <= 0) throw new TypeError(`${name} must be a positive finite number`);
|
|
1176
|
+
}
|
|
1177
|
+
function nonempty2(name, value) {
|
|
1178
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must be a nonempty string`);
|
|
1179
|
+
}
|
|
1180
|
+
function recordFence(record3) {
|
|
1181
|
+
if (!record3.owner) throw new Error("execution has no active owner");
|
|
1182
|
+
return { supervisorId: record3.owner.supervisorId, generation: record3.owner.generation };
|
|
1183
|
+
}
|
|
1184
|
+
function ownsLiveFence(record3, fence, now) {
|
|
1185
|
+
return record3.owner?.supervisorId === fence.supervisorId && record3.owner.generation === fence.generation && Date.parse(record3.owner.leaseUntil) > now;
|
|
1186
|
+
}
|
|
1187
|
+
function validateDispatchInput(input, adapterHarness) {
|
|
1188
|
+
nonempty2("executionId", input.executionId);
|
|
1189
|
+
nonempty2("requestKey", input.requestKey);
|
|
1190
|
+
if (input.request.harness !== adapterHarness) throw new TypeError("request harness does not match adapter");
|
|
1191
|
+
}
|
|
1192
|
+
function startLiveExecution(options) {
|
|
1193
|
+
let resolveProblem;
|
|
1194
|
+
const problemPromise = new Promise((resolve) => {
|
|
1195
|
+
resolveProblem = resolve;
|
|
1196
|
+
});
|
|
1197
|
+
const timer = options.schedule(options.onRenew, options.renewEveryMs);
|
|
1198
|
+
if (typeof timer === "object") timer.unref?.();
|
|
1199
|
+
const live = {
|
|
1200
|
+
fence: options.fence,
|
|
1201
|
+
controller: options.controller,
|
|
1202
|
+
completion: Promise.resolve(void 0),
|
|
1203
|
+
problemPromise,
|
|
1204
|
+
resolveProblem,
|
|
1205
|
+
timer,
|
|
1206
|
+
...options.externalSignal ? { externalSignal: options.externalSignal, onExternalAbort: options.onExternalAbort } : {}
|
|
1207
|
+
};
|
|
1208
|
+
options.externalSignal?.addEventListener("abort", options.onExternalAbort, { once: true });
|
|
1209
|
+
return live;
|
|
1210
|
+
}
|
|
1211
|
+
async function raceLiveResult(result, live) {
|
|
1212
|
+
return Promise.race([
|
|
1213
|
+
result.then((value) => ({ kind: "result", result: value })),
|
|
1214
|
+
live.problemPromise.then((problem) => ({ kind: "problem", problem }))
|
|
1215
|
+
]);
|
|
1216
|
+
}
|
|
1217
|
+
function readLedgerRecord(ledger, executionId) {
|
|
1218
|
+
try {
|
|
1219
|
+
return { ok: true, record: ledger.get(executionId) };
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
return { ok: false, evidence: `execution ledger read failed: ${message(error)}` };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function message(error) {
|
|
1225
|
+
return error instanceof Error ? error.message : String(error);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/durable-reconcile.ts
|
|
1229
|
+
import { isTerminalExecutionPhase } from "@titan-design/agent-protocol";
|
|
1230
|
+
function reconcileLive(ledger, executionId, live, now, fail) {
|
|
1231
|
+
const read = readLedgerRecord(ledger, executionId);
|
|
1232
|
+
const evidence = !read.ok ? `live ownership could not be verified: ${read.evidence}` : !read.record || !ownsLiveFence(read.record, live.fence, now) ? "live execution no longer has its captured owner lease" : void 0;
|
|
1233
|
+
if (evidence) {
|
|
1234
|
+
fail(evidence);
|
|
1235
|
+
return { kind: "ownership_lost", evidence };
|
|
1236
|
+
}
|
|
1237
|
+
return { kind: "running", running: { runnerRef: executionId, completion: live.completion }, evidence: "same dispatcher owns the completion handle" };
|
|
1238
|
+
}
|
|
1239
|
+
function claimForRecovery(record3, options, apply) {
|
|
1240
|
+
if (record3.owner && Date.parse(record3.owner.leaseUntil) > options.now) return { ok: false, evidence: "another live owner lease fences recovery" };
|
|
1241
|
+
try {
|
|
1242
|
+
const result = apply(record3, {
|
|
1243
|
+
kind: "claim_owner",
|
|
1244
|
+
owner: { supervisorId: options.supervisorId, generation: record3.ownerGeneration + 1, leaseUntil: options.leaseUntil }
|
|
1245
|
+
});
|
|
1246
|
+
return result.ok ? { ok: true, record: result.record } : { ok: false, evidence: result.reason };
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
return { ok: false, evidence: `ownership claim failed: ${message(error)}` };
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function closePrepared(record3, apply) {
|
|
1252
|
+
const terminal = {
|
|
1253
|
+
outcome: "failed",
|
|
1254
|
+
reason: "durable preparation exists but dispatch never began",
|
|
1255
|
+
retryable: true
|
|
1256
|
+
};
|
|
1257
|
+
try {
|
|
1258
|
+
const result = apply(record3, { kind: "finish", fence: recordFence(record3), terminal });
|
|
1259
|
+
return result.ok ? { kind: "terminal", terminal, evidence: "prepared phase proves begin_dispatch was never committed" } : { kind: "ownership_lost", evidence: result.reason };
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
return { kind: "ownership_lost", evidence: `prepared execution could not be closed: ${message(error)}` };
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function requireRecovery(record3, fence, evidence, apply) {
|
|
1265
|
+
if (!record3) return { kind: "ownership_lost", evidence: "execution record is unavailable" };
|
|
1266
|
+
if (!record3.owner || record3.owner.supervisorId !== fence.supervisorId || record3.owner.generation !== fence.generation) {
|
|
1267
|
+
return { kind: "ownership_lost", record: record3, evidence: "execution ownership changed before recovery evidence could be stored" };
|
|
1268
|
+
}
|
|
1269
|
+
if (record3.phase === "recovery_required") return { kind: "recovery_required", record: record3, evidence };
|
|
1270
|
+
if (isTerminalExecutionPhase(record3.phase)) return { kind: "terminal", record: record3, terminal: record3.terminal };
|
|
1271
|
+
try {
|
|
1272
|
+
const result = apply(record3, { kind: "require_recovery", fence, evidence });
|
|
1273
|
+
return result.ok ? { kind: "recovery_required", record: result.record, evidence } : { kind: "ownership_lost", record: record3, evidence: result.reason };
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
return { kind: "ownership_lost", record: record3, evidence: `recovery evidence was not durable: ${message(error)}` };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/durable-dispatcher.ts
|
|
1280
|
+
function createDurableHarnessDispatcher(adapter, options, deps = {}) {
|
|
1281
|
+
return new Dispatcher(adapter, options, deps);
|
|
1282
|
+
}
|
|
1283
|
+
var Dispatcher = class {
|
|
1284
|
+
constructor(adapter, options, deps) {
|
|
1285
|
+
this.adapter = adapter;
|
|
1286
|
+
this.options = options;
|
|
1287
|
+
nonempty2("supervisorId", options.supervisorId);
|
|
1288
|
+
positive("leaseMs", options.leaseMs);
|
|
1289
|
+
this.renewEveryMs = options.renewEveryMs ?? Math.max(1, Math.floor(options.leaseMs / 3));
|
|
1290
|
+
positive("renewEveryMs", this.renewEveryMs);
|
|
1291
|
+
if (this.renewEveryMs >= options.leaseMs) throw new TypeError("renewEveryMs must be less than leaseMs");
|
|
1292
|
+
this.eventId = deps.eventId ?? randomUUID2;
|
|
1293
|
+
this.now = deps.now ?? Date.now;
|
|
1294
|
+
this.schedule = deps.setInterval ?? globalThis.setInterval;
|
|
1295
|
+
this.unschedule = deps.clearInterval ?? globalThis.clearInterval;
|
|
1296
|
+
}
|
|
1297
|
+
adapter;
|
|
1298
|
+
options;
|
|
1299
|
+
live = /* @__PURE__ */ new Map();
|
|
1300
|
+
eventId;
|
|
1301
|
+
now;
|
|
1302
|
+
schedule;
|
|
1303
|
+
unschedule;
|
|
1304
|
+
renewEveryMs;
|
|
1305
|
+
async dispatch(input) {
|
|
1306
|
+
validateDispatchInput(input, this.adapter.descriptor.harness);
|
|
1307
|
+
const prepared = this.prepare(input);
|
|
1308
|
+
if (input.request.signal?.aborted) return this.preCancelled(input, prepared);
|
|
1309
|
+
this.apply(input.executionId, {
|
|
1310
|
+
kind: "begin_dispatch",
|
|
1311
|
+
fence: recordFence(prepared)
|
|
1312
|
+
});
|
|
1313
|
+
const controller = new AbortController();
|
|
1314
|
+
const live = this.startLive(input.executionId, recordFence(prepared), controller, input.request.signal);
|
|
1315
|
+
this.live.set(input.executionId, live);
|
|
1316
|
+
const completion = this.runAdapter(input, live);
|
|
1317
|
+
live.completion = completion;
|
|
1318
|
+
completion.catch(() => void 0);
|
|
1319
|
+
return { executionId: input.executionId, requestKey: input.requestKey, runnerRef: input.executionId, completion };
|
|
1320
|
+
}
|
|
1321
|
+
async reconcile(executionId) {
|
|
1322
|
+
nonempty2("executionId", executionId);
|
|
1323
|
+
const active = this.live.get(executionId);
|
|
1324
|
+
if (active) return reconcileLive(this.options.ledger, executionId, active, this.now(), (evidence) => this.failLive(active, "ownership_lost", evidence));
|
|
1325
|
+
const read = readLedgerRecord(this.options.ledger, executionId);
|
|
1326
|
+
if (!read.ok) return { kind: "ownership_lost", evidence: read.evidence };
|
|
1327
|
+
const current = read.record;
|
|
1328
|
+
if (!current) return { kind: "unknown", evidence: "execution ledger has no durable receipt; native submission cannot be excluded" };
|
|
1329
|
+
if (current.terminal) return { kind: "terminal", terminal: current.terminal, evidence: "terminal execution ledger receipt" };
|
|
1330
|
+
const claimed = claimForRecovery(
|
|
1331
|
+
current,
|
|
1332
|
+
{ now: this.now(), supervisorId: this.options.supervisorId, leaseUntil: this.leaseUntil() },
|
|
1333
|
+
(record3, fields) => this.tryApply(record3, fields)
|
|
1334
|
+
);
|
|
1335
|
+
if (!claimed.ok) return { kind: "ownership_lost", evidence: claimed.evidence };
|
|
1336
|
+
if (claimed.record.phase === "prepared") {
|
|
1337
|
+
return closePrepared(claimed.record, (record3, fields) => this.tryApply(record3, fields));
|
|
1338
|
+
}
|
|
1339
|
+
if (claimed.record.phase !== "recovery_required") {
|
|
1340
|
+
const recovered = requireRecovery(claimed.record, recordFence(claimed.record), "dispatcher restarted without a transport attachment handle", (record3, fields) => this.tryApply(record3, fields));
|
|
1341
|
+
if (recovered.kind === "ownership_lost") return recovered;
|
|
1342
|
+
if (recovered.kind === "terminal") return { kind: "terminal", terminal: recovered.terminal, evidence: "execution became terminal during reconciliation" };
|
|
1343
|
+
}
|
|
1344
|
+
return { kind: "unknown", evidence: "transport cannot prove liveness, terminal state, or safe absence after restart" };
|
|
1345
|
+
}
|
|
1346
|
+
cancel(executionId, reason) {
|
|
1347
|
+
nonempty2("reason", reason);
|
|
1348
|
+
const live = this.live.get(executionId);
|
|
1349
|
+
if (!live) return { kind: "not_live", evidence: "this dispatcher has no owned live handle" };
|
|
1350
|
+
let current;
|
|
1351
|
+
try {
|
|
1352
|
+
current = this.options.ledger.get(executionId);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
this.failLive(live, "ownership_lost", `cancellation ownership could not be verified: ${message(error)}`);
|
|
1355
|
+
return { kind: "ownership_lost", evidence: message(error) };
|
|
1356
|
+
}
|
|
1357
|
+
if (!current) return { kind: "ownership_lost", evidence: "execution record disappeared" };
|
|
1358
|
+
if (current.phase === "cancel_requested" && current.cancellation?.reason === reason) {
|
|
1359
|
+
if (!ownsLiveFence(current, live.fence, this.now())) {
|
|
1360
|
+
this.failLive(live, "ownership_lost", "cancellation owner lease changed or expired");
|
|
1361
|
+
return { kind: "ownership_lost", record: current, evidence: "cancellation owner lease changed or expired" };
|
|
1362
|
+
}
|
|
1363
|
+
live.controller.abort(new Error(reason));
|
|
1364
|
+
return { kind: "requested", record: current };
|
|
1365
|
+
}
|
|
1366
|
+
let result;
|
|
1367
|
+
try {
|
|
1368
|
+
result = this.tryApply(current, { kind: "request_cancellation", fence: live.fence, reason });
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
this.failLive(live, "recovery_required", `cancellation intent was not durable: ${message(error)}`);
|
|
1371
|
+
return { kind: "ownership_lost", record: current, evidence: message(error) };
|
|
1372
|
+
}
|
|
1373
|
+
if (!result.ok) {
|
|
1374
|
+
this.failLive(live, failureKind(result.kind), `cancellation intent was rejected: ${result.reason}`);
|
|
1375
|
+
return { kind: "ownership_lost", record: current, evidence: result.reason };
|
|
1376
|
+
}
|
|
1377
|
+
live.controller.abort(new Error(reason));
|
|
1378
|
+
return { kind: "requested", record: result.record };
|
|
1379
|
+
}
|
|
1380
|
+
prepare(input) {
|
|
1381
|
+
const conversation = input.request.target.kind === "resume" ? input.request.target.conversation : void 0;
|
|
1382
|
+
const result = this.options.ledger.apply({
|
|
1383
|
+
kind: "prepare",
|
|
1384
|
+
executionId: input.executionId,
|
|
1385
|
+
eventId: this.eventId(),
|
|
1386
|
+
expectedRevision: 0,
|
|
1387
|
+
occurredAt: this.iso(),
|
|
1388
|
+
execution: { executionId: input.executionId, ...conversation ? { conversation } : {} },
|
|
1389
|
+
...input.agent ? { agent: input.agent } : {},
|
|
1390
|
+
harness: input.request.harness,
|
|
1391
|
+
requestKey: input.requestKey,
|
|
1392
|
+
target: input.request.target,
|
|
1393
|
+
owner: { supervisorId: this.options.supervisorId, generation: 1, leaseUntil: this.leaseUntil() }
|
|
1394
|
+
});
|
|
1395
|
+
if (!result.ok) throw new Error(`could not prepare execution: ${result.kind}: ${result.reason}`);
|
|
1396
|
+
if (!result.applied) throw new Error("dispatch requires a new execution identity");
|
|
1397
|
+
return result.record;
|
|
1398
|
+
}
|
|
1399
|
+
preCancelled(input, record3) {
|
|
1400
|
+
const reason = String(input.request.signal?.reason ?? "aborted before dispatch");
|
|
1401
|
+
const result = this.apply(input.executionId, {
|
|
1402
|
+
kind: "finish",
|
|
1403
|
+
fence: recordFence(record3),
|
|
1404
|
+
terminal: { outcome: "cancelled", reason }
|
|
1405
|
+
});
|
|
1406
|
+
const terminal = result.terminal;
|
|
1407
|
+
const completion = Promise.resolve({ kind: "terminal", record: result, terminal });
|
|
1408
|
+
return { executionId: input.executionId, requestKey: input.requestKey, runnerRef: input.executionId, completion };
|
|
1409
|
+
}
|
|
1410
|
+
startLive(executionId, fence, controller, externalSignal) {
|
|
1411
|
+
return startLiveExecution({
|
|
1412
|
+
fence,
|
|
1413
|
+
controller,
|
|
1414
|
+
schedule: this.schedule,
|
|
1415
|
+
renewEveryMs: this.renewEveryMs,
|
|
1416
|
+
onRenew: () => this.renew(executionId),
|
|
1417
|
+
onExternalAbort: () => {
|
|
1418
|
+
this.cancel(executionId, String(externalSignal?.reason ?? "aborted by caller"));
|
|
1419
|
+
},
|
|
1420
|
+
...externalSignal ? { externalSignal } : {}
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
async runAdapter(input, live) {
|
|
1424
|
+
try {
|
|
1425
|
+
const raced = await raceLiveResult(this.adapter.run({
|
|
1426
|
+
...input.request,
|
|
1427
|
+
signal: live.controller.signal,
|
|
1428
|
+
onProgress: (progress) => this.progress(input.executionId, live, progress, input.request.onProgress)
|
|
1429
|
+
}), live);
|
|
1430
|
+
if (raced.kind === "problem") {
|
|
1431
|
+
return this.problemSettlement(input.executionId, live, raced.problem);
|
|
1432
|
+
}
|
|
1433
|
+
const result = raced.result;
|
|
1434
|
+
if (!result.ok && result.failure.kind === "runtime_error") {
|
|
1435
|
+
return this.problemSettlement(input.executionId, live, {
|
|
1436
|
+
kind: "recovery_required",
|
|
1437
|
+
evidence: `adapter runtime state is ambiguous: ${result.failure.reason}`
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
return this.persistResult(input.executionId, live, result);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
return this.problemSettlement(input.executionId, live, {
|
|
1443
|
+
kind: "recovery_required",
|
|
1444
|
+
evidence: `adapter threw after durable dispatch: ${message(error)}`
|
|
1445
|
+
});
|
|
1446
|
+
} finally {
|
|
1447
|
+
this.stopLive(input.executionId, live);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
progress(executionId, live, progress, forward) {
|
|
1451
|
+
if (progress.kind === "execution_started") {
|
|
1452
|
+
if (live.adapterExecutionId && live.adapterExecutionId !== progress.execution.executionId) {
|
|
1453
|
+
this.failLive(live, "recovery_required", "adapter execution identity changed during one dispatch");
|
|
1454
|
+
} else {
|
|
1455
|
+
live.adapterExecutionId = progress.execution.executionId;
|
|
1456
|
+
const read = readLedgerRecord(this.options.ledger, executionId);
|
|
1457
|
+
if (!read.ok || !read.record) this.failLive(live, "ownership_lost", read.ok ? "execution record disappeared" : read.evidence);
|
|
1458
|
+
const current = read.ok ? read.record : void 0;
|
|
1459
|
+
if (current && (current.phase === "dispatching" || current.phase === "recovery_required")) {
|
|
1460
|
+
try {
|
|
1461
|
+
const result = this.tryApply(current, {
|
|
1462
|
+
kind: "observe_running",
|
|
1463
|
+
fence: live.fence,
|
|
1464
|
+
runnerRef: executionId,
|
|
1465
|
+
adapterExecution: progress.execution,
|
|
1466
|
+
evidence: "adapter emitted execution_started"
|
|
1467
|
+
});
|
|
1468
|
+
if (!result.ok) this.failLive(live, failureKind(result.kind), result.reason);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
this.failLive(live, "recovery_required", `could not persist running observation: ${message(error)}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (progress.kind === "conversation_identified") this.observeConversation(executionId, live, progress);
|
|
1476
|
+
forward?.(progress);
|
|
1477
|
+
}
|
|
1478
|
+
observeConversation(executionId, live, progress) {
|
|
1479
|
+
if (live.adapterExecutionId && live.adapterExecutionId !== progress.executionId) {
|
|
1480
|
+
this.failLive(live, "recovery_required", "conversation referred to a different adapter execution");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const read = readLedgerRecord(this.options.ledger, executionId);
|
|
1485
|
+
if (!read.ok) {
|
|
1486
|
+
this.failLive(live, "ownership_lost", read.evidence);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const current = read.record;
|
|
1490
|
+
if (!current || isTerminalExecutionPhase2(current.phase)) {
|
|
1491
|
+
this.failLive(live, "ownership_lost", "execution record is unavailable or terminal while identifying conversation");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const result = this.tryApply(current, { kind: "identify_conversation", fence: live.fence, conversation: progress.conversation });
|
|
1495
|
+
if (!result.ok) this.failLive(live, failureKind(result.kind), result.reason);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
this.failLive(live, "recovery_required", `could not persist conversation identity: ${message(error)}`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
persistResult(executionId, live, result) {
|
|
1501
|
+
const read = readLedgerRecord(this.options.ledger, executionId);
|
|
1502
|
+
if (!read.ok) return { kind: "ownership_lost", evidence: read.evidence };
|
|
1503
|
+
const current = read.record;
|
|
1504
|
+
if (!current) return { kind: "ownership_lost", evidence: "execution record disappeared before terminal persistence" };
|
|
1505
|
+
const terminal = terminalFromHarnessResult(result);
|
|
1506
|
+
const applied = this.tryApply(current, {
|
|
1507
|
+
kind: "finish",
|
|
1508
|
+
fence: live.fence,
|
|
1509
|
+
terminal,
|
|
1510
|
+
...result.execution ? { adapterExecution: result.execution } : {},
|
|
1511
|
+
...result.conversation ? { conversation: result.conversation } : {}
|
|
1512
|
+
});
|
|
1513
|
+
if (!applied.ok) return requireRecovery(current, live.fence, `terminal result was not durable: ${applied.reason}`, (record3, fields) => this.tryApply(record3, fields));
|
|
1514
|
+
return {
|
|
1515
|
+
kind: "terminal",
|
|
1516
|
+
record: applied.record,
|
|
1517
|
+
terminal
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
renew(executionId) {
|
|
1521
|
+
const live = this.live.get(executionId);
|
|
1522
|
+
if (!live) return;
|
|
1523
|
+
try {
|
|
1524
|
+
const current = this.options.ledger.get(executionId);
|
|
1525
|
+
if (!current || isTerminalExecutionPhase2(current.phase)) {
|
|
1526
|
+
this.failLive(live, "ownership_lost", "execution record is unavailable or terminal during lease renewal");
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const result = this.tryApply(current, { kind: "renew_owner", fence: live.fence, leaseUntil: this.leaseUntil() });
|
|
1530
|
+
if (!result.ok) this.failLive(live, failureKind(result.kind), `lease renewal failed: ${result.reason}`);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
this.failLive(live, "ownership_lost", `lease renewal threw: ${message(error)}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
failLive(live, kind, evidence) {
|
|
1536
|
+
if (!live.problem) {
|
|
1537
|
+
live.problem = { kind, evidence };
|
|
1538
|
+
live.resolveProblem(live.problem);
|
|
1539
|
+
}
|
|
1540
|
+
live.controller.abort(new Error(evidence));
|
|
1541
|
+
}
|
|
1542
|
+
problemSettlement(executionId, live, problem) {
|
|
1543
|
+
const read = readLedgerRecord(this.options.ledger, executionId);
|
|
1544
|
+
if (!read.ok) return { kind: "ownership_lost", evidence: `${problem.evidence}; ${read.evidence}` };
|
|
1545
|
+
if (problem.kind === "ownership_lost") return { kind: "ownership_lost", record: read.record, evidence: problem.evidence };
|
|
1546
|
+
return requireRecovery(read.record, live.fence, problem.evidence, (record3, fields) => this.tryApply(record3, fields));
|
|
1547
|
+
}
|
|
1548
|
+
tryApply(current, fields) {
|
|
1549
|
+
return this.options.ledger.apply({
|
|
1550
|
+
...fields,
|
|
1551
|
+
executionId: current.execution.executionId,
|
|
1552
|
+
eventId: this.eventId(),
|
|
1553
|
+
expectedRevision: current.revision,
|
|
1554
|
+
occurredAt: this.iso()
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
apply(executionId, fields) {
|
|
1558
|
+
const current = this.options.ledger.get(executionId);
|
|
1559
|
+
if (!current) throw new Error(`execution ${executionId} is missing`);
|
|
1560
|
+
const result = this.tryApply(current, fields);
|
|
1561
|
+
if (!result.ok) throw new Error(`${result.kind}: ${result.reason}`);
|
|
1562
|
+
return result.record;
|
|
1563
|
+
}
|
|
1564
|
+
stopLive(executionId, live) {
|
|
1565
|
+
this.unschedule(live.timer);
|
|
1566
|
+
live.externalSignal?.removeEventListener("abort", live.onExternalAbort);
|
|
1567
|
+
if (this.live.get(executionId) === live) this.live.delete(executionId);
|
|
1568
|
+
}
|
|
1569
|
+
iso() {
|
|
1570
|
+
return new Date(this.now()).toISOString();
|
|
1571
|
+
}
|
|
1572
|
+
leaseUntil() {
|
|
1573
|
+
return new Date(this.now() + this.options.leaseMs).toISOString();
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
// src/claude-code-adapter.ts
|
|
1578
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1579
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1580
|
+
|
|
1581
|
+
// src/claude-code-result.ts
|
|
1582
|
+
function claudeFailure(failure) {
|
|
1583
|
+
const reason = failure.reason;
|
|
1584
|
+
switch (failure.kind) {
|
|
1585
|
+
case "rate_limited":
|
|
1586
|
+
return { kind: "rate_limited", reason, retryAtMs: failure.retryAt?.getTime() };
|
|
1587
|
+
case "budget_exceeded":
|
|
1588
|
+
return { kind: "limit_exceeded", reason, unit: "usd" };
|
|
1589
|
+
case "max_turns":
|
|
1590
|
+
return { kind: "runtime_error", reason: `native maxTurns reached: ${reason}` };
|
|
1591
|
+
case "schema_invalid":
|
|
1592
|
+
return { kind: "output_invalid", reason };
|
|
1593
|
+
case "inactivity_timeout":
|
|
1594
|
+
return { kind: "cancelled_unknown", reason };
|
|
1595
|
+
case "refusal":
|
|
1596
|
+
return { kind: "output_invalid", reason: `native refusal: ${reason}` };
|
|
1597
|
+
case "auth_misconfigured":
|
|
1598
|
+
return { kind: "auth_misconfigured", reason };
|
|
1599
|
+
case "aborted":
|
|
1600
|
+
return { kind: "aborted", reason };
|
|
1601
|
+
case "runtime_error":
|
|
1602
|
+
return { kind: "runtime_error", reason };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function claudeUsage(usage, executionId) {
|
|
1606
|
+
if (!usage) return [];
|
|
1607
|
+
const models = Object.entries(usage.modelUsage);
|
|
1608
|
+
const sum = (field) => {
|
|
1609
|
+
const values = models.map(([, model]) => model[field]);
|
|
1610
|
+
if (values.length === 0) return null;
|
|
1611
|
+
let total = 0;
|
|
1612
|
+
for (const value of values) {
|
|
1613
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
1614
|
+
total += value;
|
|
1615
|
+
}
|
|
1616
|
+
return total;
|
|
1617
|
+
};
|
|
1618
|
+
const uncached = sum("inputTokens");
|
|
1619
|
+
const cachedInput = sum("cacheReadInputTokens");
|
|
1620
|
+
const cacheWriteInput = sum("cacheCreationInputTokens");
|
|
1621
|
+
const input = uncached === null || cachedInput === null || cacheWriteInput === null ? null : uncached + cachedInput + cacheWriteInput;
|
|
1622
|
+
return [{
|
|
1623
|
+
kind: "snapshot",
|
|
1624
|
+
scope: "turn",
|
|
1625
|
+
scopeId: executionId,
|
|
1626
|
+
epoch: executionId,
|
|
1627
|
+
sequence: 0,
|
|
1628
|
+
model: models.length === 1 ? models[0][0] : null,
|
|
1629
|
+
source: "claude-sdk:query-result",
|
|
1630
|
+
tokens: { input, output: sum("outputTokens"), cachedInput, cacheWriteInput, reasoningOutput: null, total: null },
|
|
1631
|
+
cost: { usd: usage.totalCostUsd, kind: "estimate" }
|
|
1632
|
+
}];
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/claude-code-adapter.ts
|
|
1636
|
+
function createClaudeCodeAdapter(options) {
|
|
1637
|
+
for (const field of ["maxTurns", "maxBudgetUsd", "inactivityTimeoutMs"]) {
|
|
1638
|
+
const value = options[field];
|
|
1639
|
+
if (value === void 0 && field === "inactivityTimeoutMs") continue;
|
|
1640
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new TypeError(`${field} must be positive and finite`);
|
|
1641
|
+
}
|
|
1642
|
+
const descriptor = claudeCodeCapabilities();
|
|
1643
|
+
return {
|
|
1644
|
+
descriptor,
|
|
1645
|
+
async run(request) {
|
|
1646
|
+
const failure = preflightHarnessRun(request, descriptor);
|
|
1647
|
+
if (failure) return { ok: false, harness: "claude-code", failure, usage: [] };
|
|
1648
|
+
if (request.wallTimeMs > 2147483647) return { ok: false, harness: "claude-code", failure: { kind: "invalid_request", reason: "wallTimeMs exceeds the local timer range" }, usage: [] };
|
|
1649
|
+
return new ClaudeInvocation(request, options).run();
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
function claudeCodeCapabilities() {
|
|
1654
|
+
const capabilities = Object.fromEntries(EXECUTION_CAPABILITIES.map((capability) => [capability, {
|
|
1655
|
+
status: "unsupported",
|
|
1656
|
+
reason: "not exposed by this bounded Claude SDK adapter"
|
|
1657
|
+
}]));
|
|
1658
|
+
for (const capability of ["fresh_run", "resume", "structured_output", "external_cancellation", "token_reporting"]) {
|
|
1659
|
+
capabilities[capability] = { status: "supported", evidence: "bounded wrapper around the existing runAgent SDK query" };
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
harness: "claude-code",
|
|
1663
|
+
adapter: { name: "claude-sdk-bounded", version: "1" },
|
|
1664
|
+
capabilities,
|
|
1665
|
+
limits: [{ unit: "milliseconds", scope: "execution", enforcement: "hard", assessment: {
|
|
1666
|
+
status: "supported",
|
|
1667
|
+
evidence: "local deadline aborts and closes the owned query; native cancellation remains unconfirmed"
|
|
1668
|
+
} }]
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
var ClaudeInvocation = class {
|
|
1672
|
+
constructor(request, options) {
|
|
1673
|
+
this.request = request;
|
|
1674
|
+
this.options = options;
|
|
1675
|
+
this.conversation = request.target.kind === "resume" ? request.target.conversation : void 0;
|
|
1676
|
+
this.execution = { executionId: randomUUID3(), ...this.conversation ? { conversation: this.conversation } : {} };
|
|
1677
|
+
}
|
|
1678
|
+
request;
|
|
1679
|
+
options;
|
|
1680
|
+
controller = new AbortController();
|
|
1681
|
+
execution;
|
|
1682
|
+
conversation;
|
|
1683
|
+
stream;
|
|
1684
|
+
active = true;
|
|
1685
|
+
timer;
|
|
1686
|
+
stop;
|
|
1687
|
+
onAbort = () => this.stop({ kind: "aborted", reason: String(this.request.signal?.reason ?? "caller aborted") });
|
|
1688
|
+
async run() {
|
|
1689
|
+
try {
|
|
1690
|
+
const stopped = new Promise((resolve) => {
|
|
1691
|
+
this.stop = (failure) => {
|
|
1692
|
+
if (!this.active) return;
|
|
1693
|
+
this.active = false;
|
|
1694
|
+
this.controller.abort(new Error(failure.reason));
|
|
1695
|
+
this.close();
|
|
1696
|
+
resolve(this.failed(failure));
|
|
1697
|
+
};
|
|
1698
|
+
});
|
|
1699
|
+
this.timer = setTimeout(() => this.stop({ kind: "wall_time_exceeded", reason: "Claude query exceeded local wall deadline", wallTimeMs: this.request.wallTimeMs }), this.request.wallTimeMs);
|
|
1700
|
+
this.timer.unref?.();
|
|
1701
|
+
this.request.signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
1702
|
+
if (this.request.signal?.aborted) this.onAbort();
|
|
1703
|
+
this.emit({ kind: "execution_started", execution: this.execution });
|
|
1704
|
+
if (!this.active) return await stopped;
|
|
1705
|
+
const result = await Promise.race([this.invoke(), stopped]);
|
|
1706
|
+
this.active = false;
|
|
1707
|
+
this.emit({
|
|
1708
|
+
kind: "execution_finished",
|
|
1709
|
+
executionId: this.execution.executionId,
|
|
1710
|
+
outcome: result.ok ? "succeeded" : result.failure.kind === "aborted" || result.failure.kind === "wall_time_exceeded" || result.failure.kind === "cancelled_unknown" ? "cancelled" : "failed"
|
|
1711
|
+
});
|
|
1712
|
+
return result;
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
this.controller.abort(error);
|
|
1715
|
+
return this.failed({ kind: "runtime_error", reason: error instanceof Error ? error.message : String(error) });
|
|
1716
|
+
} finally {
|
|
1717
|
+
this.active = false;
|
|
1718
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1719
|
+
this.request.signal?.removeEventListener("abort", this.onAbort);
|
|
1720
|
+
this.close();
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
async invoke() {
|
|
1724
|
+
const result = await runAgent({
|
|
1725
|
+
...this.request.native,
|
|
1726
|
+
prompt: this.request.prompt,
|
|
1727
|
+
cwd: this.request.cwd,
|
|
1728
|
+
maxTurns: this.options.maxTurns,
|
|
1729
|
+
maxBudgetUsd: this.options.maxBudgetUsd,
|
|
1730
|
+
inactivityTimeoutMs: this.options.inactivityTimeoutMs,
|
|
1731
|
+
signal: this.controller.signal,
|
|
1732
|
+
resumeSessionId: this.request.target.kind === "resume" ? this.request.target.conversation.nativeId : void 0,
|
|
1733
|
+
onMessage: (message2) => this.observe(message2)
|
|
1734
|
+
}, { ...this.options.deps, query: (args) => {
|
|
1735
|
+
this.stream = (this.options.deps?.query ?? query2)(args);
|
|
1736
|
+
return this.stream;
|
|
1737
|
+
} });
|
|
1738
|
+
if (!this.active) return this.failed({ kind: "cancelled_unknown", reason: "query completed after local supervision ended" });
|
|
1739
|
+
if (result.sessionId) this.identify(result.sessionId);
|
|
1740
|
+
const usage = claudeUsage(result.usage, this.execution.executionId);
|
|
1741
|
+
if (!result.ok) return { ...this.failed(claudeFailure(result.failure)), usage };
|
|
1742
|
+
if (!this.conversation) return this.failed({ kind: "runtime_error", reason: "Claude query returned no conversation identity" });
|
|
1743
|
+
return {
|
|
1744
|
+
ok: true,
|
|
1745
|
+
harness: "claude-code",
|
|
1746
|
+
execution: { ...this.execution, conversation: this.conversation },
|
|
1747
|
+
conversation: this.conversation,
|
|
1748
|
+
output: this.request.native?.outputSchema ? { kind: "structured", value: result.output } : { kind: "text", text: String(result.output) },
|
|
1749
|
+
usage
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
observe(message2) {
|
|
1753
|
+
if (!this.active) return;
|
|
1754
|
+
if ("session_id" in message2 && typeof message2.session_id === "string") this.identify(message2.session_id);
|
|
1755
|
+
if (message2.type === "assistant") {
|
|
1756
|
+
for (const block of message2.message.content) {
|
|
1757
|
+
if (block.type === "text") this.emit({ kind: "assistant_output", executionId: this.execution.executionId, text: block.text });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
identify(nativeId) {
|
|
1762
|
+
if (this.conversation) {
|
|
1763
|
+
if (this.conversation.nativeId !== nativeId) throw new Error("Claude query changed conversation identity");
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
this.conversation = { harness: "claude-code", namespace: this.request.target.kind === "fresh" ? this.request.target.namespace : this.request.target.conversation.namespace, nativeId };
|
|
1767
|
+
this.emit({ kind: "conversation_identified", executionId: this.execution.executionId, conversation: this.conversation });
|
|
1768
|
+
}
|
|
1769
|
+
failed(failure) {
|
|
1770
|
+
return {
|
|
1771
|
+
ok: false,
|
|
1772
|
+
harness: "claude-code",
|
|
1773
|
+
execution: this.execution,
|
|
1774
|
+
...this.conversation ? { conversation: this.conversation } : {},
|
|
1775
|
+
failure,
|
|
1776
|
+
usage: []
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
emit(progress) {
|
|
1780
|
+
this.request.onProgress?.({ ...progress, harness: "claude-code", atMs: (this.options.deps?.now ?? Date.now)() });
|
|
1781
|
+
}
|
|
1782
|
+
close() {
|
|
1783
|
+
try {
|
|
1784
|
+
this.stream?.close();
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
348
1789
|
export {
|
|
349
1790
|
ANTI_NESTING_VARS,
|
|
350
1791
|
AuthMisconfiguredError,
|
|
351
1792
|
CLAUDE_CODE_PASSTHROUGH_VARS,
|
|
1793
|
+
CODEX_APPROVAL_POLICIES,
|
|
1794
|
+
CODEX_SANDBOXES,
|
|
1795
|
+
DEFAULT_CODEX_EXECUTABLE,
|
|
1796
|
+
DEFAULT_CODEX_KILL_GRACE_MS,
|
|
352
1797
|
DEFAULT_INACTIVITY_MS,
|
|
353
1798
|
DEFAULT_MAX_RETRIES,
|
|
1799
|
+
EXECUTION_CAPABILITIES,
|
|
354
1800
|
FORBIDDEN_API_KEY_SOURCES,
|
|
355
1801
|
STRIPPED_AUTH_VARS,
|
|
1802
|
+
STRIPPED_CODEX_AUTH_VARS,
|
|
356
1803
|
STRIPPED_PROXY_VARS,
|
|
1804
|
+
SUPPORTED_CODEX_EXEC_VERSION,
|
|
357
1805
|
assertApiKeySourceAllowed,
|
|
358
1806
|
assertAuthEnvOk,
|
|
1807
|
+
buildCodexExecArgs,
|
|
359
1808
|
classifyResult,
|
|
1809
|
+
claudeCodeCapabilities,
|
|
1810
|
+
codexExecCapabilities,
|
|
1811
|
+
createClaudeCodeAdapter,
|
|
1812
|
+
createCodexExecAdapter,
|
|
1813
|
+
createDurableHarnessDispatcher,
|
|
1814
|
+
dispatchHarnessRun,
|
|
1815
|
+
preflightHarnessRun,
|
|
1816
|
+
prepareCodexEnv,
|
|
360
1817
|
prepareEnv,
|
|
361
1818
|
runAgent,
|
|
362
1819
|
sumModelCost,
|