@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,60 @@
|
|
|
1
|
+
import type { AbortRun, InterruptTarget } from './interrupt.ts';
|
|
2
|
+
import { type SubmissionId, type Tx } from '@earendil-works/pi-durable';
|
|
3
|
+
import { type Obligation } from './state.ts';
|
|
4
|
+
import { type Authority as State } from './store.ts';
|
|
5
|
+
/** `kind` and `sender` name the delivery in its queue entry; inputs persisted before they existed lack them. */
|
|
6
|
+
export interface DeliveryInput extends InterruptTarget {
|
|
7
|
+
interrupt?: true;
|
|
8
|
+
epoch: string;
|
|
9
|
+
content: string;
|
|
10
|
+
whenBusy: 'steer' | 'followUp';
|
|
11
|
+
kind?: Obligation['kind'];
|
|
12
|
+
sender?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The structured data of a Team input's `team.notice` entry. A reader names the input from these fields, never from
|
|
16
|
+
* its text: `delivery` is the kind (`update` for a notifier page), `sender` the author, at most SENDER_LIMIT characters.
|
|
17
|
+
* `interrupt` marks an Alert sent to interrupt the recipient's run; the interruption outcome stays in the model-facing content.
|
|
18
|
+
* Entries written before `delivery` existed carry only `source` and `deliveryId` or `pageId`.
|
|
19
|
+
*/
|
|
20
|
+
export type TeamNoticeData = {
|
|
21
|
+
source: 'team';
|
|
22
|
+
epoch: string;
|
|
23
|
+
deliveryId?: string;
|
|
24
|
+
pageId?: string;
|
|
25
|
+
delivery?: Obligation['kind'] | 'update';
|
|
26
|
+
sender?: string;
|
|
27
|
+
interrupt?: true;
|
|
28
|
+
};
|
|
29
|
+
export declare const SENDER_LIMIT = 64;
|
|
30
|
+
export declare function eligible(s: State, o: Obligation): Promise<boolean>;
|
|
31
|
+
export declare function deliveryInput(s: State, o: Obligation): Promise<DeliveryInput>;
|
|
32
|
+
/** The input a delivery submits. The request ID names the Obligation, so a retry reacquires the same admission. */
|
|
33
|
+
export declare function deliveryDraft(input: DeliveryInput, interruption?: Obligation['interruption']): {
|
|
34
|
+
type: "input";
|
|
35
|
+
content: string;
|
|
36
|
+
whenBusy: "steer" | "followUp";
|
|
37
|
+
requestId: string;
|
|
38
|
+
entry: {
|
|
39
|
+
kind: string;
|
|
40
|
+
data: TeamNoticeData;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export declare const Deliver: import("@earendil-works/pi-durable").Task<DeliveryInput, {
|
|
44
|
+
phase: "interrupt";
|
|
45
|
+
} | {
|
|
46
|
+
phase: "submit";
|
|
47
|
+
} | {
|
|
48
|
+
phase: "settle";
|
|
49
|
+
submissionId: SubmissionId;
|
|
50
|
+
}, null, object>;
|
|
51
|
+
/** Keep the shared submit/settle mechanics; only the host-owned interruption needs a port. */
|
|
52
|
+
export declare function createDeliver(abortRun: AbortRun): import("@earendil-works/pi-durable").Task<DeliveryInput, {
|
|
53
|
+
phase: "interrupt";
|
|
54
|
+
} | {
|
|
55
|
+
phase: "submit";
|
|
56
|
+
} | {
|
|
57
|
+
phase: "settle";
|
|
58
|
+
submissionId: SubmissionId;
|
|
59
|
+
}, null, object>;
|
|
60
|
+
export declare function scheduleDeliveries(tx: Tx, s: State, delivery: typeof Deliver): Promise<void>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { withAbortSignal } from '@earendil-works/chord/context';
|
|
2
|
+
import { defineTask } from '@earendil-works/pi-durable';
|
|
3
|
+
import { plain, event } from "./state.js";
|
|
4
|
+
import { openAuthority as open, flushAuthority, markScheduled, unscheduledObligations, taskRecord, alertRecord, obligation, setObligationState } from "./store.js";
|
|
5
|
+
export const SENDER_LIMIT = 64;
|
|
6
|
+
export async function eligible(s, o) {
|
|
7
|
+
const team = s.team.current;
|
|
8
|
+
if (!team || team.epoch !== o.epoch || team.lifecycle !== 'active' || team.shutdownPending || o.state !== 'pending')
|
|
9
|
+
return false;
|
|
10
|
+
if (o.recipient === 'team-lead')
|
|
11
|
+
return team.lead === o.conversationId;
|
|
12
|
+
const worker = Object.hasOwn(team.workers, o.recipient) ? team.workers[o.recipient] : undefined;
|
|
13
|
+
if (!worker || worker.state !== 'active' || worker.generation !== o.generation || worker.conversationId !== o.conversationId)
|
|
14
|
+
return false;
|
|
15
|
+
if (o.kind !== 'task')
|
|
16
|
+
return true;
|
|
17
|
+
if (!s.head.graph || s.head.epoch !== team.epoch)
|
|
18
|
+
return false;
|
|
19
|
+
const task = (await taskRecord(s, o.taskId))?.card;
|
|
20
|
+
return !!task && task.version === o.version && task.assignee === o.recipient && task.status === 'ready';
|
|
21
|
+
}
|
|
22
|
+
export async function deliveryInput(s, o) {
|
|
23
|
+
let payload;
|
|
24
|
+
if (o.kind === 'alert')
|
|
25
|
+
payload = { kind: 'alert', alert: plain(await alertRecord(s, o.alertId)) };
|
|
26
|
+
else if (o.kind === 'stop')
|
|
27
|
+
payload = { kind: 'task_changed', task_id: o.taskId, message: 'The previous Attempt was superseded or cancelled. Read current Task authority before continuing.' };
|
|
28
|
+
else {
|
|
29
|
+
payload = { kind: 'task', task: plain((await taskRecord(s, o.taskId))?.card) };
|
|
30
|
+
}
|
|
31
|
+
// An Alert names its author; durable-team sends Tasks and stops on the Team authority's behalf, as team-lead.
|
|
32
|
+
const sender = (o.kind === 'alert' ? (await alertRecord(s, o.alertId))?.from : 'team-lead')?.slice(0, SENDER_LIMIT);
|
|
33
|
+
return { key: o.key, epoch: o.epoch, conversationId: o.conversationId, ...(o.interrupt ? { interrupt: true, ...(o.interruptRun !== undefined ? { expectedRunTaskId: o.interruptRun } : {}) } : {}), content: JSON.stringify(payload), whenBusy: o.kind === 'task' ? 'followUp' : 'steer', kind: o.kind, ...(sender ? { sender } : {}) };
|
|
34
|
+
}
|
|
35
|
+
/** The input a delivery submits. The request ID names the Obligation, so a retry reacquires the same admission. */
|
|
36
|
+
export function deliveryDraft(input, interruption) {
|
|
37
|
+
const data = { source: 'team', deliveryId: input.key, epoch: input.epoch, ...(input.kind ? { delivery: input.kind } : {}), ...(input.sender ? { sender: input.sender } : {}), ...(input.interrupt && input.kind === 'alert' ? { interrupt: true } : {}) };
|
|
38
|
+
const content = interruption ? JSON.stringify({ ...JSON.parse(input.content), interruption: { outcome: interruption, message: interruption === 'interrupted' ? 'The captured run was interrupted.' : interruption === 'ended' ? 'The captured run already ended or changed.' : 'Interruption was not proved; this Alert uses normal delivery.' } }) : input.content;
|
|
39
|
+
return { type: 'input', content, whenBusy: input.whenBusy, requestId: `ptb:${input.key}`, entry: { kind: 'team.notice', data } };
|
|
40
|
+
}
|
|
41
|
+
const draft = deliveryDraft;
|
|
42
|
+
export const Deliver = defineTask({
|
|
43
|
+
name: 'ptb.deliver', version: 1, initial: input => ({ phase: input.interrupt ? 'interrupt' : 'submit' }),
|
|
44
|
+
phases: {
|
|
45
|
+
interrupt: async () => { throw new Error('The host must supply the run-only interrupt port'); },
|
|
46
|
+
submit: async (task, runtime, context) => {
|
|
47
|
+
let valid = false, interruption;
|
|
48
|
+
await runtime.commit(async (tx) => {
|
|
49
|
+
const s = await open(tx), o = await obligation(s, task.input.key);
|
|
50
|
+
valid = !!o && await eligible(s, o);
|
|
51
|
+
interruption = o?.interruption;
|
|
52
|
+
if (!valid) {
|
|
53
|
+
if (o)
|
|
54
|
+
await setObligationState(s, o, 'retired');
|
|
55
|
+
flushAuthority(s);
|
|
56
|
+
return { status: 'terminal', outcome: { status: 'completed', result: null } };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}, context);
|
|
60
|
+
if (!valid)
|
|
61
|
+
return;
|
|
62
|
+
const target = await runtime.conversation(task.input.conversationId, context);
|
|
63
|
+
if (!target)
|
|
64
|
+
throw new Error('PTB delivery recipient is missing');
|
|
65
|
+
const submission = await target.submit(draft(task.input, interruption), context);
|
|
66
|
+
await runtime.commit(async (tx) => {
|
|
67
|
+
const s = await open(tx), o = await obligation(s, task.input.key);
|
|
68
|
+
if (!o)
|
|
69
|
+
throw new Error('PTB delivery obligation is missing');
|
|
70
|
+
o.submissionId = submission.id;
|
|
71
|
+
o.submittedAt ??= runtime.now();
|
|
72
|
+
return { status: 'running', checkpoint: { phase: 'settle', submissionId: submission.id } };
|
|
73
|
+
}, context);
|
|
74
|
+
// The settle phase rechecks revisions that raced with admission.
|
|
75
|
+
},
|
|
76
|
+
settle: async (task, runtime, context) => {
|
|
77
|
+
const target = await runtime.conversation(task.input.conversationId, context);
|
|
78
|
+
if (!target)
|
|
79
|
+
throw new Error('PTB admitted recipient is missing');
|
|
80
|
+
// Same request ID reacquires the durable admission after a process interruption.
|
|
81
|
+
let interruption;
|
|
82
|
+
await runtime.commit(async (tx) => { interruption = (await obligation(await open(tx), task.input.key))?.interruption; return undefined; }, context);
|
|
83
|
+
const submission = await target.submit(draft(task.input, interruption), context);
|
|
84
|
+
if (submission.id !== task.state.checkpoint.submissionId)
|
|
85
|
+
throw new Error('PTB admission identity changed');
|
|
86
|
+
let obsolete = false;
|
|
87
|
+
await runtime.commit(async (tx) => { const s = await open(tx), o = await obligation(s, task.input.key); obsolete = !o || !await eligible(s, o); return undefined; }, context);
|
|
88
|
+
if (obsolete)
|
|
89
|
+
await submission.abort(context);
|
|
90
|
+
const settled = await submission.wait(context);
|
|
91
|
+
await runtime.commit(async (tx) => {
|
|
92
|
+
const s = await open(tx), o = await obligation(s, task.input.key);
|
|
93
|
+
if (o) {
|
|
94
|
+
o.settledAt = runtime.now();
|
|
95
|
+
o.outcome = settled.status === 'done' ? 'answered' : `unanswered:${settled.reason}`;
|
|
96
|
+
if (o.state === 'pending')
|
|
97
|
+
await setObligationState(s, o, 'settled');
|
|
98
|
+
}
|
|
99
|
+
if (s.team.current?.epoch === task.input.epoch)
|
|
100
|
+
await event(tx, s, 'authority', 'delivery_settled', { at: new Date(runtime.now()).toISOString() });
|
|
101
|
+
return { status: 'terminal', outcome: { status: 'completed', result: null } };
|
|
102
|
+
}, context);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
abort: async (task, runtime, context) => {
|
|
106
|
+
await runtime.commit(async (tx) => { const s = await open(tx), o = await obligation(s, task.input.key); if (o) {
|
|
107
|
+
await setObligationState(s, o, 'retired');
|
|
108
|
+
o.outcome = 'stopped';
|
|
109
|
+
} flushAuthority(s); return { status: 'terminal', outcome: { status: 'aborted' } }; }, context);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
/** Keep the shared submit/settle mechanics; only the host-owned interruption needs a port. */
|
|
113
|
+
export function createDeliver(abortRun) {
|
|
114
|
+
return defineTask({ ...Deliver.definition, phases: {
|
|
115
|
+
...Deliver.definition.phases,
|
|
116
|
+
submit: (task, runtime, context) => Deliver.definition.phases.submit(task, runtime, context),
|
|
117
|
+
interrupt: async (task, runtime, context) => {
|
|
118
|
+
const bound = withAbortSignal(runtime.signal, context);
|
|
119
|
+
let outcome;
|
|
120
|
+
try {
|
|
121
|
+
outcome = await abortRun(runtime, task.input, bound);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (runtime.signal.aborted)
|
|
125
|
+
throw error;
|
|
126
|
+
runtime.report(error);
|
|
127
|
+
outcome = 'failed';
|
|
128
|
+
}
|
|
129
|
+
await runtime.commit(async (tx) => {
|
|
130
|
+
const s = await open(tx), o = await obligation(s, task.input.key);
|
|
131
|
+
if (!o || !await eligible(s, o) || outcome === 'retired') {
|
|
132
|
+
if (o)
|
|
133
|
+
await setObligationState(s, o, 'retired');
|
|
134
|
+
flushAuthority(s);
|
|
135
|
+
return { status: 'terminal', outcome: { status: 'completed', result: null } };
|
|
136
|
+
}
|
|
137
|
+
o.interruption = outcome;
|
|
138
|
+
return { status: 'running', checkpoint: { phase: 'submit' } };
|
|
139
|
+
}, bound);
|
|
140
|
+
},
|
|
141
|
+
} });
|
|
142
|
+
}
|
|
143
|
+
export async function scheduleDeliveries(tx, s, delivery) {
|
|
144
|
+
const team = s.team.current;
|
|
145
|
+
if (!team || team.lifecycle !== 'active')
|
|
146
|
+
return;
|
|
147
|
+
for (const o of await unscheduledObligations(s)) {
|
|
148
|
+
const key = o.key;
|
|
149
|
+
if (o.deliveryTaskId) {
|
|
150
|
+
await markScheduled(s, key);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!await eligible(s, o))
|
|
154
|
+
continue;
|
|
155
|
+
o.deliveryTaskId = await tx.createTask(delivery, await deliveryInput(s, o), { conversationId: team.lead, ownership: { kind: 'conversation' }, background: true, abandonOnRestart: false });
|
|
156
|
+
await markScheduled(s, key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GraphTaskReadState } from "./graph-control.ts";
|
|
2
|
+
import type { GraphTaskCard } from "./card-types.ts";
|
|
3
|
+
/** Project controller state into the public Task card. */
|
|
4
|
+
export declare function projectGraphTaskCards(controller: GraphTaskReadState): GraphTaskCard[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Project controller state into the public Task card. */
|
|
2
|
+
export function projectGraphTaskCards(controller) {
|
|
3
|
+
const attempts = controller.readAttempts();
|
|
4
|
+
return controller.readTasks().map(task => {
|
|
5
|
+
const currentAttempt = attempts.find(attempt => attempt.taskId === task.id && attempt.current);
|
|
6
|
+
const prerequisiteIds = task.state.kind === "dependency_waiting"
|
|
7
|
+
? task.state.prerequisiteTaskIds
|
|
8
|
+
: task.state.kind === "goal_failed" && "prerequisiteTaskIds" in task.state ? task.state.prerequisiteTaskIds : [];
|
|
9
|
+
const terminal = ["goal_achieved", "goal_failed", "cancelled"].includes(task.state.kind);
|
|
10
|
+
const state = task.state.kind === "dependency_waiting"
|
|
11
|
+
? { kind: "dependency_waiting", prerequisite_task_ids: [...task.state.prerequisiteTaskIds] }
|
|
12
|
+
: task.state.kind === "in_progress" ? { kind: "in_progress", attempt_id: task.state.attemptId }
|
|
13
|
+
: task.state.kind === "blocked" ? { kind: "blocked", attempt_id: task.state.attemptId, evidence: task.state.evidence }
|
|
14
|
+
: task.state.kind === "goal_achieved" ? { kind: "goal_achieved", attempt_id: task.state.attemptId }
|
|
15
|
+
: task.state.kind === "goal_failed" && task.state.reason === "criterion_failed"
|
|
16
|
+
? { kind: "goal_failed", reason: "criterion_failed", attempt_id: task.state.attemptId }
|
|
17
|
+
: task.state.kind === "goal_failed" && task.state.reason === "failure_edge_exhausted"
|
|
18
|
+
? { kind: "goal_failed", reason: "failure_edge_exhausted", attempt_id: task.state.attemptId,
|
|
19
|
+
target_task_id: task.state.targetTaskId, traversals: task.state.traversals, exhaustion_reason: task.state.exhaustionReason }
|
|
20
|
+
: task.state.kind === "goal_failed"
|
|
21
|
+
? { kind: "goal_failed", reason: task.state.reason, prerequisite_task_ids: [...task.state.prerequisiteTaskIds] }
|
|
22
|
+
: task.state.kind === "cancelled" ? { kind: "cancelled", reason: task.state.reason }
|
|
23
|
+
: { kind: "ready" };
|
|
24
|
+
return {
|
|
25
|
+
id: task.id, title: task.title, goal: task.goal, assignee: task.assignee, needs: [...task.needs],
|
|
26
|
+
...(task.onGoalFailed ? { on_goal_failed: { target: task.onGoalFailed.target, max_traversals: task.onGoalFailed.maxTraversals } } : {}),
|
|
27
|
+
status: task.state.kind, state, current_context: task.currentContext, version: task.version,
|
|
28
|
+
...(task.activationKey ? { activation_key: task.activationKey } : {}),
|
|
29
|
+
...(task.acceptedAttemptId ? { accepted_attempt_id: task.acceptedAttemptId } : {}),
|
|
30
|
+
...(currentAttempt ? { current_attempt: { id: currentAttempt.id, ordinal: currentAttempt.ordinal, input_attempt_ids: { ...currentAttempt.inputAttemptIds } } } : {}),
|
|
31
|
+
attempts_started: task.attemptsStarted,
|
|
32
|
+
relations: task.needs.map(target_task_id => ({ relation: "blocked_by", target_task_id })),
|
|
33
|
+
dependency_state: terminal || task.state.kind === "blocked"
|
|
34
|
+
? { kind: "terminal", active_blocker_ids: prerequisiteIds }
|
|
35
|
+
: prerequisiteIds.length ? { kind: "waiting", active_blocker_ids: prerequisiteIds }
|
|
36
|
+
: { kind: "ready", active_blocker_ids: [] },
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { type TaskVersionRef } from "./task-version-ref.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Executable first contract for graph-native Task control.
|
|
4
|
+
*
|
|
5
|
+
* The upstream transition design lives in Pi Team Bright's
|
|
6
|
+
* docs/projects/dag-native/graph-control-transition-spec.json.
|
|
7
|
+
* This module has no Beads, Pi, delivery, or presentation dependency.
|
|
8
|
+
*/
|
|
9
|
+
export declare const GRAPH_CONTROL_MAX_FAILURE_TRAVERSALS = 8;
|
|
10
|
+
export declare const GRAPH_CONTROL_TASK_STATES: readonly ["dependency_waiting", "ready", "in_progress", "blocked", "goal_failed", "goal_achieved", "cancelled"];
|
|
11
|
+
export type GraphVersionRef = `g_${string}`;
|
|
12
|
+
export type GraphTaskOutcome = "goal_achieved" | "goal_failed";
|
|
13
|
+
export type GraphTaskTransition = "claim" | "block" | "resume" | "goal_achieved" | "goal_failed" | "cancel";
|
|
14
|
+
export interface GraphFailureEdge {
|
|
15
|
+
target: string;
|
|
16
|
+
maxTraversals: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GraphTaskDefinitionInput {
|
|
19
|
+
key: string;
|
|
20
|
+
title: string;
|
|
21
|
+
goal: string;
|
|
22
|
+
assignee: string;
|
|
23
|
+
needs?: string[];
|
|
24
|
+
onGoalFailed?: GraphFailureEdge;
|
|
25
|
+
}
|
|
26
|
+
export interface GraphApplyInput {
|
|
27
|
+
operationId: string;
|
|
28
|
+
expectedGraphVersion?: GraphVersionRef;
|
|
29
|
+
tasks: GraphTaskDefinitionInput[];
|
|
30
|
+
}
|
|
31
|
+
export interface GraphTaskTransitionInput {
|
|
32
|
+
taskId: string;
|
|
33
|
+
operationId: string;
|
|
34
|
+
expectedVersion: TaskVersionRef;
|
|
35
|
+
transition?: GraphTaskTransition;
|
|
36
|
+
worker?: string;
|
|
37
|
+
currentContext?: string;
|
|
38
|
+
evidence?: string;
|
|
39
|
+
}
|
|
40
|
+
export type GraphGoalFailure = {
|
|
41
|
+
reason: "criterion_failed";
|
|
42
|
+
attemptId: string;
|
|
43
|
+
} | {
|
|
44
|
+
reason: "failure_edge_exhausted";
|
|
45
|
+
attemptId: string;
|
|
46
|
+
targetTaskId: string;
|
|
47
|
+
traversals: number;
|
|
48
|
+
exhaustionReason: "limit_reached" | "target_cancelled";
|
|
49
|
+
} | {
|
|
50
|
+
reason: "dependency_failed";
|
|
51
|
+
prerequisiteTaskIds: string[];
|
|
52
|
+
} | {
|
|
53
|
+
reason: "dependency_cancelled";
|
|
54
|
+
prerequisiteTaskIds: string[];
|
|
55
|
+
};
|
|
56
|
+
export type GraphTaskState = {
|
|
57
|
+
kind: "dependency_waiting";
|
|
58
|
+
prerequisiteTaskIds: string[];
|
|
59
|
+
} | {
|
|
60
|
+
kind: "ready";
|
|
61
|
+
} | {
|
|
62
|
+
kind: "in_progress";
|
|
63
|
+
attemptId: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "blocked";
|
|
66
|
+
attemptId: string;
|
|
67
|
+
evidence: string;
|
|
68
|
+
} | ({
|
|
69
|
+
kind: "goal_failed";
|
|
70
|
+
} & GraphGoalFailure) | {
|
|
71
|
+
kind: "goal_achieved";
|
|
72
|
+
attemptId: string;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "cancelled";
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
77
|
+
export interface GraphTaskView {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
goal: string;
|
|
81
|
+
assignee: string;
|
|
82
|
+
needs: string[];
|
|
83
|
+
onGoalFailed?: GraphFailureEdge;
|
|
84
|
+
state: GraphTaskState;
|
|
85
|
+
currentContext: string;
|
|
86
|
+
version: TaskVersionRef;
|
|
87
|
+
activationKey?: string;
|
|
88
|
+
acceptedAttemptId?: string;
|
|
89
|
+
attemptsStarted: number;
|
|
90
|
+
}
|
|
91
|
+
export interface GraphAttemptView {
|
|
92
|
+
id: string;
|
|
93
|
+
taskId: string;
|
|
94
|
+
ordinal: number;
|
|
95
|
+
graphVersion: GraphVersionRef;
|
|
96
|
+
taskLineage: string;
|
|
97
|
+
activationKey: string;
|
|
98
|
+
inputAttemptIds: Record<string, string>;
|
|
99
|
+
assignee: string;
|
|
100
|
+
state: "in_progress" | "blocked" | "completed" | "superseded" | "cancelled";
|
|
101
|
+
current: boolean;
|
|
102
|
+
outcome?: GraphTaskOutcome;
|
|
103
|
+
evidence?: string;
|
|
104
|
+
}
|
|
105
|
+
interface StoredTaskDefinition extends Omit<GraphTaskDefinitionInput, "needs" | "onGoalFailed"> {
|
|
106
|
+
needs: string[];
|
|
107
|
+
onGoalFailed?: GraphFailureEdge;
|
|
108
|
+
lineage: string;
|
|
109
|
+
}
|
|
110
|
+
export interface GraphRevision {
|
|
111
|
+
sequence: number;
|
|
112
|
+
version: GraphVersionRef;
|
|
113
|
+
tasks: StoredTaskDefinition[];
|
|
114
|
+
recorded_at?: string;
|
|
115
|
+
}
|
|
116
|
+
interface RecordedEvent {
|
|
117
|
+
recorded_at?: string;
|
|
118
|
+
}
|
|
119
|
+
interface AttemptStartedEvent extends RecordedEvent {
|
|
120
|
+
kind: "attempt_started";
|
|
121
|
+
sequence: number;
|
|
122
|
+
operationId: string;
|
|
123
|
+
attemptId: string;
|
|
124
|
+
taskId: string;
|
|
125
|
+
ordinal: number;
|
|
126
|
+
graphVersion: GraphVersionRef;
|
|
127
|
+
taskLineage: string;
|
|
128
|
+
activationKey: string;
|
|
129
|
+
inputAttemptIds: Record<string, string>;
|
|
130
|
+
assignee: string;
|
|
131
|
+
}
|
|
132
|
+
interface AttemptBlockedEvent extends RecordedEvent {
|
|
133
|
+
kind: "attempt_blocked";
|
|
134
|
+
sequence: number;
|
|
135
|
+
operationId: string;
|
|
136
|
+
attemptId: string;
|
|
137
|
+
evidence: string;
|
|
138
|
+
}
|
|
139
|
+
interface AttemptResumedEvent extends RecordedEvent {
|
|
140
|
+
kind: "attempt_resumed";
|
|
141
|
+
sequence: number;
|
|
142
|
+
operationId: string;
|
|
143
|
+
attemptId: string;
|
|
144
|
+
}
|
|
145
|
+
interface AttemptCompletedEvent extends RecordedEvent {
|
|
146
|
+
kind: "attempt_completed";
|
|
147
|
+
sequence: number;
|
|
148
|
+
operationId: string;
|
|
149
|
+
attemptId: string;
|
|
150
|
+
outcome: GraphTaskOutcome;
|
|
151
|
+
evidence: string;
|
|
152
|
+
}
|
|
153
|
+
interface AttemptSupersededEvent extends RecordedEvent {
|
|
154
|
+
kind: "attempt_superseded";
|
|
155
|
+
sequence: number;
|
|
156
|
+
operationId: string;
|
|
157
|
+
attemptId: string;
|
|
158
|
+
reason: "graph_revised" | "repair_requested" | "dependency_cancelled";
|
|
159
|
+
}
|
|
160
|
+
interface TaskCancelledEvent extends RecordedEvent {
|
|
161
|
+
kind: "task_cancelled";
|
|
162
|
+
sequence: number;
|
|
163
|
+
operationId: string;
|
|
164
|
+
taskId: string;
|
|
165
|
+
taskLineage: string;
|
|
166
|
+
attemptId?: string;
|
|
167
|
+
reason: string;
|
|
168
|
+
}
|
|
169
|
+
interface TaskContextUpdatedEvent extends RecordedEvent {
|
|
170
|
+
kind: "task_context_updated";
|
|
171
|
+
sequence: number;
|
|
172
|
+
operationId: string;
|
|
173
|
+
taskId: string;
|
|
174
|
+
taskLineage: string;
|
|
175
|
+
currentContext: string;
|
|
176
|
+
}
|
|
177
|
+
interface FailureEdgeTraversedEvent extends RecordedEvent {
|
|
178
|
+
kind: "failure_edge_traversed";
|
|
179
|
+
sequence: number;
|
|
180
|
+
operationId: string;
|
|
181
|
+
sourceTaskId: string;
|
|
182
|
+
sourceTaskLineage: string;
|
|
183
|
+
sourceAttemptId: string;
|
|
184
|
+
targetTaskId: string;
|
|
185
|
+
targetTaskLineage: string;
|
|
186
|
+
traversal: number;
|
|
187
|
+
}
|
|
188
|
+
interface FailureEdgeExhaustedEvent extends RecordedEvent {
|
|
189
|
+
kind: "failure_edge_exhausted";
|
|
190
|
+
sequence: number;
|
|
191
|
+
operationId: string;
|
|
192
|
+
sourceTaskId: string;
|
|
193
|
+
sourceTaskLineage: string;
|
|
194
|
+
sourceAttemptId: string;
|
|
195
|
+
targetTaskId: string;
|
|
196
|
+
traversals: number;
|
|
197
|
+
reason: "limit_reached" | "target_cancelled";
|
|
198
|
+
}
|
|
199
|
+
export type GraphControlEvent = AttemptStartedEvent | AttemptBlockedEvent | AttemptResumedEvent | AttemptCompletedEvent | AttemptSupersededEvent | TaskCancelledEvent | TaskContextUpdatedEvent | FailureEdgeTraversedEvent | FailureEdgeExhaustedEvent;
|
|
200
|
+
export interface GraphApplyResult {
|
|
201
|
+
kind: "graph_applied";
|
|
202
|
+
operationId: string;
|
|
203
|
+
graphVersion: GraphVersionRef;
|
|
204
|
+
tasks: GraphTaskView[];
|
|
205
|
+
readyTaskIds: string[];
|
|
206
|
+
}
|
|
207
|
+
export interface GraphTransitionResult {
|
|
208
|
+
kind: "task_transitioned";
|
|
209
|
+
operationId: string;
|
|
210
|
+
transition: GraphTaskTransition | "context_updated";
|
|
211
|
+
task: GraphTaskView;
|
|
212
|
+
readyTaskIds: string[];
|
|
213
|
+
failureTraversal?: {
|
|
214
|
+
sourceTaskId: string;
|
|
215
|
+
targetTaskId: string;
|
|
216
|
+
traversal: number;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export type GraphControlResult = GraphApplyResult | GraphTransitionResult;
|
|
220
|
+
export type GraphControlReceipt = GraphControlResult & {
|
|
221
|
+
replayed: boolean;
|
|
222
|
+
};
|
|
223
|
+
export interface StoredReceipt {
|
|
224
|
+
operationId: string;
|
|
225
|
+
fingerprint: string;
|
|
226
|
+
result: GraphControlResult;
|
|
227
|
+
}
|
|
228
|
+
export interface GraphControlSnapshot {
|
|
229
|
+
schema: "pi-team-bright-graph-control/1";
|
|
230
|
+
graphRevisions: GraphRevision[];
|
|
231
|
+
events: GraphControlEvent[];
|
|
232
|
+
receipts: StoredReceipt[];
|
|
233
|
+
}
|
|
234
|
+
export type GraphControlRefusalCode = "invalid_graph" | "graph_version_conflict" | "task_not_found" | "task_version_conflict" | "operation_conflict" | "invalid_transition" | "worker_mismatch" | "worker_occupied" | "evidence_required";
|
|
235
|
+
export declare class GraphControlRefusal extends Error {
|
|
236
|
+
readonly code: GraphControlRefusalCode;
|
|
237
|
+
constructor(code: GraphControlRefusalCode, message: string);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Small event-backed reference authority for deterministic Task graph control.
|
|
241
|
+
* External work is a transition command; all graph movement runs synchronously
|
|
242
|
+
* before that command returns.
|
|
243
|
+
*/
|
|
244
|
+
export declare class GraphTaskController {
|
|
245
|
+
private readonly graphRevisions;
|
|
246
|
+
private readonly events;
|
|
247
|
+
private readonly receipts;
|
|
248
|
+
private commandRecordedAt?;
|
|
249
|
+
private derivedCache?;
|
|
250
|
+
private readonly clock;
|
|
251
|
+
constructor(snapshot?: GraphControlSnapshot, clock?: () => Date);
|
|
252
|
+
static recover(snapshot: GraphControlSnapshot, clock?: () => Date): GraphTaskController;
|
|
253
|
+
snapshot(): GraphControlSnapshot;
|
|
254
|
+
currentGraphVersion(): GraphVersionRef | undefined;
|
|
255
|
+
hasOperationReceipt(operationId: string): boolean;
|
|
256
|
+
applyGraph(input: GraphApplyInput): GraphApplyResult & {
|
|
257
|
+
replayed: boolean;
|
|
258
|
+
};
|
|
259
|
+
transition(input: GraphTaskTransitionInput): GraphTransitionResult & {
|
|
260
|
+
replayed: boolean;
|
|
261
|
+
};
|
|
262
|
+
readTask(taskId: string): GraphTaskView;
|
|
263
|
+
readTasks(): GraphTaskView[];
|
|
264
|
+
readAttempts(taskId?: string): GraphAttemptView[];
|
|
265
|
+
selectReadyFrontier(): GraphTaskView[];
|
|
266
|
+
trace(): {
|
|
267
|
+
graphRevisions: GraphRevision[];
|
|
268
|
+
tasks: GraphTaskView[];
|
|
269
|
+
attempts: GraphAttemptView[];
|
|
270
|
+
events: GraphControlEvent[];
|
|
271
|
+
};
|
|
272
|
+
private claim;
|
|
273
|
+
private block;
|
|
274
|
+
private resume;
|
|
275
|
+
private complete;
|
|
276
|
+
private cancel;
|
|
277
|
+
private assertWorker;
|
|
278
|
+
private invalid;
|
|
279
|
+
private command;
|
|
280
|
+
private append;
|
|
281
|
+
private nextSequence;
|
|
282
|
+
private currentRevision;
|
|
283
|
+
private definition;
|
|
284
|
+
private attemptFolds;
|
|
285
|
+
private derivedGraph;
|
|
286
|
+
private derive;
|
|
287
|
+
private toView;
|
|
288
|
+
}
|
|
289
|
+
export type GraphTaskReadState = Pick<GraphTaskController, "readTasks" | "readAttempts" | "selectReadyFrontier">;
|
|
290
|
+
/** Read-only recovery owns revision reuse. Commit recovery always validates fully.
|
|
291
|
+
* Retain fingerprints only; changed prefixes and failures require full validation.
|
|
292
|
+
*/
|
|
293
|
+
export declare class GraphObservationRecovery {
|
|
294
|
+
private accepted?;
|
|
295
|
+
recover(snapshot: GraphControlSnapshot): GraphTaskReadState;
|
|
296
|
+
private revisionFingerprint;
|
|
297
|
+
private validateRevision;
|
|
298
|
+
}
|
|
299
|
+
export {};
|