total-recall-brain 3.44.0 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +13 -8
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +6 -2
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +31 -11
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compiler.mjs +20 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +32 -4
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +65 -54
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +1 -0
- package/src/core/vector-field.mjs +134 -192
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/** Codex app-server session. Only Node built-ins may be shipped to a remote node.
|
|
2
|
+
* Wire contract: https://learn.chatgpt.com/docs/app-server.md
|
|
3
|
+
* Response types: openai/codex, app-server-protocol/schema/typescript (2026-11-09).
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
|
|
8
|
+
const controls = ['reply', 'interrupt', 'answer', 'fork', 'goal', 'limits', 'schema', 'review'];
|
|
9
|
+
const approvalMethods = new Set(['item/commandExecution/requestApproval', 'item/fileChange/requestApproval', 'item/permissions/requestApproval']);
|
|
10
|
+
const questionMethods = new Set(['tool/requestUserInput', 'item/tool/requestUserInput']);
|
|
11
|
+
const featureForMethod = { 'turn/steer': 'reply', 'turn/interrupt': 'interrupt', 'thread/fork': 'fork', 'thread/goal/set': 'goal' };
|
|
12
|
+
const textInput = text => [{ type: 'text', text }];
|
|
13
|
+
const errorText = error => error?.message || String(error);
|
|
14
|
+
|
|
15
|
+
function answerPayload(request, value) {
|
|
16
|
+
if (questionMethods.has(request.method)) {
|
|
17
|
+
const questions = request.params.questions || [];
|
|
18
|
+
let answers = value?.answers ?? value;
|
|
19
|
+
if (typeof answers === 'string' || Array.isArray(answers)) {
|
|
20
|
+
if (questions.length !== 1) throw new Error('Answer each question by its question ID.');
|
|
21
|
+
answers = { [questions[0].id]: answers };
|
|
22
|
+
}
|
|
23
|
+
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) throw new Error('Question answers must be an object.');
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
26
|
+
if (!questions.some(question => question.id === id)) throw new Error(`Unknown question ID: ${id}`);
|
|
27
|
+
const values = typeof answer === 'string' ? [answer] : Array.isArray(answer) ? answer : answer?.answers;
|
|
28
|
+
if (!Array.isArray(values) || values.some(entry => typeof entry !== 'string')) throw new Error(`Invalid answer for ${id}`);
|
|
29
|
+
result[id] = { answers: values };
|
|
30
|
+
}
|
|
31
|
+
return { answers: result };
|
|
32
|
+
}
|
|
33
|
+
if (request.method === 'item/permissions/requestApproval') {
|
|
34
|
+
const result = value === 'allow' || value === true
|
|
35
|
+
? { permissions: request.params.permissions || {}, scope: 'turn' }
|
|
36
|
+
: value === 'deny' || value === false ? { permissions: {}, scope: 'turn' } : value;
|
|
37
|
+
if (!result?.permissions || typeof result.permissions !== 'object' || Array.isArray(result.permissions) ||
|
|
38
|
+
(result.scope !== undefined && !['turn', 'session'].includes(result.scope))) throw new Error('Invalid permission grant.');
|
|
39
|
+
return { ...result, scope: result.scope || 'turn' };
|
|
40
|
+
}
|
|
41
|
+
let decision = value?.decision ?? value;
|
|
42
|
+
if (decision === 'allow' || decision === true) decision = 'accept';
|
|
43
|
+
if (decision === 'deny' || decision === false) decision = 'decline';
|
|
44
|
+
const amendment = request.method === 'item/commandExecution/requestApproval' && decision && typeof decision === 'object' &&
|
|
45
|
+
(decision.acceptWithExecpolicyAmendment || decision.applyNetworkPolicyAmendment);
|
|
46
|
+
if (!['accept', 'acceptForSession', 'decline', 'cancel'].includes(decision) && !amendment) throw new Error('Invalid approval decision.');
|
|
47
|
+
return { decision };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createCodexDriver({ harness, binary = 'codex', args = [], cwd, env = process.env, job, options = {}, onEvent = () => {}, onFrame = () => {}, spawnFn = spawn }) {
|
|
51
|
+
const capabilities = Object.fromEntries(controls.map(key => [key, 'unsupported']));
|
|
52
|
+
const pending = new Map(), requests = new Map(), turnWaiters = new Map(), completedTurns = new Set(), messages = new Map();
|
|
53
|
+
const usage = { inputTokens: 0, outputTokens: 0, costUsd: null };
|
|
54
|
+
let child, lines, started, sessionId = null, activeTurn = null, sequence = 0;
|
|
55
|
+
let closed = false, exited = false, settled = false, closing, completion = null, finishTimer;
|
|
56
|
+
let queued = 0, controlTail = Promise.resolve(), limitError = null, stderr = '', bytes = 0;
|
|
57
|
+
let resolveDone, resolveExit;
|
|
58
|
+
let done = new Promise(resolve => { resolveDone = resolve; });
|
|
59
|
+
const exit = new Promise(resolve => { resolveExit = resolve; });
|
|
60
|
+
const emit = (kind, fields = {}) => {
|
|
61
|
+
try { onEvent({ kind, text: '', ...fields }); } catch { /* Observers do not own the session. */ }
|
|
62
|
+
};
|
|
63
|
+
const response = () => {
|
|
64
|
+
const items = [...messages.values()];
|
|
65
|
+
const final = items.filter(item => item.phase === 'final_answer');
|
|
66
|
+
return (final.length ? final : items).map(item => item.text || '').join('\n');
|
|
67
|
+
};
|
|
68
|
+
function settle(result) {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
clearImmediate(finishTimer);
|
|
72
|
+
const value = { ok: false, response: response(), sessionId, usage: { ...usage }, structured: null, error: null, failureKind: null, ...result };
|
|
73
|
+
emit('finish', { text: value.ok ? 'Finished' : value.error, ...value });
|
|
74
|
+
resolveDone(value);
|
|
75
|
+
}
|
|
76
|
+
function rejectPending(error) {
|
|
77
|
+
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(error); }
|
|
78
|
+
for (const entry of turnWaiters.values()) { clearTimeout(entry.timer); entry.reject(error); }
|
|
79
|
+
pending.clear(); requests.clear(); turnWaiters.clear();
|
|
80
|
+
}
|
|
81
|
+
function fail(error, failureKind = 'protocol') {
|
|
82
|
+
if (limitError) failureKind = 'limit';
|
|
83
|
+
emit('error', { text: errorText(error), failureKind });
|
|
84
|
+
settle({ error: errorText(error), failureKind });
|
|
85
|
+
rejectPending(error);
|
|
86
|
+
void close();
|
|
87
|
+
}
|
|
88
|
+
function write(message) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
if (!child || exited || closed || child.stdin.destroyed) { reject(new Error('Codex session is closed.')); return; }
|
|
91
|
+
child.stdin.write(`${JSON.stringify(message)}\n`, error => error ? reject(error) : resolve());
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function rpc(method, params) {
|
|
95
|
+
const id = ++sequence;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => fail(new Error(`Codex request timed out: ${method}`)), 30_000);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
pending.set(id, { resolve, reject, timer, method });
|
|
100
|
+
write({ id, method, params }).catch(error => fail(error, 'transport'));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function finishWhenIdle() {
|
|
104
|
+
clearImmediate(finishTimer);
|
|
105
|
+
// Let controls submitted by event handlers, and responses in this JSONL batch, drain first.
|
|
106
|
+
finishTimer = setImmediate(() => {
|
|
107
|
+
if (!completion || activeTurn || queued || settled) return;
|
|
108
|
+
const turn = completion;
|
|
109
|
+
if (limitError) { settle({ error: limitError, failureKind: 'limit' }); return; }
|
|
110
|
+
if (turn.status !== 'completed') {
|
|
111
|
+
const info = turn.error?.codexErrorInfo;
|
|
112
|
+
settle({ error: turn.error?.message || `Codex turn ${turn.status}.`,
|
|
113
|
+
failureKind: turn.status === 'interrupted' ? 'cancelled' : info === 'usageLimitExceeded' ? 'quota' : 'agent' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let structured = null;
|
|
117
|
+
if (options.resultSchema !== undefined) {
|
|
118
|
+
try { structured = JSON.parse(response()); }
|
|
119
|
+
catch { settle({ error: 'Codex returned invalid JSON for the result schema.', failureKind: 'schema' }); return; }
|
|
120
|
+
}
|
|
121
|
+
settle({ ok: true, structured });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function control(command, action) {
|
|
125
|
+
queued += 1;
|
|
126
|
+
const task = controlTail.then(async () => {
|
|
127
|
+
if (!started) throw new Error('Start the Codex session first.');
|
|
128
|
+
await started;
|
|
129
|
+
if (closed || exited) throw new Error('Codex session is closed.');
|
|
130
|
+
const result = await action();
|
|
131
|
+
emit('ack', { text: `${command} acknowledged`, command });
|
|
132
|
+
return result;
|
|
133
|
+
}).finally(() => { queued -= 1; finishWhenIdle(); });
|
|
134
|
+
controlTail = task.catch(() => {});
|
|
135
|
+
return task;
|
|
136
|
+
}
|
|
137
|
+
async function startTurn(text) {
|
|
138
|
+
if (limitError) throw new Error(limitError);
|
|
139
|
+
completion = null;
|
|
140
|
+
messages.clear();
|
|
141
|
+
const result = await rpc('turn/start', { threadId: sessionId, input: textInput(text),
|
|
142
|
+
...(options.effort ? { effort: options.effort } : {}),
|
|
143
|
+
...(options.resultSchema !== undefined ? { outputSchema: options.resultSchema } : {}) });
|
|
144
|
+
if (!result?.turn?.id) throw new Error('Codex turn/start returned no turn ID.');
|
|
145
|
+
if (!completedTurns.has(result.turn.id)) activeTurn = result.turn.id;
|
|
146
|
+
}
|
|
147
|
+
async function interruptTurn() {
|
|
148
|
+
const turnId = activeTurn;
|
|
149
|
+
if (!turnId) return;
|
|
150
|
+
// The RPC reply only acknowledges cancellation. Wait for turn/completed before
|
|
151
|
+
// returning, so an immediate continuation starts a new turn rather than steering it.
|
|
152
|
+
const stopped = new Promise((resolve, reject) => {
|
|
153
|
+
const timer = setTimeout(() => fail(new Error('Codex interrupt did not complete.')), 30_000);
|
|
154
|
+
timer.unref?.();
|
|
155
|
+
turnWaiters.set(turnId, { resolve, reject, timer });
|
|
156
|
+
});
|
|
157
|
+
try { await Promise.all([stopped, rpc('turn/interrupt', { threadId: sessionId, turnId })]); }
|
|
158
|
+
finally { clearTimeout(turnWaiters.get(turnId)?.timer); turnWaiters.delete(turnId); }
|
|
159
|
+
}
|
|
160
|
+
function itemEvent(method, params) {
|
|
161
|
+
const item = params.item;
|
|
162
|
+
if (item?.type === 'agentMessage' || item?.type === 'exitedReviewMode') {
|
|
163
|
+
if (item.type === 'exitedReviewMode') item.text = item.review;
|
|
164
|
+
messages.set(item.id, { ...item });
|
|
165
|
+
if (method === 'item/completed') emit('message', { text: item.text || '', item, itemId: item.id, phase: item.phase });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (item) {
|
|
169
|
+
const text = item.command || item.text || item.review || item.query || item.tool || item.type;
|
|
170
|
+
emit(method === 'item/started' ? 'step-start' : 'step-end', { text, item, itemId: item.id });
|
|
171
|
+
if (item.type === 'collabToolCall') emit('subagent', { text: item.prompt || text, item });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (method === 'item/agentMessage/delta') {
|
|
175
|
+
const previous = messages.get(params.itemId) || { id: params.itemId, type: 'agentMessage', text: '' };
|
|
176
|
+
messages.set(params.itemId, { ...previous, text: previous.text + (params.delta || '') });
|
|
177
|
+
}
|
|
178
|
+
if (typeof params.delta === 'string') emit('message', { text: params.delta, delta: true, itemId: params.itemId, method });
|
|
179
|
+
}
|
|
180
|
+
function notification(method, params) {
|
|
181
|
+
// A fork also emits thread/started. Never replace this driver's thread with that fork.
|
|
182
|
+
if (params.threadId && params.threadId !== sessionId) return;
|
|
183
|
+
if (method.startsWith('item/')) { itemEvent(method, params); return; }
|
|
184
|
+
switch (method) {
|
|
185
|
+
case 'turn/started':
|
|
186
|
+
if (!completedTurns.has(params.turn.id)) activeTurn = params.turn.id;
|
|
187
|
+
break;
|
|
188
|
+
case 'turn/completed': {
|
|
189
|
+
const turn = params.turn;
|
|
190
|
+
if (completedTurns.has(turn.id)) return;
|
|
191
|
+
if (activeTurn && activeTurn !== turn.id) return;
|
|
192
|
+
for (const item of turn.items || []) if (item.type === 'agentMessage') messages.set(item.id, item);
|
|
193
|
+
completedTurns.add(turn.id); activeTurn = null; completion = turn;
|
|
194
|
+
const waiting = turnWaiters.get(turn.id);
|
|
195
|
+
if (waiting) { clearTimeout(waiting.timer); waiting.resolve(); turnWaiters.delete(turn.id); }
|
|
196
|
+
for (const [id, request] of requests) if (request.params.turnId === turn.id) requests.delete(id);
|
|
197
|
+
finishWhenIdle();
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'serverRequest/resolved': requests.delete(params.requestId); break;
|
|
201
|
+
case 'turn/plan/updated': emit('plan', { text: params.explanation || 'Plan updated', steps: params.plan || [] }); break;
|
|
202
|
+
case 'turn/diff/updated': emit('diff', { text: params.diff || '', summary: params.diff || '' }); break;
|
|
203
|
+
case 'model/rerouted': emit('reroute', { text: `${params.fromModel} → ${params.toModel}`, from: params.fromModel, to: params.toModel, reason: params.reason }); break;
|
|
204
|
+
case 'hook/started': case 'hook/completed':
|
|
205
|
+
emit('hook', { text: params.run?.eventName || '', name: params.run?.eventName, phase: method.split('/')[1], run: params.run }); break;
|
|
206
|
+
case 'thread/tokenUsage/updated': {
|
|
207
|
+
const total = params.tokenUsage?.total;
|
|
208
|
+
if (!total) return;
|
|
209
|
+
// These are cumulative thread snapshots. Summing them would count tokens twice.
|
|
210
|
+
usage.inputTokens = total.inputTokens ?? usage.inputTokens;
|
|
211
|
+
usage.outputTokens = total.outputTokens ?? usage.outputTokens;
|
|
212
|
+
emit('usage', { text: `${usage.inputTokens} in, ${usage.outputTokens} out tokens`, ...usage });
|
|
213
|
+
if (options.maxTokens !== undefined && !limitError && (total.totalTokens ?? usage.inputTokens + usage.outputTokens) >= options.maxTokens) {
|
|
214
|
+
limitError = `Codex token limit reached (${options.maxTokens}).`;
|
|
215
|
+
void control('interrupt', async () => {
|
|
216
|
+
if (activeTurn) await interruptTurn();
|
|
217
|
+
else settle({ error: limitError, failureKind: 'limit' });
|
|
218
|
+
}).catch(error => fail(error, 'limit'));
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'error': emit('error', { text: params.error?.message || 'Codex error', error: params.error, willRetry: params.willRetry }); break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function frame(message) {
|
|
226
|
+
onFrame(message);
|
|
227
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) throw new Error('Invalid Codex JSON-RPC frame.');
|
|
228
|
+
if (message.method && Object.hasOwn(message, 'id')) {
|
|
229
|
+
if (approvalMethods.has(message.method) || questionMethods.has(message.method)) {
|
|
230
|
+
requests.set(message.id, message);
|
|
231
|
+
emit(questionMethods.has(message.method) ? 'question' : 'approval', {
|
|
232
|
+
requestId: message.id, request: message.params, method: message.method,
|
|
233
|
+
...(questionMethods.has(message.method) ? { questions: message.params.questions || [] } : {}) });
|
|
234
|
+
} else {
|
|
235
|
+
void write({ id: message.id, error: { code: -32601, message: `Unsupported client method: ${message.method}` } }).catch(error => fail(error, 'transport'));
|
|
236
|
+
}
|
|
237
|
+
} else if (Object.hasOwn(message, 'id')) {
|
|
238
|
+
const entry = pending.get(message.id);
|
|
239
|
+
if (!entry) return;
|
|
240
|
+
pending.delete(message.id); clearTimeout(entry.timer);
|
|
241
|
+
if (message.error) {
|
|
242
|
+
const feature = featureForMethod[entry.method];
|
|
243
|
+
if (feature && message.error.code === -32601) capabilities[feature] = 'unsupported';
|
|
244
|
+
entry.reject(Object.assign(new Error(message.error.message || 'Codex request failed.'), message.error));
|
|
245
|
+
} else if (Object.hasOwn(message, 'result')) entry.resolve(message.result);
|
|
246
|
+
else entry.reject(new Error('Codex response has no result or error.'));
|
|
247
|
+
} else if (typeof message.method === 'string') notification(message.method, message.params || {});
|
|
248
|
+
else throw new Error('Invalid Codex JSON-RPC frame.');
|
|
249
|
+
}
|
|
250
|
+
async function start(brief) {
|
|
251
|
+
if (started) throw new Error('Codex session has already started.');
|
|
252
|
+
if (closed) throw new Error('Codex session is closed.');
|
|
253
|
+
started = (async () => {
|
|
254
|
+
if (typeof brief !== 'string' || !brief.trim()) throw new Error('A non-empty brief is required.');
|
|
255
|
+
if (options.maxTokens !== undefined && (!Number.isSafeInteger(options.maxTokens) || options.maxTokens < 1)) throw new Error('maxTokens must be a positive integer.');
|
|
256
|
+
try {
|
|
257
|
+
child = spawnFn(binary, [...args, 'app-server', '--listen', 'stdio://'], {
|
|
258
|
+
cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
259
|
+
} catch (error) { error.failureKind = 'spawn'; throw error; }
|
|
260
|
+
child.on('error', error => { exited = true; resolveExit(); fail(error, 'spawn'); });
|
|
261
|
+
child.on('close', (code, signal) => {
|
|
262
|
+
exited = true; resolveExit(); lines?.close();
|
|
263
|
+
rejectPending(Object.assign(new Error(`Codex app-server closed (${signal || code}).${stderr ? ` ${stderr}` : ''}`), { failureKind: 'process' }));
|
|
264
|
+
if (!closed && !settled) {
|
|
265
|
+
// stdout has drained at close; allow a preceding turn/completed to settle first.
|
|
266
|
+
finishWhenIdle();
|
|
267
|
+
setImmediate(() => { if (!settled) fail(new Error(`Codex app-server exited before turn completion (${signal || code}).${stderr ? ` ${stderr}` : ''}`), 'process'); });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
child.stdin.on('error', error => { if (!closed) fail(error, 'transport'); });
|
|
271
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk.toString()).slice(-4096); });
|
|
272
|
+
child.stdout.on('data', chunk => {
|
|
273
|
+
bytes += chunk.length;
|
|
274
|
+
if (bytes > 32 * 1024 * 1024) fail(new Error('Codex transport output limit reached.'), 'limit');
|
|
275
|
+
});
|
|
276
|
+
lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
277
|
+
lines.on('line', line => {
|
|
278
|
+
if (!line.trim() || closed) return;
|
|
279
|
+
try { frame(JSON.parse(line)); } catch (error) { fail(error); }
|
|
280
|
+
});
|
|
281
|
+
const initialized = await rpc('initialize', {
|
|
282
|
+
clientInfo: { name: 'metaharness', title: 'Metaharness', version: '1.0.0' },
|
|
283
|
+
capabilities: { experimentalApi: true } });
|
|
284
|
+
if (!initialized || typeof initialized.userAgent !== 'string') throw new Error('Codex initialize returned no server identity.');
|
|
285
|
+
await write({ method: 'initialized', params: {} });
|
|
286
|
+
// InitializeResponse reports identity/platform, not a per-method feature list. Use the
|
|
287
|
+
// documented app-server surface after this handshake, never a version-string test.
|
|
288
|
+
Object.assign(capabilities, Object.fromEntries(controls.map(key => [key, key === 'limits' ? 'metaharness' : 'native'])));
|
|
289
|
+
const result = await rpc(options.forkSession ? 'thread/fork' : options.sessionId ? 'thread/resume' : 'thread/start', {
|
|
290
|
+
...(options.sessionId ? { threadId: options.sessionId, excludeTurns: true } : {}),
|
|
291
|
+
...(options.forkSession && options.atTurn ? { lastTurnId: options.atTurn } : {}),
|
|
292
|
+
cwd, sandbox: 'danger-full-access', approvalPolicy: options.approvalPolicy === 'operator' || options.approvalPolicy === 'rules'
|
|
293
|
+
? 'on-request' : options.approvalPolicy === 'auto' || !options.approvalPolicy ? 'never' : options.approvalPolicy });
|
|
294
|
+
// The resumable handle is thread.id; thread.sessionId can name a fork's ancestor.
|
|
295
|
+
sessionId = result?.thread?.id;
|
|
296
|
+
if (!sessionId) throw new Error('Codex returned no thread ID.');
|
|
297
|
+
await rpc('thread/name/set', { threadId: sessionId, name: options.name || job?.name || job?.id || (typeof job === 'string' ? job : 'Metaharness job') });
|
|
298
|
+
if (options.goal) await rpc('thread/goal/set', { threadId: sessionId, objective: options.goal });
|
|
299
|
+
emit('session', { text: `Codex session ${sessionId}`, sessionId, session: sessionId, rootSessionId: result.thread.sessionId,
|
|
300
|
+
capabilities: { ...capabilities }, server: initialized, harness });
|
|
301
|
+
if (options.reviewTarget) {
|
|
302
|
+
const review = await rpc('review/start', { threadId: sessionId, delivery: 'inline', target: options.reviewTarget });
|
|
303
|
+
if (!review?.turn?.id) throw new Error('Codex review/start returned no turn ID.');
|
|
304
|
+
if (!completedTurns.has(review.turn.id)) activeTurn = review.turn.id;
|
|
305
|
+
} else await startTurn(brief);
|
|
306
|
+
})();
|
|
307
|
+
try { await started; }
|
|
308
|
+
catch (error) { fail(error, error.failureKind || 'protocol'); throw error; }
|
|
309
|
+
}
|
|
310
|
+
function send(text) {
|
|
311
|
+
return control('send', async () => {
|
|
312
|
+
if (typeof text !== 'string' || !text.trim()) throw new Error('A non-empty message is required.');
|
|
313
|
+
if (limitError) throw new Error(limitError);
|
|
314
|
+
if (activeTurn) {
|
|
315
|
+
const turnId = activeTurn;
|
|
316
|
+
try { await rpc('turn/steer', { threadId: sessionId, expectedTurnId: turnId, input: textInput(text) }); }
|
|
317
|
+
catch (error) {
|
|
318
|
+
// The turn can finish between selecting steer and the server handling it.
|
|
319
|
+
if (error.code === -32600 && !activeTurn && /no active turn|not active|turn.*mismatch/i.test(error.message)) await startTurn(text);
|
|
320
|
+
else throw error;
|
|
321
|
+
}
|
|
322
|
+
} else await startTurn(text);
|
|
323
|
+
return { ack: true, via: 'native' };
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function interrupt() {
|
|
327
|
+
return control('interrupt', async () => {
|
|
328
|
+
await interruptTurn();
|
|
329
|
+
return { ack: true, via: 'native' };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async function answer(requestId, value) {
|
|
333
|
+
if (closed || exited) throw new Error('Codex session is closed.');
|
|
334
|
+
const id = requests.has(requestId) ? requestId : [...requests.keys()].find(key => String(key) === String(requestId));
|
|
335
|
+
const request = requests.get(id);
|
|
336
|
+
if (!request) throw new Error(`No pending Codex request: ${requestId}`);
|
|
337
|
+
const result = answerPayload(request, value);
|
|
338
|
+
// Reserve the request before yielding: two concurrent operator replies must not
|
|
339
|
+
// write two JSON-RPC responses for the same server request.
|
|
340
|
+
requests.delete(id);
|
|
341
|
+
queued += 1;
|
|
342
|
+
try {
|
|
343
|
+
await write({ id: request.id, result });
|
|
344
|
+
emit('ack', { text: 'answer acknowledged', command: 'answer', requestId: request.id });
|
|
345
|
+
return { ack: true, via: 'native' };
|
|
346
|
+
} finally { queued -= 1; finishWhenIdle(); }
|
|
347
|
+
}
|
|
348
|
+
function fork({ atTurn } = {}) {
|
|
349
|
+
return control('fork', async () => {
|
|
350
|
+
const result = await rpc('thread/fork', { threadId: sessionId, excludeTurns: true, ...(atTurn ? { lastTurnId: atTurn } : {}) });
|
|
351
|
+
if (!result?.thread?.id) throw new Error('Codex fork returned no thread ID.');
|
|
352
|
+
return { sessionId: result.thread.id };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function close() {
|
|
356
|
+
if (closing) return closing;
|
|
357
|
+
closed = true;
|
|
358
|
+
clearImmediate(finishTimer);
|
|
359
|
+
rejectPending(new Error('Codex session is closed.'));
|
|
360
|
+
if (!settled) settle({ error: limitError || 'Codex session closed.', failureKind: limitError ? 'limit' : 'cancelled' });
|
|
361
|
+
closing = (async () => {
|
|
362
|
+
if (!child) return;
|
|
363
|
+
const kill = signal => {
|
|
364
|
+
try { if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal); else child.kill(signal); }
|
|
365
|
+
catch { try { child.kill(signal); } catch { /* Already stopped. */ } }
|
|
366
|
+
};
|
|
367
|
+
if (!exited) child.stdin.end();
|
|
368
|
+
kill('SIGTERM');
|
|
369
|
+
const timer = setTimeout(() => kill('SIGKILL'), 500);
|
|
370
|
+
await exit;
|
|
371
|
+
// A server can exit while a descendant ignores TERM and has closed its pipes.
|
|
372
|
+
// Stop the owned group as well before releasing the session.
|
|
373
|
+
kill('SIGKILL'); clearTimeout(timer); lines?.close();
|
|
374
|
+
})();
|
|
375
|
+
return closing;
|
|
376
|
+
}
|
|
377
|
+
return { capabilities, start, send, interrupt, answer, fork, close, get done() { return done; },
|
|
378
|
+
setGoal: objective => control('goal', () => rpc('thread/goal/set', { threadId: sessionId, objective })),
|
|
379
|
+
async next(text, { resultSchema } = {}) {
|
|
380
|
+
if (!settled || closed || exited) throw new Error('The session is not ready for another turn.');
|
|
381
|
+
settled = false;
|
|
382
|
+
options.resultSchema = resultSchema;
|
|
383
|
+
done = new Promise(resolve => { resolveDone = resolve; });
|
|
384
|
+
await startTurn(text);
|
|
385
|
+
return done;
|
|
386
|
+
} };
|
|
387
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Portable JSON Schema validation. Unsupported assertions fail closed. */
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
export function validateSchema(schema, value, root = schema, at = '$') {
|
|
4
|
+
if (schema === true) return [];
|
|
5
|
+
if (schema === false) return [`${at}: forbidden value`];
|
|
6
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return [`${at}: invalid schema`];
|
|
7
|
+
const annotations = ['$schema', '$id', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'format', 'readOnly', 'writeOnly', '$comment'];
|
|
8
|
+
const supported = ['$ref', 'type', 'enum', 'const', 'required', 'properties', 'additionalProperties', 'items', 'prefixItems', 'minItems', 'maxItems', 'uniqueItems', 'minLength', 'maxLength', 'pattern', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'allOf', 'anyOf', 'oneOf', 'not', 'minProperties', 'maxProperties'];
|
|
9
|
+
const errors = Object.keys(schema).filter(key => !supported.includes(key) && !annotations.includes(key)).map(key => `${at}: unsupported schema keyword ${key}`);
|
|
10
|
+
if (schema.$ref) {
|
|
11
|
+
if (!schema.$ref.startsWith('#/')) return [...errors, `${at}: only local schema references are supported`];
|
|
12
|
+
const ref = schema.$ref.slice(2).split('/').reduce((s, key) => s?.[key.replaceAll('~1', '/').replaceAll('~0', '~')], root);
|
|
13
|
+
if (!ref || ref === schema) return [...errors, `${at}: invalid or recursive schema reference`];
|
|
14
|
+
errors.push(...validateSchema(ref, value, root, at));
|
|
15
|
+
}
|
|
16
|
+
const equal = isDeepStrictEqual;
|
|
17
|
+
const type = name => name === 'null' ? value === null : name === 'array' ? Array.isArray(value)
|
|
18
|
+
: name === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
19
|
+
: name === 'integer' ? Number.isInteger(value) : name === 'number' ? typeof value === 'number' && Number.isFinite(value) : typeof value === name;
|
|
20
|
+
if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(type)) errors.push(`${at}: expected ${schema.type}`);
|
|
21
|
+
if (schema.enum && !schema.enum.some(v => equal(v, value))) errors.push(`${at}: value is outside enum`);
|
|
22
|
+
if (Object.hasOwn(schema, 'const') && !equal(schema.const, value)) errors.push(`${at}: expected const`);
|
|
23
|
+
for (const key of ['allOf', 'anyOf', 'oneOf']) if (schema[key]) {
|
|
24
|
+
const matches = schema[key].filter(s => !validateSchema(s, value, root, at).length).length;
|
|
25
|
+
if (key === 'allOf' ? matches !== schema[key].length : key === 'oneOf' ? matches !== 1 : matches === 0) errors.push(`${at}: ${key} failed`);
|
|
26
|
+
}
|
|
27
|
+
if (schema.not && !validateSchema(schema.not, value, root, at).length) errors.push(`${at}: not failed`);
|
|
28
|
+
if (type('object')) {
|
|
29
|
+
for (const key of schema.required || []) if (!Object.hasOwn(value, key)) errors.push(`${at}.${key}: required`);
|
|
30
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
31
|
+
const child = schema.properties?.[key] ?? schema.additionalProperties ?? true;
|
|
32
|
+
errors.push(...validateSchema(child, entry, root, `${at}.${key}`));
|
|
33
|
+
}
|
|
34
|
+
if (schema.minProperties !== undefined && Object.keys(value).length < schema.minProperties) errors.push(`${at}: too few properties`);
|
|
35
|
+
if (schema.maxProperties !== undefined && Object.keys(value).length > schema.maxProperties) errors.push(`${at}: too many properties`);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
value.forEach((entry, i) => errors.push(...validateSchema(schema.prefixItems?.[i] ?? schema.items ?? true, entry, root, `${at}[${i}]`)));
|
|
39
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${at}: too few items`);
|
|
40
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) errors.push(`${at}: too many items`);
|
|
41
|
+
if (schema.uniqueItems && value.some((entry, i) => value.slice(0, i).some(previous => equal(entry, previous)))) errors.push(`${at}: duplicate items`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
if (schema.minLength !== undefined && [...value].length < schema.minLength) errors.push(`${at}: text too short`);
|
|
45
|
+
if (schema.maxLength !== undefined && [...value].length > schema.maxLength) errors.push(`${at}: text too long`);
|
|
46
|
+
if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) errors.push(`${at}: pattern failed`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'number') {
|
|
49
|
+
for (const [key, failed] of [['minimum', value < schema.minimum], ['maximum', value > schema.maximum],
|
|
50
|
+
['exclusiveMinimum', value <= schema.exclusiveMinimum], ['exclusiveMaximum', value >= schema.exclusiveMaximum],
|
|
51
|
+
['multipleOf', schema.multipleOf !== undefined && Math.abs(value / schema.multipleOf - Math.round(value / schema.multipleOf)) > 1e-9]]) {
|
|
52
|
+
if (failed) errors.push(`${at}: ${key} failed`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return errors;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const GOAL_SCHEMA = { type: 'object', required: ['met', 'evidence', 'remaining'], additionalProperties: false,
|
|
59
|
+
properties: { met: { type: 'boolean' }, evidence: { type: 'array', items: { type: 'string' } }, remaining: { type: 'array', items: { type: 'string' } } } };
|
|
60
|
+
|
|
61
|
+
export function readResult(result, schema) {
|
|
62
|
+
let value = result.structured;
|
|
63
|
+
if (value === undefined || value === null) {
|
|
64
|
+
try { value = JSON.parse(result.response); } catch { return { errors: ['The final message must be JSON.'] }; }
|
|
65
|
+
}
|
|
66
|
+
return { value, errors: validateSchema(schema, value) };
|
|
67
|
+
}
|