@hypercarrier/pi-team-durable 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -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 +12 -0
- package/dist/authority/contracts.js +29 -0
- package/dist/authority/delivery.d.ts +59 -0
- package/dist/authority/delivery.js +152 -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/index.d.ts +11 -0
- package/dist/authority/index.js +11 -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 +83 -0
- package/dist/authority/observation.d.ts +55 -0
- package/dist/authority/observation.js +125 -0
- package/dist/authority/projection.d.ts +4 -0
- package/dist/authority/projection.js +15 -0
- package/dist/authority/rc7-contract.json +9334 -0
- package/dist/authority/runtime.d.ts +70 -0
- package/dist/authority/runtime.js +116 -0
- package/dist/authority/state.d.ts +150 -0
- package/dist/authority/state.js +26 -0
- package/dist/authority/sync.d.ts +12 -0
- package/dist/authority/sync.js +77 -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 +68 -0
- package/dist/authority/tools.js +326 -0
- package/package.json +57 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createTeamRuntime, pendingStops, confirmStop, closing, stopPlan, type RuntimeOptions, type StopPlan, type StopOutcome } from './runtime.ts';
|
|
2
|
+
export { observe, ObservationDoc, observations, type Observations, type Page, type ObservationPortResolver } from './observation.ts';
|
|
3
|
+
export { TeamDoc, GraphDoc, JournalDoc, OutboxDoc, ReceiptDoc, open, plain, type TeamState, type GraphState, type JournalState, type OutboxState, type Team, type Worker, type ModelRole, type ModelRoleCatalog, type State } from './state.ts';
|
|
4
|
+
export { GraphTaskController } from './graph-control.ts';
|
|
5
|
+
export { projectGraphTaskCards } from './graph-card.ts';
|
|
6
|
+
export { snapshotResult, currentAlerts } from './projection.ts';
|
|
7
|
+
export { abortExpectedRun, type AbortRun, type InterruptOutcome, type InterruptTarget } from './interrupt.ts';
|
|
8
|
+
export { TASK_ID_MAX_LENGTH } from './limits.ts';
|
|
9
|
+
export { deliveryDraft, deliveryInput, SENDER_LIMIT, type DeliveryInput, type TeamNoticeData } from './delivery.ts';
|
|
10
|
+
export { pageDraft } from './notifier.ts';
|
|
11
|
+
export { updates, pageEvents, UPDATE_PAGE_EVENTS, UPDATE_SCAN_ENTRIES } from './sync.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createTeamRuntime, pendingStops, confirmStop, closing, stopPlan } from "./runtime.js";
|
|
2
|
+
export { observe, ObservationDoc, observations } from "./observation.js";
|
|
3
|
+
export { TeamDoc, GraphDoc, JournalDoc, OutboxDoc, ReceiptDoc, open, plain } from "./state.js";
|
|
4
|
+
export { GraphTaskController } from "./graph-control.js";
|
|
5
|
+
export { projectGraphTaskCards } from "./graph-card.js";
|
|
6
|
+
export { snapshotResult, currentAlerts } from "./projection.js";
|
|
7
|
+
export { abortExpectedRun } from "./interrupt.js";
|
|
8
|
+
export { TASK_ID_MAX_LENGTH } from "./limits.js";
|
|
9
|
+
export { deliveryDraft, deliveryInput, SENDER_LIMIT } from "./delivery.js";
|
|
10
|
+
export { pageDraft } from "./notifier.js";
|
|
11
|
+
export { updates, pageEvents, UPDATE_PAGE_EVENTS, UPDATE_SCAN_ENTRIES } from "./sync.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from '@earendil-works/chord/context';
|
|
2
|
+
import { type Harness, type TaskId, type ConversationId, type DocumentReader } from '@earendil-works/pi-durable';
|
|
3
|
+
export interface InterruptTarget {
|
|
4
|
+
key: string;
|
|
5
|
+
conversationId: ConversationId;
|
|
6
|
+
expectedRunTaskId?: TaskId;
|
|
7
|
+
}
|
|
8
|
+
export type InterruptOutcome = 'interrupted' | 'ended' | 'failed' | 'retired';
|
|
9
|
+
export type AbortRun = (api: Pick<DocumentReader, 'snapshot'>, target: InterruptTarget, context: Context) => Promise<InterruptOutcome>;
|
|
10
|
+
/** Host primitive: exact native identity, cancellable wait, and no conversation-wide abort. */
|
|
11
|
+
export declare function abortExpectedRun(harness: Harness, target: InterruptTarget, context: Context): Promise<InterruptOutcome>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LiveDoc } from '@earendil-works/pi-durable';
|
|
2
|
+
import { open } from "./state.js";
|
|
3
|
+
import { eligible } from "./delivery.js";
|
|
4
|
+
/** Host primitive: exact native identity, cancellable wait, and no conversation-wide abort. */
|
|
5
|
+
export async function abortExpectedRun(harness, target, context) {
|
|
6
|
+
const check = await harness.commit(async (tx) => {
|
|
7
|
+
const s = await open(tx), o = s.outbox.obligations[target.key];
|
|
8
|
+
if (!o || !eligible(s, o) || o.conversationId !== target.conversationId || o.interruptRun !== target.expectedRunTaskId)
|
|
9
|
+
return 'retired';
|
|
10
|
+
if (target.expectedRunTaskId === undefined)
|
|
11
|
+
return 'ended';
|
|
12
|
+
const task = await tx.task(target.expectedRunTaskId), live = await tx.doc(LiveDoc, target.conversationId);
|
|
13
|
+
if (!task || task.conversationId !== target.conversationId)
|
|
14
|
+
return 'failed';
|
|
15
|
+
if (task.state.status === 'terminal')
|
|
16
|
+
return task.state.outcome.status === 'aborted' ? 'interrupted' : 'ended';
|
|
17
|
+
if (live.run?.taskId !== target.expectedRunTaskId)
|
|
18
|
+
return 'ended';
|
|
19
|
+
return 'abort';
|
|
20
|
+
}, context);
|
|
21
|
+
if (check !== 'abort')
|
|
22
|
+
return check;
|
|
23
|
+
await harness.abortTask(target.expectedRunTaskId, context);
|
|
24
|
+
const task = await harness.waitForTask(target.expectedRunTaskId, context);
|
|
25
|
+
return task.state.outcome.status === 'aborted' ? 'interrupted' : 'failed';
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
/** Static card type defaults. contracts.ts applies the configured runtime goal limit. */
|
|
3
|
+
export declare const TASK_CARD_GOAL_MAX_LENGTH = 1600;
|
|
4
|
+
export declare const TASK_ID_MAX_LENGTH = 128;
|
|
5
|
+
export declare const TASK_CARD_TITLE_MAX_LENGTH = 80;
|
|
6
|
+
export declare const TASK_CARD_CONTEXT_MAX_LENGTH = 2000;
|
|
7
|
+
export declare const TaskVersionRefSchema: Type.TString;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
/** Static card type defaults. contracts.ts applies the configured runtime goal limit. */
|
|
3
|
+
export const TASK_CARD_GOAL_MAX_LENGTH = 1_600;
|
|
4
|
+
export const TASK_ID_MAX_LENGTH = 128;
|
|
5
|
+
export const TASK_CARD_TITLE_MAX_LENGTH = 80;
|
|
6
|
+
export const TASK_CARD_CONTEXT_MAX_LENGTH = 2_000;
|
|
7
|
+
export const TaskVersionRefSchema = Type.String({ pattern: "^v_[0-9a-f]{16}$", minLength: 18, maxLength: 18 });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ConversationId, type Tx } from '@earendil-works/pi-durable';
|
|
2
|
+
import { type Page } from './observation.ts';
|
|
3
|
+
import { type ModelRoleCatalog } from './state.ts';
|
|
4
|
+
import type { TeamNoticeData } from './delivery.ts';
|
|
5
|
+
export declare const NOTIFY_RECHECK_MS = 1000;
|
|
6
|
+
export declare const NOTIFY_DELAY_MS = 30000;
|
|
7
|
+
/** The input a notifier page submits to the lead: a Team update that durable-team itself writes. */
|
|
8
|
+
export declare function pageDraft(page: Page): {
|
|
9
|
+
type: "input";
|
|
10
|
+
content: string;
|
|
11
|
+
whenBusy: "followUp";
|
|
12
|
+
requestId: string;
|
|
13
|
+
entry: {
|
|
14
|
+
kind: string;
|
|
15
|
+
data: TeamNoticeData;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function createNotifier(rolesOf: (tx: Tx, lead: ConversationId) => Promise<ModelRoleCatalog>): import("@earendil-works/pi-durable").Task<{
|
|
19
|
+
epoch: string;
|
|
20
|
+
}, {
|
|
21
|
+
phase: "tick";
|
|
22
|
+
ordinal: number;
|
|
23
|
+
due?: number;
|
|
24
|
+
} | {
|
|
25
|
+
phase: "submit";
|
|
26
|
+
pageId: string;
|
|
27
|
+
ordinal: number;
|
|
28
|
+
} | {
|
|
29
|
+
phase: "settle";
|
|
30
|
+
pageId: string;
|
|
31
|
+
ordinal: number;
|
|
32
|
+
}, null, object>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineTask, LiveDoc } from '@earendil-works/pi-durable';
|
|
2
|
+
import { observations, reserve } from "./observation.js";
|
|
3
|
+
import { open, plain } from "./state.js";
|
|
4
|
+
import { snapshotResult } from "./projection.js";
|
|
5
|
+
import { pageEvents, updates } from "./sync.js";
|
|
6
|
+
export const NOTIFY_RECHECK_MS = 1000;
|
|
7
|
+
export const NOTIFY_DELAY_MS = 30000;
|
|
8
|
+
/** The input a notifier page submits to the lead: a Team update that durable-team itself writes. */
|
|
9
|
+
export function pageDraft(page) {
|
|
10
|
+
const data = { source: 'team', pageId: page.id, epoch: page.epoch, delivery: 'update', sender: 'durable-team' };
|
|
11
|
+
return { type: 'input', content: page.carrier.kind === 'input' ? page.carrier.text : page.body, whenBusy: 'followUp', requestId: page.id, entry: { kind: 'team.notice', data } };
|
|
12
|
+
}
|
|
13
|
+
const draft = pageDraft;
|
|
14
|
+
export function createNotifier(rolesOf) {
|
|
15
|
+
return defineTask({
|
|
16
|
+
name: 'ptb.notify', version: 1, initial: () => ({ phase: 'tick', ordinal: 1 }),
|
|
17
|
+
phases: {
|
|
18
|
+
tick: async (task, runtime, context) => {
|
|
19
|
+
await runtime.commit(async (tx) => {
|
|
20
|
+
const s = await open(tx), team = s.team.current, o = await observations(tx);
|
|
21
|
+
if (!team || team.epoch !== task.input.epoch || team.lifecycle !== 'active' || team.shutdownPending)
|
|
22
|
+
return { status: 'terminal', outcome: { status: 'completed', result: null } };
|
|
23
|
+
const live = await tx.doc(LiveDoc, team.lead);
|
|
24
|
+
const pending = Object.values(o.pages).some(p => p.epoch === team.epoch && ['reserved', 'presented'].includes(p.state));
|
|
25
|
+
const checkpoint = task.state.checkpoint, due = checkpoint.due ?? runtime.now() + NOTIFY_DELAY_MS;
|
|
26
|
+
if (o.paused || pending || live.run || s.journal.seq <= o.acknowledged || runtime.now() < due)
|
|
27
|
+
return { status: 'sleeping', until: runtime.now() + NOTIFY_RECHECK_MS, checkpoint: { ...checkpoint, due } };
|
|
28
|
+
const from = o.epoch === team.epoch ? o.acknowledged : 0;
|
|
29
|
+
const previous = Object.values(o.pages).filter(p => p.epoch === team.epoch && p.state === 'acknowledged' && p.scanThrough !== undefined).sort((a, b) => Number(b.scanThrough) - Number(a.scanThrough))[0];
|
|
30
|
+
const change = from ? await pageEvents(tx, s, from, previous?.scanThrough) : undefined;
|
|
31
|
+
const roles = change ? undefined : await rolesOf(tx, team.lead);
|
|
32
|
+
const result = change ? updates(s, change.events) : snapshotResult(s, roles.modelRoles, roles.defaultModelRole);
|
|
33
|
+
const id = `${team.epoch}:notify:${runtime.taskId}:${checkpoint.ordinal}`;
|
|
34
|
+
const text = JSON.stringify({ kind: 'team_update', page_id: id, result });
|
|
35
|
+
const scanThrough = change?.scanThrough ?? s.journal.events.at(-1)?.entryId;
|
|
36
|
+
reserve(o, { id, epoch: team.epoch, lead: team.lead, run: 0, from, through: change ? change.events.at(-1)?.seq ?? from : s.journal.seq, carrier: { kind: 'input', text }, ...(scanThrough ? { scanThrough } : {}) }, result);
|
|
37
|
+
return { status: 'running', checkpoint: { phase: 'submit', pageId: id, ordinal: checkpoint.ordinal } };
|
|
38
|
+
}, context);
|
|
39
|
+
},
|
|
40
|
+
submit: async (task, runtime, context) => {
|
|
41
|
+
let page;
|
|
42
|
+
await runtime.commit(async (tx) => {
|
|
43
|
+
const s = await open(tx), o = await observations(tx), p = o.pages[task.state.checkpoint.pageId];
|
|
44
|
+
if (!p || s.team.current?.epoch !== p.epoch || s.team.current?.shutdownPending || s.team.current?.lifecycle !== 'active')
|
|
45
|
+
return { status: 'terminal', outcome: { status: 'completed', result: null } };
|
|
46
|
+
page = plain(p);
|
|
47
|
+
return undefined;
|
|
48
|
+
}, context);
|
|
49
|
+
if (!page)
|
|
50
|
+
return;
|
|
51
|
+
const target = await runtime.conversation(page.lead, context);
|
|
52
|
+
if (!target)
|
|
53
|
+
throw new Error('PTB lead is missing');
|
|
54
|
+
const submission = await target.submit(draft(page), context);
|
|
55
|
+
await runtime.commit(async (tx) => { (await observations(tx)).pages[page.id].submissionId = submission.id; return { status: 'running', checkpoint: { ...task.state.checkpoint, phase: 'settle' } }; }, context);
|
|
56
|
+
},
|
|
57
|
+
settle: async (task, runtime, context) => {
|
|
58
|
+
let page;
|
|
59
|
+
await runtime.commit(async (tx) => { page = plain((await observations(tx)).pages[task.state.checkpoint.pageId]); return undefined; }, context);
|
|
60
|
+
const target = await runtime.conversation(page.lead, context);
|
|
61
|
+
if (!target)
|
|
62
|
+
throw new Error('PTB lead is missing');
|
|
63
|
+
const submission = await target.submit(draft(page), context);
|
|
64
|
+
if (submission.id !== page.submissionId)
|
|
65
|
+
throw new Error('PTB notifier admission identity changed');
|
|
66
|
+
const settled = await submission.wait(context);
|
|
67
|
+
await runtime.commit(async (tx) => {
|
|
68
|
+
const o = await observations(tx), p = o.pages[page.id];
|
|
69
|
+
if (p.run === 0)
|
|
70
|
+
p.run = submission.id;
|
|
71
|
+
if (settled.status !== 'done') {
|
|
72
|
+
p.state = 'released';
|
|
73
|
+
o.paused = true;
|
|
74
|
+
}
|
|
75
|
+
return { status: 'sleeping', until: runtime.now() + NOTIFY_RECHECK_MS, checkpoint: { phase: 'tick', ordinal: task.state.checkpoint.ordinal + 1 } };
|
|
76
|
+
}, context);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
abort: async (task, runtime, context) => runtime.commit(async (tx) => { const o = await observations(tx); for (const p of Object.values(o.pages))
|
|
80
|
+
if (p.epoch === task.input.epoch && p.carrier.kind === 'input' && ['reserved', 'presented'].includes(p.state))
|
|
81
|
+
p.state = 'released'; return { status: 'terminal', outcome: { status: 'aborted' } }; }, context),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Context } from '@earendil-works/chord/context';
|
|
2
|
+
import type { AssistantMessage } from '@earendil-works/pi-ai';
|
|
3
|
+
import { type JsonObject, type ConversationId, type TaskId, type SubmissionId, type EntryId, type Harness, type Tx, type HookApi, type Extension } from '@earendil-works/pi-durable';
|
|
4
|
+
import { type Result } from './state.ts';
|
|
5
|
+
export interface Page {
|
|
6
|
+
id: string;
|
|
7
|
+
epoch: string;
|
|
8
|
+
lead: ConversationId;
|
|
9
|
+
run: SubmissionId;
|
|
10
|
+
from: number;
|
|
11
|
+
through: number;
|
|
12
|
+
body: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
carrier: {
|
|
15
|
+
kind: 'tool';
|
|
16
|
+
callId: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'input';
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
state: 'reserved' | 'presented' | 'released' | 'acknowledged' | 'replaced';
|
|
22
|
+
scanThrough?: EntryId;
|
|
23
|
+
submissionId?: SubmissionId;
|
|
24
|
+
}
|
|
25
|
+
interface Proof {
|
|
26
|
+
generation: TaskId;
|
|
27
|
+
lead: ConversationId;
|
|
28
|
+
run: SubmissionId;
|
|
29
|
+
requestDigest: string;
|
|
30
|
+
pages: string[];
|
|
31
|
+
responseDigest?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Observations {
|
|
34
|
+
pages: Record<string, Page>;
|
|
35
|
+
proofs: Record<string, Proof>;
|
|
36
|
+
acknowledged: number;
|
|
37
|
+
epoch?: string;
|
|
38
|
+
paused?: boolean;
|
|
39
|
+
resumedOwnerRun?: SubmissionId;
|
|
40
|
+
}
|
|
41
|
+
export declare const ObservationDoc: import("@earendil-works/pi-durable").SessionDocToken<JsonObject>;
|
|
42
|
+
export declare const digest: (value: unknown) => string;
|
|
43
|
+
export declare const observations: (tx: Tx) => Promise<Observations>;
|
|
44
|
+
export declare function reservationHead(o: Observations, epoch: string, run: SubmissionId): number;
|
|
45
|
+
export declare function reserve(o: Observations, input: Omit<Page, 'body' | 'digest' | 'state'>, result: Result): Page;
|
|
46
|
+
export interface ObservationPort {
|
|
47
|
+
commit<T>(fn: (tx: Tx) => Promise<T>, context: Context): Promise<T>;
|
|
48
|
+
}
|
|
49
|
+
/** Install last among message-replacing hooks. No hook advances the acknowledged cursor. */
|
|
50
|
+
export type ObservationPortResolver = (api: HookApi, context: Context) => Promise<ObservationPort>;
|
|
51
|
+
export declare function observationExtension(resolvePort: ObservationPortResolver): Extension;
|
|
52
|
+
export declare function recordResponse(port: ObservationPort, api: Pick<HookApi, 'taskId'>, message: AssistantMessage, context: Context): Promise<void>;
|
|
53
|
+
/** Later observer: native response persistence and run settlement are both required. */
|
|
54
|
+
export declare function observe(harness: Harness, context: Context): Promise<void>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { defineDoc, GenerationTask, hook, LiveDoc } from '@earendil-works/pi-durable';
|
|
3
|
+
import { open, plain } from "./state.js";
|
|
4
|
+
export const ObservationDoc = defineDoc({ kind: 'ptb.observation', version: 1, scope: 'session', initial: () => ({ pages: {}, proofs: {}, acknowledged: 0 }), checkpointWhen: (_v, _o, i) => i.deltasSinceBase >= 31 });
|
|
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
|
+
}
|
|
22
|
+
function texts(message) {
|
|
23
|
+
const content = message.content;
|
|
24
|
+
return typeof content === 'string' ? [content] : Array.isArray(content) ? content.filter(c => c.type === 'text').map(c => c.text) : [];
|
|
25
|
+
}
|
|
26
|
+
function visible(page, messages) {
|
|
27
|
+
const carrier = page.carrier;
|
|
28
|
+
return messages.some(message => carrier.kind === 'tool'
|
|
29
|
+
? message.role === 'toolResult' && message.toolCallId === carrier.callId && texts(message).some(text => text === page.body)
|
|
30
|
+
: message.role === 'user' && texts(message).some(text => text === carrier.text));
|
|
31
|
+
}
|
|
32
|
+
export function observationExtension(resolvePort) {
|
|
33
|
+
return { name: 'ptb.observation', hooks: [hook(GenerationTask, {
|
|
34
|
+
beforeRequest: async (request, api, context) => {
|
|
35
|
+
const port = await resolvePort(api, context);
|
|
36
|
+
const live = await api.snapshot(LiveDoc, api.conversationId, context), run = live?.run?.inputs[0];
|
|
37
|
+
if (run === undefined)
|
|
38
|
+
return;
|
|
39
|
+
await port.commit(async (tx) => {
|
|
40
|
+
const o = await observations(tx), ids = [];
|
|
41
|
+
for (const p of Object.values(o.pages)) {
|
|
42
|
+
if (p.lead !== api.conversationId || (p.run !== run && p.run !== 0) || !['reserved', 'presented'].includes(p.state))
|
|
43
|
+
continue;
|
|
44
|
+
if (visible(p, request.messages)) {
|
|
45
|
+
p.run = run;
|
|
46
|
+
ids.push(p.id);
|
|
47
|
+
p.state = 'presented';
|
|
48
|
+
}
|
|
49
|
+
else if (p.state === 'reserved' && p.run === run)
|
|
50
|
+
p.state = 'released';
|
|
51
|
+
}
|
|
52
|
+
// Replace on every attempt. An earlier response cannot attest a later request.
|
|
53
|
+
o.proofs[String(api.taskId)] = { generation: api.taskId, lead: api.conversationId, run, requestDigest: digest(request.messages), pages: ids };
|
|
54
|
+
}, context);
|
|
55
|
+
},
|
|
56
|
+
afterResponse: async (message, api, context) => {
|
|
57
|
+
await recordResponse(await resolvePort(api, context), api, message, context);
|
|
58
|
+
},
|
|
59
|
+
})] };
|
|
60
|
+
}
|
|
61
|
+
export async function recordResponse(port, api, message, context) {
|
|
62
|
+
await port.commit(async (tx) => {
|
|
63
|
+
const o = await observations(tx), proof = o.proofs[String(api.taskId)];
|
|
64
|
+
if (proof && ['stop', 'length', 'toolUse'].includes(message.stopReason))
|
|
65
|
+
proof.responseDigest = digest(message);
|
|
66
|
+
}, context);
|
|
67
|
+
}
|
|
68
|
+
/** Later observer: native response persistence and run settlement are both required. */
|
|
69
|
+
export async function observe(harness, context) {
|
|
70
|
+
const snapshot = await harness.commit(async (tx) => plain(await observations(tx)), context);
|
|
71
|
+
const proofs = Object.values(snapshot.proofs);
|
|
72
|
+
const witnessed = new Set();
|
|
73
|
+
// Generation checkpoints/results name their persisted assistant entry. No transcript scan.
|
|
74
|
+
for (const proof of proofs) {
|
|
75
|
+
if (!proof.responseDigest || !proof.pages.length)
|
|
76
|
+
continue;
|
|
77
|
+
const native = await harness.getTask(proof.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 === proof.generation && entry.kind === 'pi.assistant' && entry.model?.some(m => digest(m) === proof.responseDigest))
|
|
83
|
+
for (const id of proof.pages)
|
|
84
|
+
witnessed.add(id);
|
|
85
|
+
}
|
|
86
|
+
const runs = new Map();
|
|
87
|
+
for (const page of Object.values(snapshot.pages)) {
|
|
88
|
+
if (!['reserved', 'presented'].includes(page.state) || runs.has(page.run))
|
|
89
|
+
continue;
|
|
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');
|
|
92
|
+
}
|
|
93
|
+
await harness.commit(async (tx) => {
|
|
94
|
+
const o = await observations(tx), s = await open(tx);
|
|
95
|
+
for (const p of Object.values(o.pages)) {
|
|
96
|
+
if (!['reserved', 'presented'].includes(p.state))
|
|
97
|
+
continue;
|
|
98
|
+
// Only unchanged persisted evidence may settle this reservation.
|
|
99
|
+
if (JSON.stringify(p) !== JSON.stringify(snapshot.pages[p.id]))
|
|
100
|
+
continue;
|
|
101
|
+
const outcome = runs.get(p.run);
|
|
102
|
+
if (outcome === 'live')
|
|
103
|
+
continue;
|
|
104
|
+
const stableProof = Object.values(o.proofs).some(proof => proof.pages.includes(p.id) && JSON.stringify(proof) === JSON.stringify(snapshot.proofs[String(proof.generation)]));
|
|
105
|
+
if (outcome === 'success' && witnessed.has(p.id) && stableProof) {
|
|
106
|
+
p.state = 'acknowledged';
|
|
107
|
+
}
|
|
108
|
+
else if (outcome === 'failure' || outcome === 'success') {
|
|
109
|
+
p.state = 'released';
|
|
110
|
+
if (outcome === 'failure')
|
|
111
|
+
o.paused = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (let advanced = true; advanced;) {
|
|
115
|
+
advanced = false;
|
|
116
|
+
for (const p of Object.values(o.pages))
|
|
117
|
+
if (p.epoch === o.epoch && p.state === 'acknowledged' && p.from <= o.acknowledged && p.through > o.acknowledged) {
|
|
118
|
+
o.acknowledged = p.through;
|
|
119
|
+
advanced = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (s.team.current?.epoch === o.epoch)
|
|
123
|
+
s.journal.acknowledged = o.acknowledged;
|
|
124
|
+
}, context);
|
|
125
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type State, type Result, type ModelRole } from './state.ts';
|
|
2
|
+
/** Alert IDs carry their Team epoch; consumers do not parse that identity. */
|
|
3
|
+
export declare function currentAlerts(s: State): import("./state.ts").Alert[];
|
|
4
|
+
export declare function snapshotResult(s: State, modelRoles: ModelRole[], defaultModelRole?: string): Result;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GraphTaskController } from "./graph-control.js";
|
|
2
|
+
import { projectGraphTaskCards } from "./graph-card.js";
|
|
3
|
+
import { plain } from "./state.js";
|
|
4
|
+
/** Alert IDs carry their Team epoch; consumers do not parse that identity. */
|
|
5
|
+
export function currentAlerts(s) {
|
|
6
|
+
const epoch = s.team.current?.epoch;
|
|
7
|
+
return epoch === undefined ? [] : Object.values(s.outbox.alerts).filter(a => a.id.startsWith(`${epoch}:`));
|
|
8
|
+
}
|
|
9
|
+
export function snapshotResult(s, modelRoles, defaultModelRole) {
|
|
10
|
+
const team = s.team.current, graph = GraphTaskController.recover(plain(s.graph.snapshot));
|
|
11
|
+
const received = currentAlerts(s).filter(a => a.recipients.some(r => r.name === 'team-lead' && r.conversationId === team.lead)).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: graph.readTasks().filter(t => t.assignee === w.name && !['goal_achieved', 'goal_failed', 'cancelled'].includes(t.state.kind)).map(t => t.id) })),
|
|
14
|
+
tasks: projectGraphTaskCards(graph), 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, received.length - 10) };
|
|
15
|
+
}
|