@anhlp/alp-runtime 0.0.0-beta.1
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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/acp-transport.d.ts +114 -0
- package/dist/acp-transport.js +478 -0
- package/dist/antigravity-transport.d.ts +80 -0
- package/dist/antigravity-transport.js +609 -0
- package/dist/board-domain.d.ts +56 -0
- package/dist/board-domain.js +157 -0
- package/dist/board.d.ts +21 -0
- package/dist/board.js +69 -0
- package/dist/catalog.d.ts +31 -0
- package/dist/catalog.js +38 -0
- package/dist/claude-transport.d.ts +157 -0
- package/dist/claude-transport.js +671 -0
- package/dist/contracts.d.ts +163 -0
- package/dist/contracts.js +5 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.js +0 -0
- package/dist/github.d.ts +10 -0
- package/dist/github.js +39 -0
- package/dist/handoff.d.ts +36 -0
- package/dist/handoff.js +73 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +64 -0
- package/dist/language.d.ts +41 -0
- package/dist/language.js +68 -0
- package/dist/live.d.ts +48 -0
- package/dist/live.js +45 -0
- package/dist/mailbox.d.ts +24 -0
- package/dist/mailbox.js +36 -0
- package/dist/mcp-tools.d.ts +115 -0
- package/dist/mcp-tools.js +71 -0
- package/dist/permission-path.d.ts +2 -0
- package/dist/permission-path.js +44 -0
- package/dist/process-info.d.ts +14 -0
- package/dist/process-info.js +32 -0
- package/dist/providers.d.ts +28 -0
- package/dist/providers.js +71 -0
- package/dist/recall.d.ts +42 -0
- package/dist/recall.js +60 -0
- package/dist/resolve.d.ts +121 -0
- package/dist/resolve.js +238 -0
- package/dist/runtime-context.d.ts +43 -0
- package/dist/runtime-context.js +61 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +2979 -0
- package/dist/session-config.d.ts +1973 -0
- package/dist/session-config.js +205 -0
- package/dist/session-state.d.ts +140 -0
- package/dist/session-state.js +0 -0
- package/dist/tool-results.d.ts +12 -0
- package/dist/tool-results.js +25 -0
- package/dist/tools.d.ts +674 -0
- package/dist/tools.js +375 -0
- package/dist/transport.d.ts +47 -0
- package/dist/transport.js +169 -0
- package/dist/usage.d.ts +15 -0
- package/dist/usage.js +35 -0
- package/dist/workspace.d.ts +101 -0
- package/dist/workspace.js +259 -0
- package/dist/worktree-domain.d.ts +88 -0
- package/dist/worktree-domain.js +196 -0
- package/package.json +256 -0
- package/src/acp-transport.ts +473 -0
- package/src/antigravity-transport.ts +658 -0
- package/src/board-domain.ts +185 -0
- package/src/board.ts +80 -0
- package/src/catalog.ts +32 -0
- package/src/claude-transport.ts +771 -0
- package/src/contracts.ts +166 -0
- package/src/events.ts +1 -0
- package/src/github.ts +37 -0
- package/src/handoff.ts +86 -0
- package/src/index.ts +26 -0
- package/src/language.ts +102 -0
- package/src/live.ts +86 -0
- package/src/mailbox.ts +46 -0
- package/src/mcp-tools.ts +73 -0
- package/src/permission-path.ts +37 -0
- package/src/process-info.ts +40 -0
- package/src/providers.ts +84 -0
- package/src/recall.ts +85 -0
- package/src/resolve.ts +230 -0
- package/src/runtime-context.ts +49 -0
- package/src/runtime.ts +3579 -0
- package/src/session-config.ts +301 -0
- package/src/session-state.ts +141 -0
- package/src/tool-results.ts +26 -0
- package/src/tools.ts +379 -0
- package/src/transport.ts +116 -0
- package/src/usage.ts +42 -0
- package/src/workspace.ts +318 -0
- package/src/worktree-domain.ts +216 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { moveFile } from '@anhlp/alp-core/atomic';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { BOARD_KEEP, live, normalizePaths, overlapping, PIN_BODY_CHARS, PIN_KINDS, renderPin, type Pin, type PinKind } from './board.js';
|
|
6
|
+
import { plainObject, toolResult } from './tool-results.js';
|
|
7
|
+
import type { RuntimeOptions } from './contracts.js';
|
|
8
|
+
import type { Session } from './session-state.js';
|
|
9
|
+
import type { AlpEvent } from './events.js';
|
|
10
|
+
import type { MailEvent } from './mailbox.js';
|
|
11
|
+
|
|
12
|
+
type BoardContext = {
|
|
13
|
+
options: RuntimeOptions;
|
|
14
|
+
sessions: Map<string, Session>;
|
|
15
|
+
boards: Map<string, Promise<Pin[]>>;
|
|
16
|
+
loadedBoards: Map<string, Pin[]>;
|
|
17
|
+
rootOf(sessionId: string): string;
|
|
18
|
+
lineage(sessionId: string): string[];
|
|
19
|
+
runLog(rootId: string, entry: Record<string, unknown>): void;
|
|
20
|
+
emit(sessionId: string, event: AlpEvent): void;
|
|
21
|
+
post(sessionId: string, event: Omit<MailEvent, 'id'>): MailEvent;
|
|
22
|
+
steerMail(sessionId: string, session: Session): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Shared project boards retain their original write ordering and session ownership. */
|
|
26
|
+
export function createBoardDomain({ options, sessions, boards, loadedBoards, rootOf, lineage, runLog, emit, post, steerMail }: BoardContext) {
|
|
27
|
+
const boardFile = (project: string) => path.join(options.boardDir!, `${createHash('sha256').update(project).digest('hex').slice(0, 16)}.jsonl`);
|
|
28
|
+
let boardWrites = Promise.resolve();
|
|
29
|
+
|
|
30
|
+
/** Live claims, then the most recent decisions and findings. */
|
|
31
|
+
function prune(pins: Pin[]) {
|
|
32
|
+
const notes = pins.filter(pin => pin.kind !== 'claim' && live(pin));
|
|
33
|
+
return [...pins.filter(pin => pin.kind === 'claim' && live(pin)), ...notes.slice(-BOARD_KEEP)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A board from an earlier daemon keeps its decisions and findings; its claims ended with their sessions. */
|
|
37
|
+
function boardOf(project: string) {
|
|
38
|
+
let board = boards.get(project);
|
|
39
|
+
if (!board) {
|
|
40
|
+
board = (async () => {
|
|
41
|
+
if (!options.boardDir) return [];
|
|
42
|
+
const text = await readFile(boardFile(project), 'utf8').catch(() => '');
|
|
43
|
+
const pins = new Map<string, Pin>();
|
|
44
|
+
for (const line of text.split('\n').filter(Boolean)) {
|
|
45
|
+
try {
|
|
46
|
+
const entry = JSON.parse(line);
|
|
47
|
+
if (entry.pin?.id) pins.set(entry.pin.id, entry.pin);
|
|
48
|
+
else if (entry.release && pins.has(entry.release)) pins.get(entry.release)!.released = entry.at;
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
for (const pin of pins.values()) if (pin.kind === 'claim' && !pin.released) pin.released = now;
|
|
53
|
+
const kept = prune([...pins.values()]);
|
|
54
|
+
// Compact the file to what is kept.
|
|
55
|
+
boardWrites = boardWrites.then(async () => {
|
|
56
|
+
await mkdir(options.boardDir!, { recursive: true });
|
|
57
|
+
const temporary = `${boardFile(project)}.${randomUUID().slice(0, 8)}.tmp`;
|
|
58
|
+
await writeFile(temporary, kept.map(pin => JSON.stringify({ pin })).join('\n') + (kept.length ? '\n' : ''));
|
|
59
|
+
await moveFile(temporary, boardFile(project));
|
|
60
|
+
}).catch(() => {});
|
|
61
|
+
loadedBoards.set(project, kept);
|
|
62
|
+
return kept;
|
|
63
|
+
})();
|
|
64
|
+
boards.set(project, board);
|
|
65
|
+
}
|
|
66
|
+
return board;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveBoard(project: string, entry: object) {
|
|
70
|
+
if (!options.boardDir) return;
|
|
71
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
72
|
+
boardWrites = boardWrites.then(() => mkdir(options.boardDir!, { recursive: true })).then(() => appendFile(boardFile(project), line)).catch(() => {});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether two sessions are in one line of delegation, where a claim is shared. */
|
|
76
|
+
const related = (a: string, b: string) => lineage(a).includes(b) || lineage(b).includes(a);
|
|
77
|
+
|
|
78
|
+
/** Live claims of agents outside this session's line of delegation that overlap `paths`. */
|
|
79
|
+
function claimConflicts(sessionId: string, pins: Pin[], paths: string[]) {
|
|
80
|
+
return pins
|
|
81
|
+
.filter(pin => pin.kind === 'claim' && live(pin) && pin.sessionId !== sessionId && !related(sessionId, pin.sessionId) && overlapping(paths, pin.paths ?? []).length)
|
|
82
|
+
.map(pin => ({ pinId: pin.id, agent: pin.agent, sessionId: pin.sessionId, paths: overlapping(pin.paths ?? [], paths), body: pin.body, at: pin.at, ...(pin.task ? { task: pin.task } : {}) }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The task a session works on: its own assignment's, or the nearest requester's. */
|
|
86
|
+
function taskOf(sessionId: string) {
|
|
87
|
+
for (const id of lineage(sessionId)) {
|
|
88
|
+
const parent = sessions.get(id)?.parent;
|
|
89
|
+
const taskId = parent ? sessions.get(parent)?.assignments.get(id)?.taskId : undefined;
|
|
90
|
+
if (taskId) return taskId;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Pins to the project board `pins` (from boardOf) and tells agents at work on the project. */
|
|
96
|
+
function addPin(sessionId: string, session: Session, pins: Pin[], fields: Pick<Pin, 'kind' | 'body' | 'paths' | 'task'>) {
|
|
97
|
+
const project = session.mapping.agent.projectRoot;
|
|
98
|
+
const pin: Pin = {
|
|
99
|
+
id: `p-${randomUUID().slice(0, 8)}`, project, kind: fields.kind, body: fields.body, ...(fields.paths ? { paths: fields.paths } : {}), ...(fields.task ? { task: fields.task } : {}),
|
|
100
|
+
agent: session.mapping.agent.name, sessionId, rootId: rootOf(sessionId), at: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
const kept = prune([...pins, pin]);
|
|
103
|
+
pins.splice(0, pins.length, ...kept);
|
|
104
|
+
saveBoard(project, { pin });
|
|
105
|
+
runLog(pin.rootId, { event: 'board.pin', pinId: pin.id, kind: pin.kind, agent: pin.agent, body: pin.body, ...(pin.paths ? { paths: pin.paths } : {}), ...(pin.task ? { task: pin.task } : {}) });
|
|
106
|
+
emit(sessionId, { type: 'pin', pin });
|
|
107
|
+
// Agents at work on the project read it in their running turn; idle ones see it on alp_board or their next assignment.
|
|
108
|
+
for (const [id, other] of sessions) {
|
|
109
|
+
if (id === sessionId || other.closed || !other.active || other.mapping.agent.projectRoot !== project) continue;
|
|
110
|
+
post(id, { kind: 'board', from: pin.agent, assignment: pin.id, body: renderPin(pin), passive: true });
|
|
111
|
+
if (!other.pending) void steerMail(id, other);
|
|
112
|
+
}
|
|
113
|
+
return pin;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function pinTool(sessionId: string, session: Session, args: unknown) {
|
|
117
|
+
if (!plainObject(args, ['kind', 'body', 'paths']) || !PIN_KINDS.includes(args.kind) || typeof args.body !== 'string' || !args.body.trim() || args.body.length > PIN_BODY_CHARS) {
|
|
118
|
+
return toolResult(false, { error: `A pin needs kind (${PIN_KINDS.join(', ')}) and a body of at most ${PIN_BODY_CHARS} characters` });
|
|
119
|
+
}
|
|
120
|
+
// A claim reserves files to change; a read-only session changes none.
|
|
121
|
+
if (args.kind === 'claim' && session.mapping.mode === 'read-only') return toolResult(false, { error: 'A read-only session changes no files and cannot claim paths; pin a decision or finding instead' });
|
|
122
|
+
const project = session.mapping.agent.projectRoot;
|
|
123
|
+
let paths: string[] | undefined;
|
|
124
|
+
if (args.paths !== undefined || args.kind === 'claim') {
|
|
125
|
+
const normalized = normalizePaths(args.paths, project, session.spec.writeRoot ? [session.spec.writeRoot] : undefined);
|
|
126
|
+
if (typeof normalized === 'string') return toolResult(false, { error: normalized });
|
|
127
|
+
paths = normalized;
|
|
128
|
+
}
|
|
129
|
+
const pins = await boardOf(project);
|
|
130
|
+
if (session.closed) return toolResult(false, { error: 'Session closed' });
|
|
131
|
+
if (args.kind === 'claim') {
|
|
132
|
+
const conflicts = claimConflicts(sessionId, pins, paths!);
|
|
133
|
+
if (conflicts.length) {
|
|
134
|
+
return toolResult(false, {
|
|
135
|
+
error: 'Another agent has claimed overlapping paths; do not edit them. Ask your requester, or claim other paths.',
|
|
136
|
+
conflicts,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const task = args.kind === 'claim' ? taskOf(sessionId) : undefined;
|
|
141
|
+
const pin = addPin(sessionId, session, pins, { kind: args.kind as PinKind, body: args.body, ...(paths ? { paths } : {}), ...(task ? { task } : {}) });
|
|
142
|
+
return toolResult(true, { pinned: pin.id, kind: pin.kind, ...(paths ? { paths } : {}), ...(task ? { task } : {}) });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function boardTool(session: Session, args: unknown) {
|
|
146
|
+
if (!plainObject(args, ['kinds', 'limit']) || (args.kinds !== undefined && (!Array.isArray(args.kinds) || !args.kinds.every((kind: unknown) => PIN_KINDS.includes(kind as PinKind)))) || (args.limit !== undefined && (!Number.isSafeInteger(args.limit) || args.limit < 1))) {
|
|
147
|
+
return toolResult(false, { error: 'kinds lists claim, decision or finding; limit is a positive integer' });
|
|
148
|
+
}
|
|
149
|
+
const kinds: string[] = args.kinds ?? [...PIN_KINDS];
|
|
150
|
+
const pins = (await boardOf(session.mapping.agent.projectRoot)).filter(pin => live(pin) && kinds.includes(pin.kind));
|
|
151
|
+
const claims = pins.filter(pin => pin.kind === 'claim');
|
|
152
|
+
const notes = pins.filter(pin => pin.kind !== 'claim').slice(-(args.limit ?? 30));
|
|
153
|
+
const view = ({ project: _project, rootId: _root, released: _released, ...pin }: Pin) => pin;
|
|
154
|
+
return toolResult(true, { claims: claims.map(view), notes: notes.map(view) });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function unpinTool(sessionId: string, session: Session, args: unknown) {
|
|
158
|
+
if (!plainObject(args, ['pinId']) || typeof args.pinId !== 'string') return toolResult(false, { error: 'pinId is required' });
|
|
159
|
+
const pins = await boardOf(session.mapping.agent.projectRoot);
|
|
160
|
+
const pin = pins.find(candidate => candidate.id === args.pinId && live(candidate));
|
|
161
|
+
if (!pin) return toolResult(false, { error: 'No such pin on this board' });
|
|
162
|
+
if (pin.sessionId !== sessionId) return toolResult(false, { error: `Only ${pin.agent}, who pinned it, can take it down` });
|
|
163
|
+
release(pin, 'unpinned');
|
|
164
|
+
return toolResult(true, { unpinned: pin.id });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function release(pin: Pin, reason: 'unpinned' | 'session_ended') {
|
|
168
|
+
pin.released = new Date().toISOString();
|
|
169
|
+
saveBoard(pin.project, { release: pin.id, at: pin.released });
|
|
170
|
+
runLog(pin.rootId, { event: 'board.unpin', pinId: pin.id, agent: pin.agent, reason });
|
|
171
|
+
if (sessions.has(pin.sessionId)) emit(pin.sessionId, { type: 'unpin', pinId: pin.id, reason });
|
|
172
|
+
void boardOf(pin.project).then(pins => pins.splice(0, pins.length, ...prune(pins)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A session's claims end with it. */
|
|
176
|
+
function releaseClaims(sessionId: string, project: string) {
|
|
177
|
+
const board = boards.get(project);
|
|
178
|
+
if (!board) return;
|
|
179
|
+
void board.then(pins => {
|
|
180
|
+
for (const pin of [...pins]) if (pin.kind === 'claim' && live(pin) && pin.sessionId === sessionId) release(pin, 'session_ended');
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { boardOf, claimConflicts, taskOf, addPin, pinTool, boardTool, unpinTool, releaseClaims, flush: () => boardWrites };
|
|
185
|
+
}
|
package/src/board.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The project board (plans/reference/ALPD.md §18): one group channel per project that every
|
|
6
|
+
* agent working on it shares, across trees. Agents pin claims (paths they are changing),
|
|
7
|
+
* decisions (the approach everyone follows) and findings (what others need to know), so
|
|
8
|
+
* agents working independently neither edit each other's files nor pursue different ideas.
|
|
9
|
+
* These helpers stay pure; storage and delivery live in the runtime.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const PIN_KINDS = ['claim', 'decision', 'finding'] as const;
|
|
13
|
+
import type { PinKind, Pin } from '@anhlp/alp-protocol/dto';
|
|
14
|
+
export type { PinKind, Pin } from '@anhlp/alp-protocol/dto';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const PIN_BODY_CHARS = 2000;
|
|
18
|
+
export const PIN_PATHS = 50;
|
|
19
|
+
/** Decisions and findings kept per project; live claims are always kept. */
|
|
20
|
+
export const BOARD_KEEP = 200;
|
|
21
|
+
const DIGEST_CHARS = 3000;
|
|
22
|
+
|
|
23
|
+
/** Resolve symlinked parents even for a file an assignment has not created yet. */
|
|
24
|
+
function canonicalPath(file: string): string {
|
|
25
|
+
try { return realpathSync(file); } catch {
|
|
26
|
+
const parent = path.dirname(file);
|
|
27
|
+
return parent === file ? file : path.join(canonicalPath(parent), path.basename(file));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const containsPath = (outer: string, inner: string) => {
|
|
31
|
+
const relative = path.relative(outer, inner);
|
|
32
|
+
return !relative || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Normalized project-relative paths, or absolute paths in a declared external write root. */
|
|
36
|
+
export function normalizePaths(paths: unknown, project: string, roots: string[] = [project]): string[] | string {
|
|
37
|
+
if (!Array.isArray(paths) || !paths.length || paths.length > PIN_PATHS) return `paths must list 1 to ${PIN_PATHS} files or directories`;
|
|
38
|
+
const normalized: string[] = [];
|
|
39
|
+
for (const entry of paths) {
|
|
40
|
+
if (typeof entry !== 'string' || !entry.trim()) return 'paths must be nonempty strings';
|
|
41
|
+
const absolute = canonicalPath(path.resolve(project, entry.trim()));
|
|
42
|
+
if (!roots.some(root => containsPath(canonicalPath(root), absolute))) return `${entry} is outside the project or declared write root`;
|
|
43
|
+
const relative = path.relative(canonicalPath(project), absolute).split(path.sep).join('/');
|
|
44
|
+
normalized.push(relative === '' ? '.' : relative === '..' || relative.startsWith('../') || path.isAbsolute(relative) ? absolute.split(path.sep).join('/') : relative);
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(normalized)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Two paths overlap when one is the other or contains it; '.' is the whole project. */
|
|
50
|
+
const contains = (outer: string, inner: string) => path.isAbsolute(outer) === path.isAbsolute(inner) && (outer === '.' || inner === outer || inner.startsWith(outer.endsWith('/') ? outer : `${outer}/`));
|
|
51
|
+
|
|
52
|
+
export function overlapping(a: string[], b: string[]) {
|
|
53
|
+
return a.filter(left => b.some(right => contains(left, right) || contains(right, left)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const live = (pin: Pin) => !pin.released;
|
|
57
|
+
|
|
58
|
+
export function renderPin(pin: Pin) {
|
|
59
|
+
const where = pin.paths?.length ? ` [${pin.paths.join(', ')}]` : '';
|
|
60
|
+
return `${pin.kind} ${pin.id} by ${pin.agent}${pin.task ? ` for task ${pin.task}` : ''}${where}: ${pin.body}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What an agent should know before it starts: live claims, then recent decisions and findings. */
|
|
64
|
+
export function renderBoard(pins: Pin[], limit = DIGEST_CHARS) {
|
|
65
|
+
const claims = pins.filter(pin => pin.kind === 'claim' && live(pin));
|
|
66
|
+
const notes = pins.filter(pin => pin.kind !== 'claim' && live(pin)).reverse();
|
|
67
|
+
if (!claims.length && !notes.length) return '';
|
|
68
|
+
const lines = ['Project board (shared by every agent on this project; read more with alp_board):'];
|
|
69
|
+
let size = lines[0].length;
|
|
70
|
+
for (const pin of [...claims, ...notes]) {
|
|
71
|
+
const line = `- ${renderPin(pin)}`;
|
|
72
|
+
if (size + line.length > limit) {
|
|
73
|
+
lines.push('- … more on alp_board');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
lines.push(line);
|
|
77
|
+
size += line.length;
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Models, modes and thinking options the runtime accepts; viewers render them as-is. */
|
|
2
|
+
import { MODELS } from '@anhlp/alp-core/models';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MODEL = 'gpt-5.6-sol';
|
|
5
|
+
export const DEFAULT_CLAUDE_MODEL = 'sonnet';
|
|
6
|
+
export const DEFAULT_ANTIGRAVITY_MODEL = 'gemini-3.8-flash-high';
|
|
7
|
+
// Main's and the supervisor's models and effort come from the session's team: templates/teams/<id>/team.json (ALPD §42).
|
|
8
|
+
/** Oracle runs on one of these; main may consult both for two opinions. */
|
|
9
|
+
export const ORACLE_MODELS = ['claude:claude-fable-5-1', 'codex:gpt-6-astra'];
|
|
10
|
+
export const ORACLE_THINKING = 'high';
|
|
11
|
+
export const modes = [{ id: 'read-only', label: 'Read only' }, { id: 'workspace-write', label: 'Workspace write' }, { id: 'full-access', label: 'Full access' }];
|
|
12
|
+
const modeRank: Record<string, number> = { 'read-only': 0, 'workspace-write': 1, 'full-access': 2 };
|
|
13
|
+
/** Whether a session in `mode` may run with `requested`; a child never exceeds its requester. */
|
|
14
|
+
export const withinMode = (requested: string, mode: string) => (modeRank[requested] ?? 3) <= (modeRank[mode] ?? -1);
|
|
15
|
+
export const writes = (mode: string) => mode !== 'read-only';
|
|
16
|
+
const option = (id: string) => ({ id, label: id });
|
|
17
|
+
const options = (ids: string[]) => ids.map(option);
|
|
18
|
+
export const thinkingOptions = options(['none', 'off', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra', 'ultracode']);
|
|
19
|
+
|
|
20
|
+
const model = (runtime: 'codex' | 'claude' | 'antigravity', id: string, label: string, description: string, modelThinkingOptions: Array<{ id: string; label: string }>, defaultThinkingOptionId?: string) => ({
|
|
21
|
+
id: `${runtime}:${id}`,
|
|
22
|
+
label: `${runtime === 'codex' ? 'Codex' : runtime === 'claude' ? 'Claude Code' : 'Antigravity'} · ${label}`,
|
|
23
|
+
description,
|
|
24
|
+
thinkingOptions: modelThinkingOptions,
|
|
25
|
+
...(defaultThinkingOptionId ? { defaultThinkingOptionId } : {}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const models = MODELS.map(entry => model(entry.runtime as 'codex' | 'claude' | 'antigravity', entry.id, entry.label, entry.description, options(entry.efforts), entry.effort));
|
|
29
|
+
|
|
30
|
+
export function thinkingOptionsFor(runtime: 'codex' | 'claude' | 'antigravity', nativeModel: string) {
|
|
31
|
+
return models.find(candidate => candidate.id === `${runtime}:${nativeModel}`)?.thinkingOptions ?? thinkingOptions;
|
|
32
|
+
}
|