@hypercarrier/pi-team-durable 0.0.0-stage → 0.1.0-rc.2
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 +21 -0
- package/README.md +209 -2
- package/dist/authority/anchor.d.ts +10 -0
- package/dist/authority/anchor.js +19 -0
- package/dist/authority/card-types.d.ts +152 -0
- package/dist/authority/card-types.js +123 -0
- package/dist/authority/contracts.d.ts +19 -0
- package/dist/authority/contracts.js +45 -0
- package/dist/authority/delivery.d.ts +60 -0
- package/dist/authority/delivery.js +158 -0
- package/dist/authority/graph-card.d.ts +4 -0
- package/dist/authority/graph-card.js +39 -0
- package/dist/authority/graph-control.d.ts +299 -0
- package/dist/authority/graph-control.js +1009 -0
- package/dist/authority/incremental-graph-control.d.ts +156 -0
- package/dist/authority/incremental-graph-control.js +1051 -0
- package/dist/authority/index.d.ts +15 -0
- package/dist/authority/index.js +15 -0
- package/dist/authority/interrupt.d.ts +11 -0
- package/dist/authority/interrupt.js +26 -0
- package/dist/authority/limits.d.ts +7 -0
- package/dist/authority/limits.js +7 -0
- package/dist/authority/notifier.d.ts +32 -0
- package/dist/authority/notifier.js +89 -0
- package/dist/authority/observation-store.d.ts +104 -0
- package/dist/authority/observation-store.js +226 -0
- package/dist/authority/observation.d.ts +13 -0
- package/dist/authority/observation.js +143 -0
- package/dist/authority/panel.d.ts +121 -0
- package/dist/authority/panel.js +44 -0
- package/dist/authority/projection.d.ts +5 -0
- package/dist/authority/projection.js +15 -0
- package/dist/authority/rc7-contract.json +9390 -0
- package/dist/authority/runtime.d.ts +106 -0
- package/dist/authority/runtime.js +115 -0
- package/dist/authority/state.d.ts +165 -0
- package/dist/authority/state.js +28 -0
- package/dist/authority/store.d.ts +144 -0
- package/dist/authority/store.js +675 -0
- package/dist/authority/sync.d.ts +13 -0
- package/dist/authority/sync.js +78 -0
- package/dist/authority/task-version-ref.d.ts +3 -0
- package/dist/authority/task-version-ref.js +5 -0
- package/dist/authority/tools.d.ts +105 -0
- package/dist/authority/tools.js +302 -0
- package/dist/testing.d.ts +11 -0
- package/dist/testing.js +10 -0
- package/package.json +63 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Tx, type EntryId } from '@earendil-works/pi-durable';
|
|
2
|
+
import type { CoreApi } from './tools.ts';
|
|
3
|
+
import { type Event, type Result } from './state.ts';
|
|
4
|
+
import { type Authority as State } from './store.ts';
|
|
5
|
+
export declare const UPDATE_PAGE_EVENTS = 50;
|
|
6
|
+
export declare const UPDATE_SCAN_ENTRIES = 100;
|
|
7
|
+
export declare function updates(s: State, events: Event[]): Promise<Result>;
|
|
8
|
+
/** Page native history without making the journal document an unbounded cache. */
|
|
9
|
+
export declare function pageEvents(tx: Tx, s: State, from: number, after?: EntryId): Promise<{
|
|
10
|
+
events: Event[];
|
|
11
|
+
scanThrough?: EntryId;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function sync(tx: Tx, s: State, view: 'snapshot' | 'updates' | undefined, api: CoreApi, snapshot: () => Promise<Result>): Promise<Result>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LiveDoc } from '@earendil-works/pi-durable';
|
|
2
|
+
import { openObservations, reservationHead, reserve, pages, savePage } from "./observation-store.js";
|
|
3
|
+
import { taskCards, recentEvents } from "./store.js";
|
|
4
|
+
export const UPDATE_PAGE_EVENTS = 50;
|
|
5
|
+
export const UPDATE_SCAN_ENTRIES = 100;
|
|
6
|
+
export async function updates(s, events) {
|
|
7
|
+
const team = s.team.current;
|
|
8
|
+
const ids = [...new Set(events.flatMap(e => e.taskIds ?? (e.taskId ? [e.taskId] : [])))];
|
|
9
|
+
const cards = await taskCards(s, ids);
|
|
10
|
+
return { kind: 'updates',
|
|
11
|
+
team_changes: events.filter(e => e.kind === 'team_created' || e.kind === 'team_stopped').map(e => ({ kind: e.kind === 'team_created' ? 'created' : 'lifecycle', text: e.kind === 'team_created' ? team.purpose : 'Team stopped.' })),
|
|
12
|
+
worker_changes: events.filter(e => e.worker && ['worker_created', 'worker_stopped'].includes(e.kind)).map(e => ({ worker: e.worker, scope: team.workers[e.worker]?.scope ?? 'Historical Worker', kind: e.kind === 'worker_created' ? 'created' : 'stopped', text: e.kind === 'worker_created' ? 'Worker created.' : 'Worker stopped.' })),
|
|
13
|
+
task_changes: ids.flatMap(id => { const current = cards.find(t => t.id === id); return current ? [{ task_id: id, change_kinds: ['status'], journal_entries: events.filter(e => e.taskId === id && e.at && e.text).map(e => ({ id: `${e.epoch}:${e.seq}`, at: e.at, actor: e.actor, kind: 'note', text: e.text })), current }] : []; }),
|
|
14
|
+
alerts: [], };
|
|
15
|
+
}
|
|
16
|
+
/** Page native history without making the journal document an unbounded cache. */
|
|
17
|
+
export async function pageEvents(tx, s, from, after) {
|
|
18
|
+
const recent = s.journal.events.filter(e => e.seq > from);
|
|
19
|
+
if (recent.length && s.journal.events[0].seq <= from + 1) {
|
|
20
|
+
const events = await recentEvents(s, recent.slice(0, UPDATE_PAGE_EVENTS));
|
|
21
|
+
return { events, scanThrough: events.at(-1)?.entryId };
|
|
22
|
+
}
|
|
23
|
+
const native = await tx.scanEntries({ conversationId: s.team.current.lead, order: 'ascending', ...(after ? { minEntryId: after } : {}) }, UPDATE_SCAN_ENTRIES);
|
|
24
|
+
const events = [];
|
|
25
|
+
let scanThrough = after;
|
|
26
|
+
for (const entry of native.items) {
|
|
27
|
+
if (after !== undefined && entry.id <= after)
|
|
28
|
+
continue;
|
|
29
|
+
scanThrough = entry.id;
|
|
30
|
+
if (entry.kind === 'ptb.event') {
|
|
31
|
+
const event = entry.data;
|
|
32
|
+
if (event.epoch === s.team.current.epoch && event.seq > from)
|
|
33
|
+
events.push({ ...event, entryId: entry.id });
|
|
34
|
+
}
|
|
35
|
+
if (events.length >= UPDATE_PAGE_EVENTS)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return { events, scanThrough };
|
|
39
|
+
}
|
|
40
|
+
export async function sync(tx, s, view, api, snapshot) {
|
|
41
|
+
const team = s.team.current, live = await tx.doc(LiveDoc, api.conversationId), run = live.run?.inputs[0];
|
|
42
|
+
if (run === undefined)
|
|
43
|
+
return { kind: 'unavailable', reason: 'team_state_unavailable', message: 'Observation requires a persisted lead run.' };
|
|
44
|
+
const o = await openObservations(tx);
|
|
45
|
+
o.head.paused = false;
|
|
46
|
+
const active = await pages(o, 'openPages');
|
|
47
|
+
if (active.some(p => p.epoch === team.epoch && p.run !== run))
|
|
48
|
+
return { kind: 'refused', reason: 'observation_in_progress', message: 'Another run or automatic delivery owns an unacknowledged page.', state_changed: false, observation_advanced: false };
|
|
49
|
+
const id = `${team.epoch}:sync:${api.taskId}`, from = await reservationHead(o, team.epoch, run);
|
|
50
|
+
const carrier = { kind: 'tool', callId: api.callId ?? String(api.taskId) };
|
|
51
|
+
// Failed-run pages reappear once in order; the old replay receipt remains immutable.
|
|
52
|
+
const released = (await pages(o, 'releasedPages', true)).find(p => p.epoch === team.epoch);
|
|
53
|
+
if (released) {
|
|
54
|
+
const result = JSON.parse(released.body);
|
|
55
|
+
released.state = 'replaced';
|
|
56
|
+
await savePage(o, released);
|
|
57
|
+
await reserve(o, { id, epoch: team.epoch, lead: team.lead, run, from: released.from, through: released.through, carrier, ...(released.scanThrough ? { scanThrough: released.scanThrough } : {}) }, result);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
const outstanding = active.filter(p => p.epoch === team.epoch && p.run === run);
|
|
61
|
+
if ((view !== 'snapshot' || outstanding.length > 0) && from >= s.journal.seq)
|
|
62
|
+
return { kind: 'caught_up', head: s.journal.seq, epoch_id: team.epoch };
|
|
63
|
+
let result, through, scanThrough;
|
|
64
|
+
if (!outstanding.length && (view === 'snapshot' || from === 0)) {
|
|
65
|
+
result = await snapshot();
|
|
66
|
+
through = s.journal.seq;
|
|
67
|
+
scanThrough = s.journal.events.at(-1)?.entryId;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const previous = Math.max(o.head.scanByEpoch[team.epoch] ?? 0, ...outstanding.map(p => p.scanThrough ?? 0));
|
|
71
|
+
const page = await pageEvents(tx, s, from, previous || undefined);
|
|
72
|
+
result = await updates(s, page.events);
|
|
73
|
+
through = page.events.at(-1)?.seq ?? from;
|
|
74
|
+
scanThrough = page.scanThrough;
|
|
75
|
+
}
|
|
76
|
+
await reserve(o, { id, epoch: team.epoch, lead: team.lead, run, from, through, carrier, ...(scanThrough ? { scanThrough } : {}) }, result);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Context } from '@earendil-works/chord/context';
|
|
2
|
+
import { type ToolExecutionApi, type Tx } from '@earendil-works/pi-durable';
|
|
3
|
+
import { type Role, type ToolName } from './contracts.ts';
|
|
4
|
+
import { type GraphTaskTransition, type GraphVersionRef } from './graph-control.ts';
|
|
5
|
+
import type { TaskVersionRef } from './task-version-ref.ts';
|
|
6
|
+
import type { ConversationId } from '@earendil-works/pi-durable';
|
|
7
|
+
import { type Binding, type ModelRole, type ModelRoleCatalog, type Result, type StopIntent } from './state.ts';
|
|
8
|
+
import { type Authority as State } from './store.ts';
|
|
9
|
+
export type CoreApi = Pick<ToolExecutionApi, 'taskId' | 'conversationId' | 'commit' | 'now'> & Partial<Pick<ToolExecutionApi, 'callId'>>;
|
|
10
|
+
export interface CoreOptions {
|
|
11
|
+
modelRoles: ModelRole[];
|
|
12
|
+
defaultModelRole?: string;
|
|
13
|
+
goalMaxLength?: number;
|
|
14
|
+
createWorker?: (tx: Tx, role: ModelRole) => Promise<Binding>;
|
|
15
|
+
resolveModelRole?: (tx: Tx, role: ModelRole) => Promise<ModelRole>;
|
|
16
|
+
catalog?: (tx: Tx, lead: ConversationId) => Promise<ModelRoleCatalog>;
|
|
17
|
+
stopWorker?: (binding: Binding, context: Context) => Promise<boolean>;
|
|
18
|
+
settleStops?: (stops: {
|
|
19
|
+
key: string;
|
|
20
|
+
intent: StopIntent;
|
|
21
|
+
}[], context: Context, api: CoreApi) => Promise<boolean[]>;
|
|
22
|
+
afterCommand?: (tx: Tx, s: State, name: ToolName, result: Result | {
|
|
23
|
+
stops: string[];
|
|
24
|
+
epoch: string;
|
|
25
|
+
}, api: CoreApi) => Promise<void>;
|
|
26
|
+
sync?: (tx: Tx, s: State, view: Args['view'], api: CoreApi, snapshot: () => Promise<Result>) => Promise<Result>;
|
|
27
|
+
}
|
|
28
|
+
interface Args {
|
|
29
|
+
name?: string;
|
|
30
|
+
purpose?: string;
|
|
31
|
+
scope?: string;
|
|
32
|
+
model_role?: string;
|
|
33
|
+
worker?: string;
|
|
34
|
+
view?: 'snapshot' | 'updates';
|
|
35
|
+
task_id?: string;
|
|
36
|
+
task_ids?: string[];
|
|
37
|
+
operation_id?: string;
|
|
38
|
+
expected_graph_version?: GraphVersionRef;
|
|
39
|
+
expected_version?: TaskVersionRef;
|
|
40
|
+
transition?: GraphTaskTransition;
|
|
41
|
+
current_context?: string;
|
|
42
|
+
evidence?: string;
|
|
43
|
+
tasks?: {
|
|
44
|
+
key: string;
|
|
45
|
+
title: string;
|
|
46
|
+
goal: string;
|
|
47
|
+
assignee: string;
|
|
48
|
+
needs?: string[];
|
|
49
|
+
on_goal_failed?: {
|
|
50
|
+
target: string;
|
|
51
|
+
max_traversals: number;
|
|
52
|
+
};
|
|
53
|
+
}[];
|
|
54
|
+
to?: string;
|
|
55
|
+
kind?: string;
|
|
56
|
+
text?: string;
|
|
57
|
+
task_version?: string;
|
|
58
|
+
interrupt?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** The catalog resolver that options select: the host's per-lead port, else the static list. */
|
|
61
|
+
export declare function catalogOf(options: Pick<CoreOptions, 'modelRoles' | 'defaultModelRole' | 'catalog'>): (tx: Tx, lead: ConversationId) => Promise<ModelRoleCatalog>;
|
|
62
|
+
/** Authority commands commit intent and replay receipts; injected ports own effects. */
|
|
63
|
+
export declare function createAuthorityTools(options: CoreOptions): {
|
|
64
|
+
lead: {
|
|
65
|
+
label: string;
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
parameters: import("typebox").TSchema;
|
|
69
|
+
constrainedSampling?: false | import("@earendil-works/pi-ai").ConstrainedSamplingConfig;
|
|
70
|
+
replay?: "safe" | "unsafe";
|
|
71
|
+
executionMode?: import("@earendil-works/pi-durable").ToolExecutionMode;
|
|
72
|
+
prepareArguments?(args: unknown): unknown;
|
|
73
|
+
structuredOutputSchema?: import("typebox").TSchema;
|
|
74
|
+
callers?: readonly ("model" | "tools")[];
|
|
75
|
+
deferred?: true;
|
|
76
|
+
outputLimits?: {
|
|
77
|
+
readonly maxBytes?: number;
|
|
78
|
+
readonly maxLines?: number;
|
|
79
|
+
readonly retain?: "head" | "tail";
|
|
80
|
+
};
|
|
81
|
+
execute(args: unknown, api: ToolExecutionApi<import("@earendil-works/chord").JsonValue>, context: Context): Promise<import("@earendil-works/pi-durable").ToolExecutionResult<import("@earendil-works/chord").JsonValue>>;
|
|
82
|
+
}[];
|
|
83
|
+
worker: {
|
|
84
|
+
label: string;
|
|
85
|
+
name: string;
|
|
86
|
+
description: string;
|
|
87
|
+
parameters: import("typebox").TSchema;
|
|
88
|
+
constrainedSampling?: false | import("@earendil-works/pi-ai").ConstrainedSamplingConfig;
|
|
89
|
+
replay?: "safe" | "unsafe";
|
|
90
|
+
executionMode?: import("@earendil-works/pi-durable").ToolExecutionMode;
|
|
91
|
+
prepareArguments?(args: unknown): unknown;
|
|
92
|
+
structuredOutputSchema?: import("typebox").TSchema;
|
|
93
|
+
callers?: readonly ("model" | "tools")[];
|
|
94
|
+
deferred?: true;
|
|
95
|
+
outputLimits?: {
|
|
96
|
+
readonly maxBytes?: number;
|
|
97
|
+
readonly maxLines?: number;
|
|
98
|
+
readonly retain?: "head" | "tail";
|
|
99
|
+
};
|
|
100
|
+
execute(args: unknown, api: ToolExecutionApi<import("@earendil-works/chord").JsonValue>, context: Context): Promise<import("@earendil-works/pi-durable").ToolExecutionResult<import("@earendil-works/chord").JsonValue>>;
|
|
101
|
+
}[];
|
|
102
|
+
execute: (name: ToolName, role: Role, input: unknown, api: CoreApi, context: Context) => Promise<Result>;
|
|
103
|
+
contract: import("./contracts.ts").Contract;
|
|
104
|
+
};
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { defineTool, LiveDoc } from '@earendil-works/pi-durable';
|
|
2
|
+
import { contracts, checkSchema, checkResult, TOOL_NAMES } from "./contracts.js";
|
|
3
|
+
import { GraphControlRefusal } from "./graph-control.js";
|
|
4
|
+
import { projectGraphTaskCards } from "./graph-card.js";
|
|
5
|
+
import { snapshotResult } from "./projection.js";
|
|
6
|
+
import { plain, event } from "./state.js";
|
|
7
|
+
import { openAuthority as open, startEpoch, taskCards, unfinished, graphCommand, hasOperation, addAlert, addObligation, callReceipt, saveCall, allObligations, setObligationState } from "./store.js";
|
|
8
|
+
const LEAD = 'team-lead';
|
|
9
|
+
class Declined extends Error {
|
|
10
|
+
result;
|
|
11
|
+
constructor(result) { super(String(result.message ?? result.reason)); this.result = result; }
|
|
12
|
+
}
|
|
13
|
+
const decline = (result) => { throw new Declined(result); };
|
|
14
|
+
function unavailable(name, args, reason, message) {
|
|
15
|
+
return decline({ kind: 'unavailable', reason, message,
|
|
16
|
+
...(['task_graph_apply', 'task_update'].includes(name) ? { operation_id: args.operation_id } : {}),
|
|
17
|
+
...(name === 'task_update' ? { task_id: args.task_id } : {}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function failureReason(name) {
|
|
21
|
+
return ['task_read', 'task_update', 'task_graph_apply'].includes(name) ? 'task_authority_unavailable' : name === 'team_sync' ? 'team_state_unavailable' : 'team_authority_unavailable';
|
|
22
|
+
}
|
|
23
|
+
function caller(s, api, role, name, args) {
|
|
24
|
+
const team = s.team.current;
|
|
25
|
+
if (!team || (team.lifecycle !== 'active' && !(name === 'team_shutdown' && role === 'lead' && team.lead === api.conversationId)))
|
|
26
|
+
return unavailable(name, args, 'no_active_team', 'No active Team.');
|
|
27
|
+
if (role === 'lead' && team.lead === api.conversationId)
|
|
28
|
+
return LEAD;
|
|
29
|
+
const workerName = s.head.workerBindings[String(api.conversationId)];
|
|
30
|
+
const candidate = workerName ? team.workers[workerName] : undefined;
|
|
31
|
+
const worker = candidate?.state === 'active' && candidate.conversationId === api.conversationId ? candidate : undefined;
|
|
32
|
+
if (role === 'worker' && worker)
|
|
33
|
+
return worker.name;
|
|
34
|
+
return unavailable(name, args, failureReason(name), 'Caller is not the exact current Team binding.');
|
|
35
|
+
}
|
|
36
|
+
function controller(s, _api, name, args) {
|
|
37
|
+
if (!s.head.graph || s.head.epoch !== s.team.current?.epoch)
|
|
38
|
+
return unavailable(name, args, failureReason(name), 'Established graph authority is missing.');
|
|
39
|
+
if (!Number.isSafeInteger(s.head.graph.sequence) || !s.head.graph.ready || !s.head.graph.occupied)
|
|
40
|
+
return unavailable(name, args, failureReason(name), 'Established graph authority is corrupt.');
|
|
41
|
+
}
|
|
42
|
+
function modelTask(t) {
|
|
43
|
+
const [card] = projectGraphTaskCards({ readTasks: () => [t], readAttempts: () => [], selectReadyFrontier: () => [] });
|
|
44
|
+
return { id: t.id, status: t.state.kind, assignee: t.assignee, needs: t.needs, state: card.state, attempts_started: t.attemptsStarted, version: t.version,
|
|
45
|
+
...('attemptId' in t.state ? { current_attempt_id: t.state.attemptId } : {}), ...(t.acceptedAttemptId ? { accepted_attempt_id: t.acceptedAttemptId } : {}) };
|
|
46
|
+
}
|
|
47
|
+
/** The catalog resolver that options select: the host's per-lead port, else the static list. */
|
|
48
|
+
export function catalogOf(options) {
|
|
49
|
+
return async (tx, lead) => options.catalog ? options.catalog(tx, lead) : { modelRoles: options.modelRoles, ...(options.defaultModelRole ? { defaultModelRole: options.defaultModelRole } : {}) };
|
|
50
|
+
}
|
|
51
|
+
/** Authority commands commit intent and replay receipts; injected ports own effects. */
|
|
52
|
+
export function createAuthorityTools(options) {
|
|
53
|
+
const contract = contracts(options.goalMaxLength);
|
|
54
|
+
const rolesOf = catalogOf(options);
|
|
55
|
+
const catalog = (c) => ({ model_roles: c.modelRoles.map(r => ({ name: r.name, use: r.use })), ...(c.defaultModelRole ? { default_model_role: c.defaultModelRole } : {}) });
|
|
56
|
+
async function command(name, role, a, api, tx, s) {
|
|
57
|
+
if (name === 'team_create') {
|
|
58
|
+
if (role !== 'lead')
|
|
59
|
+
return unavailable(name, a, 'team_authority_unavailable', 'Worker cannot create a Team.');
|
|
60
|
+
if (s.team.current?.lifecycle === 'active')
|
|
61
|
+
return decline({ kind: 'refused', reason: 'active_team_exists', message: 'An active Team already owns this store.' });
|
|
62
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(a.name))
|
|
63
|
+
return decline({ kind: 'refused', reason: 'name_unavailable', message: 'Invalid Team name.' });
|
|
64
|
+
if (s.team.current) {
|
|
65
|
+
if (s.team.current.lead !== api.conversationId)
|
|
66
|
+
return unavailable(name, a, 'team_authority_unavailable', 'Only the established lead can create the next Team.');
|
|
67
|
+
s.team.history.push(plain(s.team.current));
|
|
68
|
+
}
|
|
69
|
+
s.team.current = { epoch: `team:${api.taskId}`, name: a.name, purpose: a.purpose, lead: api.conversationId, lifecycle: 'active', workers: {}, createdAt: api.now() };
|
|
70
|
+
await startEpoch(s, s.team.current.epoch);
|
|
71
|
+
delete s.journal.reservation;
|
|
72
|
+
s.journal.acknowledged = 0;
|
|
73
|
+
await event(tx, s, LEAD, 'team_created', { at: new Date(api.now()).toISOString() });
|
|
74
|
+
return { kind: 'team_created', team: { name: a.name, lifecycle: 'active' }, ...catalog(await rolesOf(tx, api.conversationId)) };
|
|
75
|
+
}
|
|
76
|
+
const actor = caller(s, api, role, name, a), team = s.team.current;
|
|
77
|
+
if (name === 'ensure_worker') {
|
|
78
|
+
if (team.shutdownPending)
|
|
79
|
+
return unavailable(name, a, 'carrier_unavailable', 'Team shutdown is pending.');
|
|
80
|
+
if (a.name === LEAD || !/^[A-Za-z0-9_-]{1,64}$/.test(a.name))
|
|
81
|
+
return unavailable(name, a, 'team_authority_unavailable', 'Invalid or reserved Worker name.');
|
|
82
|
+
const prior = Object.hasOwn(team.workers, a.name) ? team.workers[a.name] : undefined;
|
|
83
|
+
const existing = prior ? { name: prior.name, scope: prior.scope, carrier: prior.state === 'active' ? 'connected' : 'absent', model_role: prior.role.name } : undefined;
|
|
84
|
+
if (prior && prior.scope !== a.scope)
|
|
85
|
+
return decline({ kind: 'refused', reason: 'name_scope_conflict', existing_worker: existing });
|
|
86
|
+
if (prior && a.model_role && prior.role.name !== a.model_role)
|
|
87
|
+
return decline({ kind: 'refused', reason: 'model_conflict', existing_worker: existing });
|
|
88
|
+
if (prior?.state === 'stopping')
|
|
89
|
+
return unavailable(name, a, 'carrier_unavailable', 'The exact Worker stop is pending.');
|
|
90
|
+
if (prior?.state === 'active')
|
|
91
|
+
return { kind: 'worker_ensured', effect: 'reused', worker: { name: prior.name, carrier: 'connected', model_role: prior.role.name } };
|
|
92
|
+
const roles = prior ? undefined : await rolesOf(tx, team.lead);
|
|
93
|
+
let selected = prior?.role ?? roles.modelRoles.find(r => r.name === (a.model_role ?? roles.defaultModelRole));
|
|
94
|
+
if (!selected)
|
|
95
|
+
return decline({ kind: 'refused', reason: 'invalid_model_role', valid_model_roles: catalog(roles).model_roles, message: 'Select a configured model role.' });
|
|
96
|
+
if (!options.createWorker)
|
|
97
|
+
return unavailable(name, a, 'carrier_unavailable', 'Worker anchor adapter is not installed.');
|
|
98
|
+
if (options.resolveModelRole)
|
|
99
|
+
selected = await options.resolveModelRole(tx, plain(selected));
|
|
100
|
+
const binding = await options.createWorker(tx, plain(selected));
|
|
101
|
+
team.workers = { ...plain(team.workers), [a.name]: { ...binding, name: a.name, scope: a.scope, generation: (prior?.generation ?? 0) + 1, state: 'active', role: plain(selected), createdAt: api.now() } };
|
|
102
|
+
if (prior) {
|
|
103
|
+
delete s.head.workerBindings[String(prior.conversationId)];
|
|
104
|
+
s.head.stopRefresh = true;
|
|
105
|
+
}
|
|
106
|
+
s.head.workerBindings[String(binding.conversationId)] = a.name;
|
|
107
|
+
await event(tx, s, actor, 'worker_created', { worker: a.name, at: new Date(api.now()).toISOString() });
|
|
108
|
+
return { kind: 'worker_ensured', effect: prior ? 'reconnected' : 'created', worker: { name: a.name, carrier: 'connected', model_role: selected.name } };
|
|
109
|
+
}
|
|
110
|
+
controller(s, api, name, a);
|
|
111
|
+
if (name === 'team_shutdown' && team.lifecycle === 'stopped')
|
|
112
|
+
return { kind: 'team_shutdown', lifecycle: 'stopped', stopped_workers: Object.values(team.workers).filter(w => w.state === 'stopped').map(w => w.name), unfinished_task_ids: await unfinished(s) };
|
|
113
|
+
if (name === 'task_read') {
|
|
114
|
+
const cards = new Map((await taskCards(s, role === 'worker' ? [a.task_id] : a.task_ids)).map(t => [t.id, t]));
|
|
115
|
+
if (role === 'worker')
|
|
116
|
+
return cards.has(a.task_id) ? { kind: 'found', task: cards.get(a.task_id) } : { kind: 'missing', task_id: a.task_id, reason: 'task_not_found' };
|
|
117
|
+
return { kind: 'task_read_batch', outcomes: a.task_ids.map((id, input_index) => cards.has(id) ? { kind: 'found', input_index, task_id: id, task: cards.get(id) } : { kind: 'missing', input_index, task_id: id, reason: 'task_not_found' }) };
|
|
118
|
+
}
|
|
119
|
+
if (name === 'task_graph_apply') {
|
|
120
|
+
// The controller owns replay identity. Check current assignees only for a new operation.
|
|
121
|
+
const already = await hasOperation(s, a.operation_id);
|
|
122
|
+
if (!already && a.tasks.some(t => !Object.hasOwn(team.workers, t.assignee) || team.workers[t.assignee].state !== 'active'))
|
|
123
|
+
return decline({ kind: 'refused', operation_id: a.operation_id, reason: 'worker_unavailable', message: 'Every assignee must be a current Worker.' });
|
|
124
|
+
const tasks = a.tasks.map(t => ({ key: t.key, title: t.title, goal: t.goal, assignee: t.assignee, needs: t.needs, ...(t.on_goal_failed ? { onGoalFailed: { target: t.on_goal_failed.target, maxTraversals: t.on_goal_failed.max_traversals } } : {}) }));
|
|
125
|
+
const r = await graphCommand(s, { kind: 'apply', input: { operationId: a.operation_id, expectedGraphVersion: a.expected_graph_version, tasks } }, api.now);
|
|
126
|
+
if (!r.replayed)
|
|
127
|
+
await event(tx, s, actor, 'graph_applied', { taskIds: r.tasks.map(t => t.id), at: new Date(api.now()).toISOString() });
|
|
128
|
+
return { kind: 'task_graph_applied', operation_id: r.operationId, graph_version: r.graphVersion, replayed: r.replayed, tasks_by_key: Object.fromEntries(r.tasks.map(t => [t.id, modelTask(t)])), ready_task_ids: r.readyTaskIds };
|
|
129
|
+
}
|
|
130
|
+
if (name === 'task_update') {
|
|
131
|
+
if (role === 'worker' && a.transition === 'cancel')
|
|
132
|
+
return decline({ kind: 'refused', task_id: a.task_id, operation_id: a.operation_id, reason: 'worker_mismatch', message: 'Only the lead cancels Tasks.' });
|
|
133
|
+
const r = await graphCommand(s, { kind: 'transition', input: { taskId: a.task_id, operationId: a.operation_id, expectedVersion: a.expected_version, transition: a.transition, currentContext: a.current_context, evidence: a.evidence, worker: role === 'worker' ? actor : a.transition && a.transition !== 'cancel' ? LEAD : undefined } }, api.now);
|
|
134
|
+
if (!r.replayed)
|
|
135
|
+
await event(tx, s, actor, 'task_transitioned', { taskId: r.task.id, at: new Date(api.now()).toISOString(), text: a.evidence ?? a.current_context ?? r.transition });
|
|
136
|
+
return { kind: 'updated', operation_id: r.operationId, replayed: r.replayed, transition: r.transition, task: { id: r.task.id, status: r.task.state.kind, assignee: r.task.assignee, version: r.task.version }, ready_task_ids: r.readyTaskIds, ...(r.failureTraversal ? { failure_traversal: { source_task_id: r.failureTraversal.sourceTaskId, target_task_id: r.failureTraversal.targetTaskId, traversal: r.failureTraversal.traversal } } : {}) };
|
|
137
|
+
}
|
|
138
|
+
if (name === 'alert_send') {
|
|
139
|
+
const to = role === 'worker' ? LEAD : a.to;
|
|
140
|
+
if (!a.text?.trim() || a.task_version && !a.task_id || (to === '*') !== (a.kind === 'announcement'))
|
|
141
|
+
return decline({ kind: 'refused', reason: 'invalid_fanout', message: 'Invalid Alert text, Task reference or announcement target.' });
|
|
142
|
+
const recipients = to === LEAD ? [{ name: LEAD, conversationId: team.lead, generation: 0 }] : Object.values(team.workers).filter(w => w.state === 'active' && (to === '*' || w.name === to)).map(w => ({ name: w.name, conversationId: w.conversationId, generation: w.generation }));
|
|
143
|
+
if (!recipients.length)
|
|
144
|
+
return decline({ kind: 'refused', reason: to === '*' ? 'no_eligible_recipients' : 'recipient_not_current', message: 'No current recipient.' });
|
|
145
|
+
const id = `${team.epoch}:alert:${api.taskId}`;
|
|
146
|
+
await addAlert(s, plain({ id, from: actor, kind: a.kind, text: a.text.trim(), ...(a.interrupt ? { interrupt: true } : {}), task_id: a.task_id, task_version: a.task_version, receivedAt: new Date(api.now()).toISOString(), recipients }));
|
|
147
|
+
for (const recipient of recipients) {
|
|
148
|
+
const key = `${id}:${recipient.name}:g${recipient.generation}`;
|
|
149
|
+
const run = a.interrupt ? (await tx.doc(LiveDoc, recipient.conversationId)).run?.taskId : undefined;
|
|
150
|
+
await addObligation(s, { key, epoch: team.epoch, kind: 'alert', recipient: recipient.name, conversationId: recipient.conversationId, generation: recipient.generation, alertId: id, state: 'pending', acceptedAt: api.now(), ...(a.interrupt ? { interrupt: true, ...(run !== undefined ? { interruptRun: run } : {}) } : {}) });
|
|
151
|
+
}
|
|
152
|
+
await event(tx, s, actor, 'alert_accepted', { at: new Date(api.now()).toISOString() });
|
|
153
|
+
return { kind: 'alert_sent', accepted_recipients: recipients.map(r => r.name), failed_recipients: [], ...(a.task_id ? { task_id: a.task_id } : {}), ...(a.task_version ? { task_version: a.task_version } : {}) };
|
|
154
|
+
}
|
|
155
|
+
if (name === 'team_sync') {
|
|
156
|
+
// Never consume a page on a tool call alone.
|
|
157
|
+
if (!options.sync && s.journal.reservation)
|
|
158
|
+
return { kind: 'refused', reason: 'observation_in_progress', message: 'An observation awaits presentation acknowledgement.', state_changed: false, observation_advanced: false };
|
|
159
|
+
if (!options.sync)
|
|
160
|
+
s.journal.reservation = { epoch: team.epoch, call: String(api.taskId), through: s.journal.seq };
|
|
161
|
+
const roles = await rolesOf(tx, team.lead), snapshot = () => snapshotResult(s, roles.modelRoles, roles.defaultModelRole);
|
|
162
|
+
return options.sync ? options.sync(tx, s, a.view, api, snapshot) : snapshot();
|
|
163
|
+
}
|
|
164
|
+
// Lifecycle effects happen after the committed stop intent, outside this transaction.
|
|
165
|
+
let workers;
|
|
166
|
+
if (name === 'worker_stop') {
|
|
167
|
+
if (a.worker === LEAD)
|
|
168
|
+
return decline({ kind: 'refused', reason: 'leader_reserved', worker: a.worker });
|
|
169
|
+
const worker = Object.hasOwn(team.workers, a.worker) ? team.workers[a.worker] : undefined;
|
|
170
|
+
if (!worker)
|
|
171
|
+
return decline({ kind: 'refused', reason: 'worker_not_found', worker: a.worker });
|
|
172
|
+
const held = await unfinished(s, worker.name);
|
|
173
|
+
if (held.length)
|
|
174
|
+
return decline({ kind: 'refused', reason: 'nonterminal_tasks_assigned', worker: worker.name, guarding_task_ids: held });
|
|
175
|
+
if (worker.state === 'stopped')
|
|
176
|
+
return { kind: 'worker_stopped', worker: worker.name };
|
|
177
|
+
workers = [worker];
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
workers = Object.values(team.workers).filter(w => w.state !== 'stopped');
|
|
181
|
+
if (workers.length && !options.stopWorker && !options.settleStops)
|
|
182
|
+
return unavailable(name, a, name === 'worker_stop' ? 'carrier_unavailable' : 'team_authority_unavailable', 'Anchor stop adapter is not installed.');
|
|
183
|
+
const stops = workers.map(w => { const key = `${team.epoch}:stop:${w.name}:g${w.generation}`; w.state = 'stopping'; s.team.stops[key] ??= { epoch: team.epoch, worker: w.name, binding: { conversationId: w.conversationId, anchorId: w.anchorId }, generation: w.generation, tasks: [w.anchorId], conversations: [w.conversationId], confirmed: false }; return key; });
|
|
184
|
+
for (const obligation of await allObligations(s))
|
|
185
|
+
if (obligation.epoch === team.epoch && workers.some(w => w.name === obligation.recipient))
|
|
186
|
+
await setObligationState(s, obligation, 'retired');
|
|
187
|
+
if (name === 'team_shutdown')
|
|
188
|
+
team.shutdownPending = true;
|
|
189
|
+
return { stops, epoch: team.epoch };
|
|
190
|
+
}
|
|
191
|
+
async function execute(name, role, input, api, context) {
|
|
192
|
+
const schema = contract[role][name];
|
|
193
|
+
if (!schema || !checkSchema(schema, input))
|
|
194
|
+
throw new Error(`Invalid ${role} ${name} parameters`);
|
|
195
|
+
const a = input;
|
|
196
|
+
const fingerprint = JSON.stringify({ name, role, input, conversation: api.conversationId });
|
|
197
|
+
try {
|
|
198
|
+
const result = await api.commit(async (tx) => {
|
|
199
|
+
const native = (await tx.task(api.taskId))?.kind === 'pi.tool';
|
|
200
|
+
const s = await open(tx), receipt = await callReceipt(s, api.taskId, native), saved = receipt.fingerprint ? receipt : undefined;
|
|
201
|
+
s.deferFlush = true;
|
|
202
|
+
if (saved) {
|
|
203
|
+
if (saved.fingerprint !== fingerprint)
|
|
204
|
+
throw new Error('Durable tool identity reused with changed input');
|
|
205
|
+
return saved.result ? JSON.parse(saved.result) : plain({ stops: saved.stops, epoch: saved.epoch });
|
|
206
|
+
}
|
|
207
|
+
const r = await command(name, role, a, api, tx, s);
|
|
208
|
+
if (name !== 'task_read')
|
|
209
|
+
await options.afterCommand?.(tx, s, name, r, api);
|
|
210
|
+
if ('kind' in r) {
|
|
211
|
+
const clean = plain(checkResult(contract, name, r));
|
|
212
|
+
if (name !== 'task_read')
|
|
213
|
+
await saveCall(s, api.taskId, { fingerprint, result: clean });
|
|
214
|
+
return clean;
|
|
215
|
+
}
|
|
216
|
+
await saveCall(s, api.taskId, { fingerprint, stops: r.stops, epoch: r.epoch });
|
|
217
|
+
return plain(r);
|
|
218
|
+
}, context);
|
|
219
|
+
if ('kind' in result)
|
|
220
|
+
return result;
|
|
221
|
+
const pending = (await api.commit(async (tx) => { const s = await open(tx); return result.stops.map(key => ({ key, intent: plain(s.team.stops[key]) })); }, context)).filter(p => !p.intent.confirmed);
|
|
222
|
+
const settled = options.settleStops ? await options.settleStops(pending, context, api) : await Promise.all(pending.map(p => p.intent.binding ? options.stopWorker(p.intent.binding, context) : false));
|
|
223
|
+
for (const [i, { key: stop, intent }] of pending.entries()) {
|
|
224
|
+
if (settled[i])
|
|
225
|
+
await api.commit(async (tx) => {
|
|
226
|
+
const s = await open(tx), w = intent.worker ? s.team.current?.workers[intent.worker] : undefined;
|
|
227
|
+
if (s.team.current?.epoch !== intent.epoch || (intent.binding && (!w || w.generation !== intent.generation || w.anchorId !== intent.binding.anchorId)))
|
|
228
|
+
throw new Error('Stop binding changed');
|
|
229
|
+
if (!s.team.stops[stop].confirmed) {
|
|
230
|
+
s.team.stops[stop].confirmed = true;
|
|
231
|
+
if (w) {
|
|
232
|
+
w.state = 'stopped';
|
|
233
|
+
await event(tx, s, LEAD, 'worker_stopped', { worker: w.name, at: new Date(api.now()).toISOString() });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, context);
|
|
237
|
+
}
|
|
238
|
+
return await api.commit(async (tx) => {
|
|
239
|
+
const s = await open(tx), team = s.team.current;
|
|
240
|
+
await callReceipt(s, api.taskId);
|
|
241
|
+
if (team.epoch !== result.epoch)
|
|
242
|
+
throw new Error('Team epoch changed during stop');
|
|
243
|
+
const stopped = result.stops.filter(id => s.team.stops[id].confirmed && s.team.stops[id].worker).map(id => s.team.stops[id].worker), failed = result.stops.filter(id => !s.team.stops[id].confirmed).map(id => s.team.stops[id].worker ?? LEAD);
|
|
244
|
+
let r;
|
|
245
|
+
if (name === 'worker_stop')
|
|
246
|
+
r = failed.length ? { kind: 'refused', reason: 'stop_not_confirmed', worker: a.worker } : { kind: 'worker_stopped', worker: a.worker };
|
|
247
|
+
else {
|
|
248
|
+
controller(s, api, name, a);
|
|
249
|
+
const remaining = await unfinished(s);
|
|
250
|
+
if (!failed.length && team.lifecycle !== 'stopped') {
|
|
251
|
+
team.lifecycle = 'stopped';
|
|
252
|
+
await event(tx, s, LEAD, 'team_stopped', { at: new Date(api.now()).toISOString() });
|
|
253
|
+
}
|
|
254
|
+
r = failed.length ? { kind: 'partial', lifecycle: 'active', stopped_workers: stopped, failed_workers: failed, unfinished_task_ids: remaining, recovery: { action: 'retry_team_shutdown' } } : { kind: 'team_shutdown', lifecycle: 'stopped', stopped_workers: stopped, unfinished_task_ids: remaining };
|
|
255
|
+
}
|
|
256
|
+
const clean = plain(checkResult(contract, name, r));
|
|
257
|
+
await saveCall(s, api.taskId, { fingerprint, result: clean });
|
|
258
|
+
return clean;
|
|
259
|
+
}, context);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
let refusal;
|
|
263
|
+
if (error instanceof Declined)
|
|
264
|
+
refusal = plain(error.result);
|
|
265
|
+
else if (error instanceof GraphControlRefusal) {
|
|
266
|
+
const reason = error.code === 'task_version_conflict' ? 'version_conflict' : error.code;
|
|
267
|
+
refusal = { kind: 'refused', operation_id: a.operation_id, ...(name === 'task_update' ? { task_id: a.task_id } : {}), reason, message: error.message };
|
|
268
|
+
}
|
|
269
|
+
else
|
|
270
|
+
throw error;
|
|
271
|
+
const clean = checkResult(contract, name, refusal);
|
|
272
|
+
// The failed transaction rolls back all mutations. Persist its model result separately.
|
|
273
|
+
if (name !== 'task_read')
|
|
274
|
+
await api.commit(async (tx) => {
|
|
275
|
+
const s = await open(tx);
|
|
276
|
+
await saveCall(s, api.taskId, { fingerprint, result: clean });
|
|
277
|
+
}, context);
|
|
278
|
+
return clean;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function tools(role) {
|
|
282
|
+
return TOOL_NAMES.filter(name => contract[role][name]).map(name => {
|
|
283
|
+
const presentation = contract.presentation[role][name];
|
|
284
|
+
if (!presentation)
|
|
285
|
+
throw new Error(`The rc.7 contract has no ${role} ${name} label and description`);
|
|
286
|
+
// pi-durable registers no label. It stays on the tool for hosts that show one, as Pi does.
|
|
287
|
+
return { ...defineTool({
|
|
288
|
+
name, description: presentation.description, parameters: contract[role][name], replay: 'safe',
|
|
289
|
+
execute: async (args, api, context) => {
|
|
290
|
+
const result = await execute(name, role, args, api, context);
|
|
291
|
+
const pending = options.settleStops && (result.kind === 'partial' || result.reason === 'stop_not_confirmed');
|
|
292
|
+
const names = result.kind === 'partial' ? result.failed_workers.map(n => n === LEAD ? 'Team delivery and notification tasks' : n).join(', ') : String(result.worker);
|
|
293
|
+
const output = [{ type: 'text', text: JSON.stringify(result) }];
|
|
294
|
+
if (pending)
|
|
295
|
+
output.push({ type: 'text', text: `Still stopping: ${names}. The stop is recorded and no new work starts. Settlement continues in the background. A later ${name} confirms completion.` });
|
|
296
|
+
return { output };
|
|
297
|
+
},
|
|
298
|
+
}), label: presentation.label };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return { lead: tools('lead'), worker: tools('worker'), execute, contract };
|
|
302
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unstable test support for hosts that exercise authority internals.
|
|
3
|
+
* It names only the internals HyperCarrier's AmPi tests import. Production code must use `./authority`.
|
|
4
|
+
* Names and behavior can change in any release without a major version.
|
|
5
|
+
*/
|
|
6
|
+
export { Deliver } from './authority/delivery.ts';
|
|
7
|
+
export { createAuthorityTools, type CoreApi } from './authority/tools.ts';
|
|
8
|
+
export { addAlert, addObligation, flushAuthority, graphCommand, obligation, setObligationState, taskRecord, type Authority } from './authority/store.ts';
|
|
9
|
+
export type { Alert, Obligation } from './authority/state.ts';
|
|
10
|
+
export { ObservationHeadDoc, PageDoc, PageBodyDoc, reserve, startObservationEpoch } from './authority/observation.ts';
|
|
11
|
+
export { advanceAcknowledged, page, retireBody, savePage } from './authority/observation-store.ts';
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unstable test support for hosts that exercise authority internals.
|
|
3
|
+
* It names only the internals HyperCarrier's AmPi tests import. Production code must use `./authority`.
|
|
4
|
+
* Names and behavior can change in any release without a major version.
|
|
5
|
+
*/
|
|
6
|
+
export { Deliver } from "./authority/delivery.js";
|
|
7
|
+
export { createAuthorityTools } from "./authority/tools.js";
|
|
8
|
+
export { addAlert, addObligation, flushAuthority, graphCommand, obligation, setObligationState, taskRecord } from "./authority/store.js";
|
|
9
|
+
export { ObservationHeadDoc, PageDoc, PageBodyDoc, reserve, startObservationEpoch } from "./authority/observation.js";
|
|
10
|
+
export { advanceAcknowledged, page, retireBody, savePage } from "./authority/observation-store.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypercarrier/pi-team-durable",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
|
+
"description": "Pi Team Bright semantics on pi-durable: one Task authority per Team, Workers as durable subthreads, accepted-then-delivered notices.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/deephbz/pi-team-durable.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/deephbz/pi-team-durable#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/deephbz/pi-team-durable/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "deephbz",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.19"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./authority": {
|
|
21
|
+
"types": "./dist/authority/index.d.ts",
|
|
22
|
+
"import": "./dist/authority/index.js",
|
|
23
|
+
"default": "./dist/authority/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./testing": {
|
|
26
|
+
"types": "./dist/testing.d.ts",
|
|
27
|
+
"import": "./dist/testing.js",
|
|
28
|
+
"default": "./dist/testing.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/authority/rc7-contract.json dist/authority/",
|
|
41
|
+
"prepare": "npm run build",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:oracle": "node test/oracle-run.ts",
|
|
45
|
+
"verify:package": "node scripts/verify-package.mjs"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"typebox": "1.3.27"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/chord": "1.4.0",
|
|
52
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
53
|
+
"@earendil-works/pi-durable": "1.4.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@earendil-works/chord": "1.4.0",
|
|
57
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
58
|
+
"@earendil-works/pi-durable": "1.4.0",
|
|
59
|
+
"@hypercarrier/pi-team-bright": "0.25.1",
|
|
60
|
+
"@types/node": "^22.19.0",
|
|
61
|
+
"jiti": "2.7.0",
|
|
62
|
+
"typescript": "6.0.3",
|
|
63
|
+
"vitest": "4.1.10"
|
|
64
|
+
}
|
|
65
|
+
}
|