@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/dist/authority/anchor.d.ts +10 -0
  4. package/dist/authority/anchor.js +19 -0
  5. package/dist/authority/card-types.d.ts +152 -0
  6. package/dist/authority/card-types.js +123 -0
  7. package/dist/authority/contracts.d.ts +19 -0
  8. package/dist/authority/contracts.js +45 -0
  9. package/dist/authority/delivery.d.ts +60 -0
  10. package/dist/authority/delivery.js +158 -0
  11. package/dist/authority/graph-card.d.ts +4 -0
  12. package/dist/authority/graph-card.js +39 -0
  13. package/dist/authority/graph-control.d.ts +299 -0
  14. package/dist/authority/graph-control.js +1009 -0
  15. package/dist/authority/incremental-graph-control.d.ts +156 -0
  16. package/dist/authority/incremental-graph-control.js +1051 -0
  17. package/dist/authority/index.d.ts +15 -0
  18. package/dist/authority/index.js +15 -0
  19. package/dist/authority/interrupt.d.ts +11 -0
  20. package/dist/authority/interrupt.js +26 -0
  21. package/dist/authority/limits.d.ts +7 -0
  22. package/dist/authority/limits.js +7 -0
  23. package/dist/authority/notifier.d.ts +32 -0
  24. package/dist/authority/notifier.js +89 -0
  25. package/dist/authority/observation-store.d.ts +104 -0
  26. package/dist/authority/observation-store.js +226 -0
  27. package/dist/authority/observation.d.ts +13 -0
  28. package/dist/authority/observation.js +143 -0
  29. package/dist/authority/panel.d.ts +121 -0
  30. package/dist/authority/panel.js +44 -0
  31. package/dist/authority/projection.d.ts +5 -0
  32. package/dist/authority/projection.js +15 -0
  33. package/dist/authority/rc7-contract.json +9390 -0
  34. package/dist/authority/runtime.d.ts +106 -0
  35. package/dist/authority/runtime.js +115 -0
  36. package/dist/authority/state.d.ts +165 -0
  37. package/dist/authority/state.js +28 -0
  38. package/dist/authority/store.d.ts +144 -0
  39. package/dist/authority/store.js +675 -0
  40. package/dist/authority/sync.d.ts +13 -0
  41. package/dist/authority/sync.js +78 -0
  42. package/dist/authority/task-version-ref.d.ts +3 -0
  43. package/dist/authority/task-version-ref.js +5 -0
  44. package/dist/authority/tools.d.ts +105 -0
  45. package/dist/authority/tools.js +302 -0
  46. package/dist/testing.d.ts +11 -0
  47. package/dist/testing.js +10 -0
  48. package/package.json +63 -4
