@hypercarrier/pi-team-durable 0.1.0-rc.1 → 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/README.md +100 -9
- package/dist/authority/contracts.d.ts +7 -0
- package/dist/authority/contracts.js +20 -4
- package/dist/authority/delivery.d.ts +4 -3
- package/dist/authority/delivery.js +35 -29
- package/dist/authority/graph-control.d.ts +1 -1
- 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 +5 -1
- package/dist/authority/index.js +5 -1
- package/dist/authority/interrupt.js +3 -3
- package/dist/authority/notifier.d.ts +1 -1
- package/dist/authority/notifier.js +26 -20
- package/dist/authority/observation-store.d.ts +104 -0
- package/dist/authority/observation-store.js +226 -0
- package/dist/authority/observation.d.ts +3 -45
- package/dist/authority/observation.js +86 -68
- package/dist/authority/panel.d.ts +121 -0
- package/dist/authority/panel.js +44 -0
- package/dist/authority/projection.d.ts +5 -4
- package/dist/authority/projection.js +11 -11
- package/dist/authority/rc7-contract.json +57 -1
- package/dist/authority/runtime.d.ts +38 -2
- package/dist/authority/runtime.js +13 -14
- package/dist/authority/state.d.ts +19 -4
- package/dist/authority/state.js +14 -12
- package/dist/authority/store.d.ts +144 -0
- package/dist/authority/store.js +675 -0
- package/dist/authority/sync.d.ts +4 -3
- package/dist/authority/sync.js +20 -19
- package/dist/authority/tools.d.ts +41 -4
- package/dist/authority/tools.js +63 -87
- package/dist/testing.d.ts +11 -0
- package/dist/testing.js +10 -0
- package/package.json +7 -1
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export
|
|
5
|
-
export const digest = (value) => createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex');
|
|
6
|
-
export const observations = async (tx) => await tx.doc(ObservationDoc);
|
|
7
|
-
export function reservationHead(o, epoch, run) {
|
|
8
|
-
return Math.max(o.epoch === epoch ? o.acknowledged : 0, ...Object.values(o.pages).filter(p => p.epoch === epoch && p.run === run && (p.state === 'reserved' || p.state === 'presented')).map(p => p.through));
|
|
9
|
-
}
|
|
10
|
-
export function reserve(o, input, result) {
|
|
11
|
-
if (o.epoch !== input.epoch) {
|
|
12
|
-
o.epoch = input.epoch;
|
|
13
|
-
o.acknowledged = 0;
|
|
14
|
-
o.paused = false;
|
|
15
|
-
}
|
|
16
|
-
if (o.pages[input.id])
|
|
17
|
-
return o.pages[input.id];
|
|
18
|
-
const body = JSON.stringify(result), page = { ...input, body, digest: digest(body), state: 'reserved' };
|
|
19
|
-
o.pages[input.id] = page;
|
|
20
|
-
return page;
|
|
21
|
-
}
|
|
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";
|
|
22
5
|
function texts(message) {
|
|
23
6
|
const content = message.content;
|
|
24
7
|
return typeof content === 'string' ? [content] : Array.isArray(content) ? content.filter(c => c.type === 'text').map(c => c.text) : [];
|
|
25
8
|
}
|
|
26
9
|
function visible(page, messages) {
|
|
27
10
|
const carrier = page.carrier;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
19
|
}
|
|
32
20
|
export function observationExtension(resolvePort) {
|
|
33
21
|
return { name: 'ptb.observation', hooks: [hook(GenerationTask, {
|
|
@@ -37,20 +25,28 @@ export function observationExtension(resolvePort) {
|
|
|
37
25
|
if (run === undefined)
|
|
38
26
|
return;
|
|
39
27
|
await port.commit(async (tx) => {
|
|
40
|
-
const o = await
|
|
41
|
-
for (const p of
|
|
42
|
-
if (p.lead !== api.conversationId || (p.run !== run && p.run !== 0)
|
|
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))
|
|
43
31
|
continue;
|
|
44
32
|
if (visible(p, request.messages)) {
|
|
45
|
-
p.run = run;
|
|
46
33
|
ids.push(p.id);
|
|
47
|
-
p.state
|
|
34
|
+
if (p.run !== run || p.state !== 'presented') {
|
|
35
|
+
p.run = run;
|
|
36
|
+
p.state = 'presented';
|
|
37
|
+
await savePage(o, p);
|
|
38
|
+
}
|
|
48
39
|
}
|
|
49
|
-
else if (p.state === 'reserved' && p.run === run)
|
|
40
|
+
else if (p.state === 'reserved' && p.run === run) {
|
|
50
41
|
p.state = 'released';
|
|
42
|
+
await savePage(o, p);
|
|
43
|
+
}
|
|
51
44
|
}
|
|
52
45
|
// Replace on every attempt. An earlier response cannot attest a later request.
|
|
53
|
-
|
|
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));
|
|
54
50
|
}, context);
|
|
55
51
|
},
|
|
56
52
|
afterResponse: async (message, api, context) => {
|
|
@@ -60,66 +56,88 @@ export function observationExtension(resolvePort) {
|
|
|
60
56
|
}
|
|
61
57
|
export async function recordResponse(port, api, message, context) {
|
|
62
58
|
await port.commit(async (tx) => {
|
|
63
|
-
const o = await
|
|
64
|
-
if (
|
|
65
|
-
|
|
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
|
+
}
|
|
66
64
|
}, context);
|
|
67
65
|
}
|
|
68
|
-
/**
|
|
66
|
+
/** Native persistence and settled runs permit ACK. Proofs never scan transcript history. */
|
|
69
67
|
export async function observe(harness, context) {
|
|
70
|
-
const snapshot = await harness.commit(async (tx) =>
|
|
71
|
-
|
|
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]));
|
|
72
73
|
const witnessed = new Set();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!proof.responseDigest || !proof.pages.length)
|
|
74
|
+
for (const p of snapshot.proofs) {
|
|
75
|
+
if (!p.responseDigest || !p.pages.length)
|
|
76
76
|
continue;
|
|
77
|
-
const native = await harness.getTask(
|
|
77
|
+
const native = await harness.getTask(p.generation, context), state = native?.state;
|
|
78
78
|
const entryId = state?.checkpoint?.assistant ?? (state?.outcome?.status === 'completed' ? state.outcome.result?.entryId : undefined);
|
|
79
79
|
if (entryId === undefined)
|
|
80
80
|
continue;
|
|
81
81
|
const entry = await harness.commit(tx => tx.entry(entryId), context);
|
|
82
|
-
if (entry?.byTaskId ===
|
|
83
|
-
for (const id of
|
|
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
84
|
witnessed.add(id);
|
|
85
85
|
}
|
|
86
86
|
const runs = new Map();
|
|
87
|
-
for (const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const submission = await harness.submission(page.run, context), record = await submission?.status(context);
|
|
91
|
-
runs.set(page.run, record?.status === 'done' ? 'success' : record?.status === 'unanswered' ? 'failure' : 'live');
|
|
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');
|
|
92
90
|
}
|
|
93
91
|
await harness.commit(async (tx) => {
|
|
94
|
-
const o = await
|
|
95
|
-
for (const p of
|
|
96
|
-
if (
|
|
97
|
-
continue;
|
|
98
|
-
// Only unchanged persisted evidence may settle this reservation.
|
|
99
|
-
if (JSON.stringify(p) !== JSON.stringify(snapshot.pages[p.id]))
|
|
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)))
|
|
100
95
|
continue;
|
|
101
96
|
const outcome = runs.get(p.run);
|
|
102
|
-
if (outcome
|
|
97
|
+
if (outcome !== 'success' && outcome !== 'failure')
|
|
103
98
|
continue;
|
|
104
|
-
const stableProof =
|
|
105
|
-
if (outcome === 'success' && witnessed.has(p.id) && stableProof)
|
|
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)
|
|
106
101
|
p.state = 'acknowledged';
|
|
107
|
-
|
|
108
|
-
else if (outcome === 'failure' || outcome === 'success') {
|
|
102
|
+
else {
|
|
109
103
|
p.state = 'released';
|
|
110
104
|
if (outcome === 'failure')
|
|
111
|
-
o.paused = true;
|
|
105
|
+
o.head.paused = true;
|
|
112
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));
|
|
113
115
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
120
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);
|
|
121
141
|
}
|
|
122
|
-
if (s.team.current?.epoch === o.epoch)
|
|
123
|
-
s.journal.acknowledged = o.acknowledged;
|
|
124
142
|
}, context);
|
|
125
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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
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>;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/** Alert IDs carry their Team epoch; consumers do not parse that identity. */
|
|
5
|
-
export function currentAlerts(s) {
|
|
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) {
|
|
6
4
|
const epoch = s.team.current?.epoch;
|
|
7
|
-
|
|
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
8
|
}
|
|
9
|
-
export function snapshotResult(s, modelRoles, defaultModelRole) {
|
|
10
|
-
const team = s.team.current,
|
|
11
|
-
const received = currentAlerts(s
|
|
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
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:
|
|
14
|
-
tasks:
|
|
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
15
|
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
"src/model-tool-contract/catalog.ts": "99b45ac334660047f573707aa7ba1de064212f847449c5019c05b32674c385a2",
|
|
8
8
|
"src/model-tool-contract/result-projection.ts": "e5df0bc33d8617a5f749975b59f66d3b3c2b44aa0401b5b4d6a87b05452ce9cc",
|
|
9
9
|
"src/model-tool-contract/worker-parameters.ts": "46e727fdbe3c1ea22e59e8b27cd0f349a80648a8bb1656b891cc05bfe0ddc4a8",
|
|
10
|
-
"src/task-authority/graph-control-schemas.ts": "0de45ef0eaa6774289c961bfd9f661a7a92078432c0e4b2167a503d840e1e709"
|
|
10
|
+
"src/task-authority/graph-control-schemas.ts": "0de45ef0eaa6774289c961bfd9f661a7a92078432c0e4b2167a503d840e1e709",
|
|
11
|
+
"src/model-tool-contract/pi-registration.ts": "487320f04f9480e8c661d61c5c54de4ed801e83d99545107a95ce0f009223aac",
|
|
12
|
+
"extensions/index.ts": "a5e233c6d011903b664261d3b5a31ebac2f0acf5af7f26497e373442312cb36f"
|
|
11
13
|
}
|
|
12
14
|
},
|
|
13
15
|
"lead": {
|
|
@@ -9330,5 +9332,59 @@
|
|
|
9330
9332
|
}
|
|
9331
9333
|
]
|
|
9332
9334
|
}
|
|
9335
|
+
},
|
|
9336
|
+
"presentation": {
|
|
9337
|
+
"lead": {
|
|
9338
|
+
"team_create": {
|
|
9339
|
+
"label": "Create Team",
|
|
9340
|
+
"description": "Create and bind one active Team for this leader Session. Does not create Workers or Tasks."
|
|
9341
|
+
},
|
|
9342
|
+
"team_sync": {
|
|
9343
|
+
"label": "Sync Team",
|
|
9344
|
+
"description": "Return a whole-Team snapshot or incremental coordination updates. Use task_read for selected complete Task cards. Snapshots also list recent received Alerts; recent is not unresolved."
|
|
9345
|
+
},
|
|
9346
|
+
"ensure_worker": {
|
|
9347
|
+
"label": "Ensure Worker",
|
|
9348
|
+
"description": "Create or reuse a Worker for a standing area. Assign work with task_graph_apply."
|
|
9349
|
+
},
|
|
9350
|
+
"task_graph_apply": {
|
|
9351
|
+
"label": "Apply Task Graph",
|
|
9352
|
+
"description": "Atomically apply the complete assigned Task graph revision. Task authority then derives readiness and bounded failure routing."
|
|
9353
|
+
},
|
|
9354
|
+
"task_read": {
|
|
9355
|
+
"label": "Read Tasks",
|
|
9356
|
+
"description": "Read complete current cards for selected Tasks. Use team_sync for whole-Team observation."
|
|
9357
|
+
},
|
|
9358
|
+
"task_update": {
|
|
9359
|
+
"label": "Update Tasks",
|
|
9360
|
+
"description": "Apply one exact-version Task transition or context update. Waiting and ready remain derived."
|
|
9361
|
+
},
|
|
9362
|
+
"worker_stop": {
|
|
9363
|
+
"label": "Stop Worker",
|
|
9364
|
+
"description": "Stop one Worker after its nonterminal assigned Tasks resolve. Does not change Tasks."
|
|
9365
|
+
},
|
|
9366
|
+
"team_shutdown": {
|
|
9367
|
+
"label": "Shutdown Team",
|
|
9368
|
+
"description": "Stop remaining Workers and close the Team. A partial failure leaves it active for retry."
|
|
9369
|
+
},
|
|
9370
|
+
"alert_send": {
|
|
9371
|
+
"label": "Send Alert",
|
|
9372
|
+
"description": "Send exceptional Alerts to a Worker or announce to *. Accepted means stored, not presented or processed. Task state stays unchanged."
|
|
9373
|
+
}
|
|
9374
|
+
},
|
|
9375
|
+
"worker": {
|
|
9376
|
+
"task_read": {
|
|
9377
|
+
"label": "Read Task",
|
|
9378
|
+
"description": "Read current Task cards by ID from the active Team."
|
|
9379
|
+
},
|
|
9380
|
+
"task_update": {
|
|
9381
|
+
"label": "Update Task",
|
|
9382
|
+
"description": "Apply one explicit graph-native Task transition with an exact Task version. dependency_waiting and ready are derived."
|
|
9383
|
+
},
|
|
9384
|
+
"alert_send": {
|
|
9385
|
+
"label": "Send Alert",
|
|
9386
|
+
"description": "Send exceptional clarification or attention to team-lead. Accepted means stored, not presented or processed."
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9333
9389
|
}
|
|
9334
9390
|
}
|
|
@@ -29,8 +29,44 @@ export interface RuntimeOptions extends Pick<CoreOptions, 'modelRoles' | 'defaul
|
|
|
29
29
|
}
|
|
30
30
|
export declare function createTeamRuntime(options: RuntimeOptions): {
|
|
31
31
|
core: {
|
|
32
|
-
lead:
|
|
33
|
-
|
|
32
|
+
lead: {
|
|
33
|
+
label: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
parameters: import("typebox").TSchema;
|
|
37
|
+
constrainedSampling?: false | import("@earendil-works/pi-ai").ConstrainedSamplingConfig;
|
|
38
|
+
replay?: "safe" | "unsafe";
|
|
39
|
+
executionMode?: import("@earendil-works/pi-durable").ToolExecutionMode;
|
|
40
|
+
prepareArguments?(args: unknown): unknown;
|
|
41
|
+
structuredOutputSchema?: import("typebox").TSchema;
|
|
42
|
+
callers?: readonly ("model" | "tools")[];
|
|
43
|
+
deferred?: true;
|
|
44
|
+
outputLimits?: {
|
|
45
|
+
readonly maxBytes?: number;
|
|
46
|
+
readonly maxLines?: number;
|
|
47
|
+
readonly retain?: "head" | "tail";
|
|
48
|
+
};
|
|
49
|
+
execute(args: unknown, api: import("@earendil-works/pi-durable").ToolExecutionApi<import("@earendil-works/chord").JsonValue>, context: Context): Promise<import("@earendil-works/pi-durable").ToolExecutionResult<import("@earendil-works/chord").JsonValue>>;
|
|
50
|
+
}[];
|
|
51
|
+
worker: {
|
|
52
|
+
label: string;
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
parameters: import("typebox").TSchema;
|
|
56
|
+
constrainedSampling?: false | import("@earendil-works/pi-ai").ConstrainedSamplingConfig;
|
|
57
|
+
replay?: "safe" | "unsafe";
|
|
58
|
+
executionMode?: import("@earendil-works/pi-durable").ToolExecutionMode;
|
|
59
|
+
prepareArguments?(args: unknown): unknown;
|
|
60
|
+
structuredOutputSchema?: import("typebox").TSchema;
|
|
61
|
+
callers?: readonly ("model" | "tools")[];
|
|
62
|
+
deferred?: true;
|
|
63
|
+
outputLimits?: {
|
|
64
|
+
readonly maxBytes?: number;
|
|
65
|
+
readonly maxLines?: number;
|
|
66
|
+
readonly retain?: "head" | "tail";
|
|
67
|
+
};
|
|
68
|
+
execute(args: unknown, api: import("@earendil-works/pi-durable").ToolExecutionApi<import("@earendil-works/chord").JsonValue>, context: Context): Promise<import("@earendil-works/pi-durable").ToolExecutionResult<import("@earendil-works/chord").JsonValue>>;
|
|
69
|
+
}[];
|
|
34
70
|
execute: (name: import("./contracts.ts").ToolName, role: import("./contracts.ts").Role, input: unknown, api: CoreApi, context: Context) => Promise<import("./state.ts").Result>;
|
|
35
71
|
contract: import("./contracts.ts").Contract;
|
|
36
72
|
};
|
|
@@ -3,9 +3,10 @@ import { catalogOf, createAuthorityTools } from "./tools.js";
|
|
|
3
3
|
import { createAnchoredConversation, WorkerAnchor } from "./anchor.js";
|
|
4
4
|
import { createDeliver, scheduleDeliveries } from "./delivery.js";
|
|
5
5
|
import { createNotifier } from "./notifier.js";
|
|
6
|
-
import {
|
|
6
|
+
import { openObservations, startObservationEpoch, observationExtension } from "./observation.js";
|
|
7
7
|
import { sync } from "./sync.js";
|
|
8
|
-
import {
|
|
8
|
+
import { event } from "./state.js";
|
|
9
|
+
import { openAuthority as open, flushAuthority, allObligations, setObligationState } from "./store.js";
|
|
9
10
|
export const stopPlan = (id, intent) => ({ id, conversations: intent.conversations.map(String), tasks: intent.tasks.map(String) });
|
|
10
11
|
export async function pendingStops(harness, context) {
|
|
11
12
|
return harness.commit(async (tx) => Object.entries((await open(tx)).team.stops).filter(([, i]) => !i.confirmed).map(([id, i]) => stopPlan(id, i)), context);
|
|
@@ -42,19 +43,19 @@ export async function confirmStop(harness, id, outcome, context, now = Date.now)
|
|
|
42
43
|
}
|
|
43
44
|
}, context);
|
|
44
45
|
}
|
|
45
|
-
function addMechanicStops(s, shutdown) {
|
|
46
|
-
const team = s.team.current;
|
|
46
|
+
async function addMechanicStops(s, shutdown) {
|
|
47
|
+
const team = s.team.current, obligations = await allObligations(s);
|
|
47
48
|
for (const intent of Object.values(s.team.stops)) {
|
|
48
49
|
if (intent.epoch !== team.epoch || intent.confirmed)
|
|
49
50
|
continue;
|
|
50
|
-
for (const o of
|
|
51
|
+
for (const o of obligations)
|
|
51
52
|
if (o.epoch === team.epoch && o.recipient === intent.worker && o.deliveryTaskId && !intent.tasks.includes(o.deliveryTaskId))
|
|
52
53
|
intent.tasks.push(o.deliveryTaskId);
|
|
53
54
|
}
|
|
54
55
|
if (!shutdown)
|
|
55
56
|
return;
|
|
56
57
|
const key = `${team.epoch}:shutdown-mechanics`;
|
|
57
|
-
const tasks =
|
|
58
|
+
const tasks = obligations.filter(o => o.epoch === team.epoch && o.deliveryTaskId).map(o => o.deliveryTaskId);
|
|
58
59
|
if (team.notifierId)
|
|
59
60
|
tasks.push(team.notifierId);
|
|
60
61
|
s.team.stops[key] ??= { epoch: team.epoch, tasks, conversations: [], confirmed: false };
|
|
@@ -73,10 +74,11 @@ export async function closing(harness, conversationId, context) {
|
|
|
73
74
|
w.state = 'stopping';
|
|
74
75
|
s.team.stops[key] ??= { epoch: team.epoch, worker: w.name, generation: w.generation, binding: { conversationId: w.conversationId, anchorId: w.anchorId }, tasks: [w.anchorId], conversations: [w.conversationId], confirmed: false };
|
|
75
76
|
}
|
|
76
|
-
for (const o of
|
|
77
|
+
for (const o of await allObligations(s))
|
|
77
78
|
if (o.epoch === team.epoch)
|
|
78
|
-
o
|
|
79
|
-
addMechanicStops(s, true);
|
|
79
|
+
await setObligationState(s, o, 'retired');
|
|
80
|
+
await addMechanicStops(s, true);
|
|
81
|
+
flushAuthority(s);
|
|
80
82
|
return Object.entries(s.team.stops).filter(([, i]) => i.epoch === team.epoch && !i.confirmed).map(([id, i]) => stopPlan(id, i));
|
|
81
83
|
}, context);
|
|
82
84
|
}
|
|
@@ -96,15 +98,12 @@ export function createTeamRuntime(options) {
|
|
|
96
98
|
if (!team || team.lifecycle !== 'active')
|
|
97
99
|
return;
|
|
98
100
|
if (name === 'team_create') {
|
|
99
|
-
|
|
100
|
-
o.epoch = team.epoch;
|
|
101
|
-
o.acknowledged = 0;
|
|
102
|
-
o.paused = false;
|
|
101
|
+
startObservationEpoch(await openObservations(tx), team.epoch);
|
|
103
102
|
}
|
|
104
103
|
if (!team.notifierId && !team.shutdownPending)
|
|
105
104
|
team.notifierId = await tx.createTask(Notify, { epoch: team.epoch }, { conversationId: team.lead, ownership: { kind: 'conversation' }, background: true, abandonOnRestart: false });
|
|
106
105
|
await scheduleDeliveries(tx, s, Deliver);
|
|
107
|
-
const extra = addMechanicStops(s, name === 'team_shutdown');
|
|
106
|
+
const extra = name === 'worker_stop' || name === 'team_shutdown' ? await addMechanicStops(s, name === 'team_shutdown') : undefined;
|
|
108
107
|
if (extra && Array.isArray(result.stops) && !result.stops.includes(extra))
|
|
109
108
|
result.stops.push(extra);
|
|
110
109
|
}, });
|