@@ -0,0 +1,143 @@
1
+ import { GenerationTask, hook, LiveDoc } from '@earendil-works/pi-durable';
2
+ import { openAuthority as open, flushAuthority } from "./store.js";
3
+ import { openObservations, pages, savePage, openProofs, proof, saveProof, retireProof, retireBody, advanceAcknowledged, digest } from "./observation-store.js";
4
+ export { ObservationDoc, ObservationHeadDoc, PageDoc, PageBodyDoc, ProofDoc, observations, observationStats, openObservations, reservationHead, reserve, startObservationEpoch, digest } from "./observation-store.js";
5
+ function texts(message) {
6
+ const content = message.content;
7
+ return typeof content === 'string' ? [content] : Array.isArray(content) ? content.filter(c => c.type === 'text').map(c => c.text) : [];
8
+ }
9
+ function visible(page, messages) {
10
+ const carrier = page.carrier;
11
+ for (let i = messages.length - 1; i >= 0; i--) {
12
+ const message = messages[i];
13
+ if (carrier.kind === 'tool'
14
+ ? message.role === 'toolResult' && message.toolCallId === carrier.callId && texts(message).some(text => text === page.body)
15
+ : message.role === 'user' && texts(message).some(text => text === carrier.text))
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ export function observationExtension(resolvePort) {
21
+ return { name: 'ptb.observation', hooks: [hook(GenerationTask, {
22
+ beforeRequest: async (request, api, context) => {
23
+ const port = await resolvePort(api, context);
24
+ const live = await api.snapshot(LiveDoc, api.conversationId, context), run = live?.run?.inputs[0];
25
+ if (run === undefined)
26
+ return;
27
+ await port.commit(async (tx) => {
28
+ const o = await openObservations(tx), ids = [];
29
+ for (const p of await pages(o, 'openPages', true)) {
30
+ if (p.lead !== api.conversationId || (p.run !== run && p.run !== 0))
31
+ continue;
32
+ if (visible(p, request.messages)) {
33
+ ids.push(p.id);
34
+ if (p.run !== run || p.state !== 'presented') {
35
+ p.run = run;
36
+ p.state = 'presented';
37
+ await savePage(o, p);
38
+ }
39
+ }
40
+ else if (p.state === 'reserved' && p.run === run) {
41
+ p.state = 'released';
42
+ await savePage(o, p);
43
+ }
44
+ }
45
+ // Replace on every attempt. An earlier response cannot attest a later request.
46
+ if (ids.length)
47
+ await saveProof(o, { generation: api.taskId, lead: api.conversationId, run, pages: ids });
48
+ else
49
+ await retireProof(o, String(api.taskId));
50
+ }, context);
51
+ },
52
+ afterResponse: async (message, api, context) => {
53
+ await recordResponse(await resolvePort(api, context), api, message, context);
54
+ },
55
+ })] };
56
+ }
57
+ export async function recordResponse(port, api, message, context) {
58
+ await port.commit(async (tx) => {
59
+ const o = await openObservations(tx), p = await proof(o, String(api.taskId));
60
+ if (p && ['stop', 'length', 'toolUse'].includes(message.stopReason)) {
61
+ p.responseDigest = digest(message);
62
+ await saveProof(o, p);
63
+ }
64
+ }, context);
65
+ }
66
+ /** Native persistence and settled runs permit ACK. Proofs never scan transcript history. */
67
+ export async function observe(harness, context) {
68
+ const snapshot = await harness.commit(async (tx) => {
69
+ const o = await openObservations(tx);
70
+ return { pages: await pages(o, 'openPages'), proofs: await openProofs(o) };
71
+ }, context);
72
+ const byPage = new Map(snapshot.pages.map(p => [p.id, p])), byProof = new Map(snapshot.proofs.map(p => [String(p.generation), p]));
73
+ const witnessed = new Set();
74
+ for (const p of snapshot.proofs) {
75
+ if (!p.responseDigest || !p.pages.length)
76
+ continue;
77
+ const native = await harness.getTask(p.generation, context), state = native?.state;
78
+ const entryId = state?.checkpoint?.assistant ?? (state?.outcome?.status === 'completed' ? state.outcome.result?.entryId : undefined);
79
+ if (entryId === undefined)
80
+ continue;
81
+ const entry = await harness.commit(tx => tx.entry(entryId), context);
82
+ if (entry?.byTaskId === p.generation && entry.kind === 'pi.assistant' && entry.model?.some(m => digest(m) === p.responseDigest))
83
+ for (const id of p.pages)
84
+ witnessed.add(id);
85
+ }
86
+ const runs = new Map();
87
+ for (const run of new Set([...snapshot.pages.map(p => p.run), ...snapshot.proofs.map(p => p.run)])) {
88
+ const submission = await harness.submission(run, context), record = await submission?.status(context);
89
+ runs.set(run, record?.status === 'done' ? 'success' : record?.status === 'unanswered' ? 'failure' : 'live');
90
+ }
91
+ await harness.commit(async (tx) => {
92
+ const o = await openObservations(tx), currentProofs = await openProofs(o);
93
+ for (const p of await pages(o, 'openPages')) {
94
+ if (JSON.stringify(p) !== JSON.stringify(byPage.get(p.id)))
95
+ continue;
96
+ const outcome = runs.get(p.run);
97
+ if (outcome !== 'success' && outcome !== 'failure')
98
+ continue;
99
+ const stableProof = currentProofs.some(candidate => candidate.pages.includes(p.id) && JSON.stringify(candidate) === JSON.stringify(byProof.get(String(candidate.generation))));
100
+ if (outcome === 'success' && witnessed.has(p.id) && stableProof)
101
+ p.state = 'acknowledged';
102
+ else {
103
+ p.state = 'released';
104
+ if (outcome === 'failure')
105
+ o.head.paused = true;
106
+ }
107
+ await savePage(o, p);
108
+ }
109
+ await advanceAcknowledged(o);
110
+ // A settled, processed proof cannot attest a later request. Drop its body and index.
111
+ for (const p of currentProofs) {
112
+ const outcome = runs.get(p.run);
113
+ if ((outcome === 'success' || outcome === 'failure') && JSON.stringify(p) === JSON.stringify(byProof.get(String(p.generation))))
114
+ await retireProof(o, String(p.generation));
115
+ }
116
+ const pinned = new Set(currentProofs.filter(p => !runs.has(p.run) || runs.get(p.run) === 'live').flatMap(p => p.pages));
117
+ for (const p of await pages(o, 'prunePages')) {
118
+ if (p.through > (o.head.ackByEpoch[p.epoch] ?? 0) || pinned.has(p.id))
119
+ continue;
120
+ if (p.carrier.kind === 'input') {
121
+ // Admission replay still needs the exact input until its notifier leaves settle.
122
+ // Unknown legacy ownership cannot prove that the body is unused.
123
+ if (p.notifierId === undefined)
124
+ continue;
125
+ const native = await tx.task(p.notifierId), checkpoint = native?.state?.checkpoint;
126
+ if (!native || native.kind !== 'ptb.notify' || native.input?.epoch !== p.epoch)
127
+ continue;
128
+ if (native.state.status !== 'terminal') {
129
+ if (!checkpoint || !['tick', 'submit', 'settle'].includes(checkpoint.phase))
130
+ continue;
131
+ if (['submit', 'settle'].includes(checkpoint.phase) && checkpoint.pageId === p.id)
132
+ continue;
133
+ }
134
+ }
135
+ await retireBody(o, p);
136
+ }
137
+ const s = await open(tx);
138
+ if (s.team.current?.epoch === o.head.epoch) {
139
+ s.journal.acknowledged = o.head.acknowledged;
140
+ flushAuthority(s);
141
+ }
142
+ }, context);
143
+ }
@@ -0,0 +1,121 @@
1
+ import type { Tx } from '@earendil-works/pi-durable';
2
+ export declare function readTeamSummary(tx: Tx): Promise<{
3
+ team: import("./state.ts").TeamState;
4
+ attention: Record<string, "blocked" | "failed">;
5
+ taskCount: number;
6
+ }>;
7
+ export declare function readTeamPanel(tx: Tx, storeId: string): Promise<{
8
+ team: null;
9
+ messages?: undefined;
10
+ leadThreadId?: undefined;
11
+ graphVersion?: undefined;
12
+ tasks?: undefined;
13
+ workers?: undefined;
14
+ events?: undefined;
15
+ alerts?: undefined;
16
+ deliveries?: undefined;
17
+ } | {
18
+ messages: {
19
+ availability: "available";
20
+ source: string;
21
+ epoch: string;
22
+ limits: {
23
+ alerts: number;
24
+ deliveries: number;
25
+ events: number;
26
+ };
27
+ records: ({
28
+ status: string;
29
+ task?: string | undefined;
30
+ kind: "alert";
31
+ id: string;
32
+ at: number | null;
33
+ from: string;
34
+ to: string[];
35
+ alertKind: string;
36
+ text: string;
37
+ } | {
38
+ outcome?: string | undefined;
39
+ submissionId?: string | undefined;
40
+ settledAt?: number | undefined;
41
+ submittedAt?: number | undefined;
42
+ acceptedAt?: number | undefined;
43
+ kind: "delivery";
44
+ id: string;
45
+ at: number | null;
46
+ from: string | null;
47
+ to: string[];
48
+ deliveryKind: "task" | "stop" | "alert";
49
+ ref: string;
50
+ status: string;
51
+ conversationId: string;
52
+ generation: number;
53
+ } | {
54
+ status: string;
55
+ worker?: string | undefined;
56
+ tasks?: string[] | undefined;
57
+ task?: string | undefined;
58
+ text?: string | undefined;
59
+ kind: "lead-update";
60
+ id: string;
61
+ at: number | null;
62
+ from: string;
63
+ to: string[];
64
+ eventKind: string;
65
+ })[];
66
+ };
67
+ team: {
68
+ name: string;
69
+ purpose: string;
70
+ status: "active" | "stopped";
71
+ createdAt: number | undefined;
72
+ leadConversationId: string;
73
+ };
74
+ leadThreadId: string;
75
+ graphVersion: string;
76
+ tasks: import("./store.ts").OperatorTask[];
77
+ workers: {
78
+ name: string;
79
+ scope: string;
80
+ modelRole: string;
81
+ createdAt: number | undefined;
82
+ threadId: string;
83
+ membership: {
84
+ deactivationReason?: "stopping" | "stopped" | undefined;
85
+ id: string;
86
+ generation: number;
87
+ conversationId: string;
88
+ active: boolean;
89
+ };
90
+ }[];
91
+ events: {
92
+ detail?: string | undefined;
93
+ worker?: string | undefined;
94
+ task?: string | undefined;
95
+ actor: string;
96
+ kind: string;
97
+ ts?: number | undefined;
98
+ seq: number;
99
+ }[];
100
+ alerts: {
101
+ task_id?: string | undefined;
102
+ id: string;
103
+ ts: number;
104
+ from: string;
105
+ to: string;
106
+ kind: string;
107
+ text: string;
108
+ }[];
109
+ deliveries: {
110
+ settled?: string | undefined;
111
+ settledAt?: number | undefined;
112
+ submissionId?: string | undefined;
113
+ submittedAt?: number | undefined;
114
+ id: string;
115
+ kind: string;
116
+ ref: string;
117
+ to: string;
118
+ toConversationId: string;
119
+ acceptedAt: number | undefined;
120
+ }[];
121
+ }>;
@@ -0,0 +1,44 @@
1
+ import { plain } from "./state.js";
2
+ import { openAuthority, taskRecord, taskIds, recentEvents, obligation } from "./store.js";
3
+ import { currentAlerts } from "./projection.js";
4
+ export async function readTeamSummary(tx) {
5
+ const s = await openAuthority(tx);
6
+ return { team: plain(s.team), attention: plain(s.head.attention), taskCount: s.head.taskCount };
7
+ }
8
+ export async function readTeamPanel(tx, storeId) {
9
+ const s = await openAuthority(tx), team = s.team.current;
10
+ if (!team)
11
+ return { team: null };
12
+ const tasks = (await Promise.all((await taskIds(s)).map(async (id) => (await taskRecord(s, id)).operator)))
13
+ .map(plain).sort((a, b) => a.key.localeCompare(b.key));
14
+ const alerts = await currentAlerts(s, s.head.recentAlerts);
15
+ const deliveries = (await Promise.all(s.head.recentObligations.map(key => obligation(s, key)))).filter((o) => !!o && o.epoch === team.epoch);
16
+ const alertById = new Map(alerts.map(a => [a.id, a]));
17
+ const missing = [...new Set(deliveries.filter(o => o.kind === 'alert' && o.alertId && !alertById.has(o.alertId)).map(o => o.alertId))];
18
+ for (const a of await currentAlerts(s, missing))
19
+ alertById.set(a.id, a);
20
+ const journal = (await recentEvents(s)).filter(e => e.epoch === team.epoch).slice(-80);
21
+ const records = [
22
+ ...alerts.map((a) => ({ kind: 'alert', id: a.id, at: Number.isFinite(Date.parse(a.receivedAt)) ? Date.parse(a.receivedAt) : null, from: a.from, to: a.recipients.map(r => r.name), alertKind: a.kind, text: a.text, ...(a.task_id ? { task: a.task_id } : {}), status: 'accepted' })),
23
+ ...deliveries.map((o) => ({ kind: 'delivery', id: o.key, at: o.settledAt ?? o.submittedAt ?? o.acceptedAt ?? null,
24
+ from: o.kind === 'alert' ? (alertById.get(o.alertId ?? '')?.from ?? null) : 'authority', to: [o.recipient],
25
+ deliveryKind: o.kind, ref: o.alertId ?? o.taskId ?? '', status: o.state === 'retired' ? 'retired' : o.state === 'settled' ? 'settled' : o.submissionId !== undefined || o.submittedAt !== undefined ? 'submitted' : 'accepted',
26
+ conversationId: String(o.conversationId), generation: o.generation,
27
+ ...(o.acceptedAt !== undefined ? { acceptedAt: o.acceptedAt } : {}), ...(o.submittedAt !== undefined ? { submittedAt: o.submittedAt } : {}),
28
+ ...(o.settledAt !== undefined ? { settledAt: o.settledAt } : {}), ...(o.submissionId !== undefined ? { submissionId: String(o.submissionId) } : {}), ...(o.outcome ? { outcome: o.outcome } : {}) })),
29
+ ...journal.filter(e => ['team_created', 'team_stopped', 'worker_created', 'worker_stopped', 'graph_applied', 'task_transitioned'].includes(e.kind)).map((e) => ({
30
+ kind: 'lead-update', id: String(e.seq), at: e.at ? Date.parse(e.at) : null, from: e.actor, to: ['team-lead'], eventKind: e.kind,
31
+ ...(e.text ? { text: e.text } : {}), ...(e.taskId ? { task: e.taskId } : {}), ...(e.taskIds ? { tasks: e.taskIds } : {}), ...(e.worker ? { worker: e.worker } : {}), status: 'recorded'
32
+ })),
33
+ ];
34
+ const messages = { availability: 'available', source: `durable:${storeId}`, epoch: team.epoch, limits: { alerts: 30, deliveries: 60, events: 80 }, records };
35
+ return {
36
+ messages,
37
+ team: { name: team.name, purpose: team.purpose, status: team.lifecycle, createdAt: team.createdAt, leadConversationId: String(team.lead) },
38
+ leadThreadId: `${storeId}.${team.lead}`, graphVersion: s.head.graph?.revision?.version ?? '', tasks,
39
+ workers: Object.values(team.workers).map(w => ({ name: w.name, scope: w.scope, modelRole: w.role.name, createdAt: w.createdAt, threadId: `${storeId}.${w.conversationId}`, membership: { id: `${team.epoch}:${w.name}:g${w.generation}`, generation: w.generation, conversationId: String(w.conversationId), active: w.state === 'active', ...(w.state !== 'active' ? { deactivationReason: w.state } : {}) } })),
40
+ events: journal.map(e => ({ seq: e.seq, ...(e.at ? { ts: Date.parse(e.at) } : {}), actor: e.actor, kind: e.kind, ...(e.taskId ? { task: e.taskId } : {}), ...(e.worker ? { worker: e.worker } : {}), ...(e.text ? { detail: e.text } : {}) })),
41
+ alerts: alerts.map(a => ({ id: a.id, ts: Date.parse(a.receivedAt), from: a.from, to: a.recipients.map(r => r.name).join(', '), kind: a.kind, text: a.text, ...(a.task_id ? { task_id: a.task_id } : {}) })),
42
+ deliveries: deliveries.map(o => ({ id: o.key, kind: o.kind === 'stop' ? 'update' : o.kind, ref: o.taskId ?? o.alertId ?? '', to: o.recipient, toConversationId: String(o.conversationId), acceptedAt: o.acceptedAt, ...(o.submittedAt !== undefined ? { submittedAt: o.submittedAt } : {}), ...(o.submissionId !== undefined ? { submissionId: String(o.submissionId) } : {}), ...(o.settledAt !== undefined ? { settledAt: o.settledAt } : {}), ...(o.outcome ? { settled: o.outcome } : o.state === 'retired' && o.submissionId === undefined ? { settled: 'retired' } : {}) })),
43
+ };
44
+ }
@@ -0,0 +1,5 @@
1
+ import { type Result, type ModelRole, type Alert } from './state.ts';
2
+ import { type Authority as State } from './store.ts';
3
+ /** Read only the requested Alert records in the current Team epoch. */
4
+ export declare function currentAlerts(s: State, ids: readonly string[]): Promise<Alert[]>;
5
+ export declare function snapshotResult(s: State, modelRoles: ModelRole[], defaultModelRole?: string): Promise<Result>;
@@ -0,0 +1,15 @@
1
+ import { taskCards, alertRecord } from "./store.js";
2
+ /** Read only the requested Alert records in the current Team epoch. */
3
+ export async function currentAlerts(s, ids) {
4
+ const epoch = s.team.current?.epoch;
5
+ if (epoch === undefined)
6
+ return [];
7
+ return (await Promise.all(ids.map(id => alertRecord(s, id)))).filter((a) => !!a && a.id.startsWith(`${epoch}:`));
8
+ }
9
+ export async function snapshotResult(s, modelRoles, defaultModelRole) {
10
+ const team = s.team.current, cards = await taskCards(s);
11
+ const received = await currentAlerts(s, [...s.head.leadAlerts].reverse());
12
+ return { kind: 'snapshot', team: { name: team.name, purpose: team.purpose, lifecycle: 'active' }, model_roles: modelRoles.map(r => ({ name: r.name, use: r.use })), ...(defaultModelRole ? { default_model_role: defaultModelRole } : {}),
13
+ workers: Object.values(team.workers).map(w => ({ name: w.name, scope: w.scope, carrier: w.state === 'active' ? 'connected' : 'absent', model_role: w.role.name, nonterminal_task_ids: cards.filter(t => t.assignee === w.name && !['goal_achieved', 'goal_failed', 'cancelled'].includes(t.status)).map(t => t.id) })),
14
+ tasks: cards, alerts: received.slice(0, 10).map(a => { const chars = Array.from(a.text); return { alert_id: a.id, from: a.from, kind: a.kind, ...(a.task_id ? { task_id: a.task_id } : {}), ...(a.task_version ? { task_version: a.task_version } : {}), received_at: a.receivedAt, text: chars.slice(0, 2000).join(''), ...(chars.length > 2000 ? { truncated: true, text_length: chars.length } : {}) }; }), alerts_omitted: Math.max(0, s.head.leadAlertCount - 10) };
15
+ }