@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -2
  3. package/dist/authority/anchor.d.ts +10 -0
  4. package/dist/authority/anchor.js +19 -0
  5. package/dist/authority/card-types.d.ts +152 -0
  6. package/dist/authority/card-types.js +123 -0
  7. package/dist/authority/contracts.d.ts +12 -0
  8. package/dist/authority/contracts.js +29 -0
  9. package/dist/authority/delivery.d.ts +59 -0
  10. package/dist/authority/delivery.js +152 -0
  11. package/dist/authority/graph-card.d.ts +4 -0
  12. package/dist/authority/graph-card.js +39 -0
  13. package/dist/authority/graph-control.d.ts +299 -0
  14. package/dist/authority/graph-control.js +1009 -0
  15. package/dist/authority/index.d.ts +11 -0
  16. package/dist/authority/index.js +11 -0
  17. package/dist/authority/interrupt.d.ts +11 -0
  18. package/dist/authority/interrupt.js +26 -0
  19. package/dist/authority/limits.d.ts +7 -0
  20. package/dist/authority/limits.js +7 -0
  21. package/dist/authority/notifier.d.ts +32 -0
  22. package/dist/authority/notifier.js +83 -0
  23. package/dist/authority/observation.d.ts +55 -0
  24. package/dist/authority/observation.js +125 -0
  25. package/dist/authority/projection.d.ts +4 -0
  26. package/dist/authority/projection.js +15 -0
  27. package/dist/authority/rc7-contract.json +9334 -0
  28. package/dist/authority/runtime.d.ts +70 -0
  29. package/dist/authority/runtime.js +116 -0
  30. package/dist/authority/state.d.ts +150 -0
  31. package/dist/authority/state.js +26 -0
  32. package/dist/authority/sync.d.ts +12 -0
  33. package/dist/authority/sync.js +77 -0
  34. package/dist/authority/task-version-ref.d.ts +3 -0
  35. package/dist/authority/task-version-ref.js +5 -0
  36. package/dist/authority/tools.d.ts +68 -0
  37. package/dist/authority/tools.js +326 -0
  38. package/package.json +57 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deephbz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,119 @@
1
- # Temporary Holding Version
1
+ # @hypercarrier/pi-team-durable
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pi Team Bright semantics on [pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable):
4
+ one Task authority per Team, Workers as durable subthreads, accepted-then-delivered
5
+ notices.
6
+
7
+ ## Status
8
+
9
+ - **Prerelease.** The npm `next` tag carries it. This package has its own version
10
+ line, independent of Pi Team Bright. Pi Team Bright 0.25.1 is its contract pin.
11
+ - **No standalone runtime.** The package needs a host that supplies the Team ports:
12
+ model roles or a `catalog`, `observationPort`, `abortRun` and `stop`. The runtime
13
+ builds Worker creation and stop settlement from them. Today the host is the AmPi
14
+ node.
15
+ - **Exact peers.** The peer dependencies pin `@earendil-works/pi-durable`,
16
+ `@earendil-works/pi-ai` and `@earendil-works/chord` to 1.4.0. Other versions give
17
+ a peer conflict.
18
+ - **First version without provenance.** The first version was published by hand.
19
+ Later versions publish from CI with npm provenance.
20
+ - **Protocol value.** The persisted notice value `sender: 'durable-team'` is a
21
+ protocol value from the package's former name. It stays unchanged.
22
+ - **Tests here.** CI runs the unit suites.
23
+ The SIGKILL crash suite and `bench/e2e.ts` stay in HyperCarrier (private).
24
+ - **Store format.** This version stores version-1 documents. The next version
25
+ migrates them to version 2, and a downgrade cannot read a migrated store.
26
+
27
+ Known issues:
28
+
29
+ 1. An Alert is lost when the recipient's model errors. It shows as unanswered. Pi
30
+ Team Bright retries it; this package does not.
31
+ 2. `team_sync` can replay pages that an earlier call released. Act only on
32
+ `task_read`, and never call `team_sync` from a nested tool call.
33
+ 3. Interrupt: a Worker can interrupt the lead, the run to stop is chosen when the
34
+ Alert is accepted, and the abort has no settle bound.
35
+ 4. An Alert can be admitted after its recipient is closed or fenced. After an owner
36
+ Close, a busy Worker can still run one tool call.
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ npm install @hypercarrier/pi-team-durable@next
42
+ ```
43
+
44
+ The package ships compiled JavaScript and type declarations under `dist`.
45
+ Node 22.19 or newer. Install the three pinned peers beside it.
46
+
47
+ ## API
48
+
49
+ `./authority` exports the runtime factory, authority documents, observation
50
+ observer, and structural stop ports. A host adapter registers this runtime.
51
+
52
+ - `ptb.team`, `ptb.graph`, `ptb.journal`, `ptb.outbox`, and `ptb.receipts` store
53
+ authority, graph history, recent events, delivery obligations, and replay results.
54
+ Each accepted command and its mechanics commit together.
55
+ - Nine lead tools and three Worker tools use the rc.7 contract. Exact conversation
56
+ and generation bindings guard authority. A fork does not acquire an existing
57
+ Team's lead authority.
58
+ - `ptb.worker-anchor` owns a Worker conversation and renews a durable sleep.
59
+ Lead Esc does not end it. Stops use stored target IDs through the node's single
60
+ mark-before-resume fence. Failed confirmation retains the intent.
61
+ - `ptb.deliver` rechecks an obligation, submits with a stable request ID, records
62
+ admission, and waits for settlement. Notices use `team.notice` entries whose
63
+ data (`TeamNoticeData`) names the delivery kind and its sender: an Alert's
64
+ author, or `team-lead` for Tasks and stops. An interrupt Alert's data also
65
+ has `interrupt: true`. A reader names a notice from this data, never from its
66
+ text.
67
+ Interrupt Alerts persist the recipient run ID at acceptance. A separate phase
68
+ asks the host `abortRun` port to stop that exact run, then checkpoints before
69
+ normal admission. Queue identities stay native; no input is re-queued.
70
+ A failed interruption appears as unproved in the notice. Retry never selects
71
+ a replacement run. The node supplies a cancellable wait outside its admission lock.
72
+ - `ptb.notify` reserves an automatic page before admission. A failed lead input
73
+ pauses notification. Explicit sync or a new owner input resumes it.
74
+ - `team_sync` reserves successive bounded pages. It never waits for consumption.
75
+ The final request hook records exact page visibility. A later observer requires
76
+ a matching persisted native assistant entry and successful input settlement
77
+ before it advances the cursor. Compaction and failed runs release unseen pages.
78
+ - The journal keeps 100 recent events in its document and immutable `ptb.event`
79
+ entries in native storage. This bound does not cover graph history, receipts,
80
+ outbox, observation proofs, or storage history.
81
+
82
+ The host injects model roles: a static `modelRoles` list, or a `catalog(tx,
83
+ lead)` resolver that gives each lead its own roles and default. The AmPi
84
+ node's `default` role captures the authoritative lead model at each Worker
85
+ generation. Unknown roles return a typed refusal.
86
+ The package never reads Pi configuration. The observation commit port resolves
87
+ its store for every hook; a registry can serve multiple stores.
88
+
89
+ The executable contract snapshot comes from Pi Team Bright 0.24.0-rc.7 at
90
+ `382950ce62bd38b6cd9f7fb80b91ef5b9140e15a` (unreleased Alert interrupt support).
91
+ `scripts/rc7-contract.mjs` records source hashes. Pi Team Bright 0.25.1 is the
92
+ contract pin: it ships the same source files, and a test compares their SHA-256
93
+ values with the recorded ones. The tests regenerate the contract from the
94
+ installed devDependency `@hypercarrier/pi-team-bright@0.25.1`.
95
+
96
+ ```sh
97
+ npm ci
98
+ npm test
99
+ ```
100
+
101
+ Fast tests use MemoryStorage, a fixed clock, inert timers, and faux responses.
102
+ The SIGKILL crash-recovery suite and `bench/e2e.ts` stay in HyperCarrier (private).
103
+ They use the AmPi node's subprocess carrier. This repository's tests do not
104
+ cover crash recovery.
105
+
106
+ The C1 delivery hold remains unported. pi-durable places steers and follow-ups at
107
+ turn boundaries. A stale admitted notice cannot grant authority: Task version and
108
+ Worker generation checks still apply.
109
+
110
+ ## Development
111
+
112
+ `npm run build` emits `dist`; `npm install` runs it through `prepare`.
113
+ `npm run typecheck`, `npm test` and `npm run verify:package` are the CI checks.
114
+ See [releasing](docs/maintainers/releasing.md) for the release procedure.
115
+
116
+ ## Provenance
117
+
118
+ Extracted from HyperCarrier (private) at the certified port, renamed from
119
+ `durable-team`, without its history.
@@ -0,0 +1,10 @@
1
+ import { type ConversationId, type TaskId, type Tx } from '@earendil-works/pi-durable';
2
+ export declare const ANCHOR_SLEEP_MS: number;
3
+ export declare const WorkerAnchor: import("@earendil-works/pi-durable").Task<null, {
4
+ phase: "hold";
5
+ }, null, object>;
6
+ /** The caller commits this binding with Membership in the same transaction. */
7
+ export declare function createAnchoredConversation(tx: Tx, lead: ConversationId): Promise<{
8
+ conversationId: ConversationId;
9
+ anchorId: TaskId;
10
+ }>;
@@ -0,0 +1,19 @@
1
+ import { defineTask } from '@earendil-works/pi-durable';
2
+ // A live background root owns the Worker conversation. A terminal placeholder
3
+ // cannot be an abort boundary because abortTask returns early for terminal tasks.
4
+ export const ANCHOR_SLEEP_MS = 24 * 60 * 60 * 1000;
5
+ export const WorkerAnchor = defineTask({
6
+ name: 'ptb.worker-anchor', version: 1, initial: () => ({ phase: 'hold' }),
7
+ phases: { hold: async (_task, runtime, context) => {
8
+ await runtime.sleepUntil(runtime.now() + ANCHOR_SLEEP_MS, { phase: 'hold' }, context);
9
+ } },
10
+ abort: async (_task, runtime, context) => {
11
+ await runtime.commit(() => ({ status: 'terminal', outcome: { status: 'aborted' } }), context);
12
+ },
13
+ });
14
+ /** The caller commits this binding with Membership in the same transaction. */
15
+ export async function createAnchoredConversation(tx, lead) {
16
+ const anchorId = await tx.createTask(WorkerAnchor, null, { conversationId: lead, ownership: { kind: 'conversation' }, background: true, abandonOnRestart: false });
17
+ const conversation = await tx.createConversation({ ownership: { kind: 'task', taskId: anchorId } });
18
+ return { conversationId: conversation.id, anchorId };
19
+ }
@@ -0,0 +1,152 @@
1
+ import { Type, type Static } from "typebox";
2
+ export declare const GraphVersionRefSchema: Type.TString;
3
+ export declare const GraphTaskTransitionSchema: Type.TEnum<["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]>;
4
+ export declare const GraphFailureEdgeSchema: Type.TObject<{
5
+ target: Type.TString;
6
+ max_traversals: Type.TInteger;
7
+ }>;
8
+ export declare const GraphTaskStateSchema: Type.TUnion<[Type.TObject<{
9
+ kind: Type.TLiteral<"dependency_waiting">;
10
+ prerequisite_task_ids: Type.TArray<Type.TString>;
11
+ }>, Type.TObject<{
12
+ kind: Type.TLiteral<"ready">;
13
+ }>, Type.TObject<{
14
+ kind: Type.TLiteral<"in_progress">;
15
+ attempt_id: Type.TString;
16
+ }>, Type.TObject<{
17
+ kind: Type.TLiteral<"blocked">;
18
+ attempt_id: Type.TString;
19
+ evidence: Type.TString;
20
+ }>, Type.TObject<{
21
+ kind: Type.TLiteral<"goal_achieved">;
22
+ attempt_id: Type.TString;
23
+ }>, Type.TObject<{
24
+ kind: Type.TLiteral<"goal_failed">;
25
+ reason: Type.TLiteral<"criterion_failed">;
26
+ attempt_id: Type.TString;
27
+ }>, Type.TObject<{
28
+ kind: Type.TLiteral<"goal_failed">;
29
+ reason: Type.TLiteral<"failure_edge_exhausted">;
30
+ attempt_id: Type.TString;
31
+ target_task_id: Type.TString;
32
+ traversals: Type.TInteger;
33
+ exhaustion_reason: Type.TEnum<["limit_reached", "target_cancelled"]>;
34
+ }>, Type.TObject<{
35
+ kind: Type.TLiteral<"goal_failed">;
36
+ reason: Type.TEnum<["dependency_failed", "dependency_cancelled"]>;
37
+ prerequisite_task_ids: Type.TArray<Type.TString>;
38
+ }>, Type.TObject<{
39
+ kind: Type.TLiteral<"cancelled">;
40
+ reason: Type.TString;
41
+ }>]>;
42
+ export declare const GraphAttemptSummarySchema: Type.TObject<{
43
+ id: Type.TString;
44
+ ordinal: Type.TInteger;
45
+ input_attempt_ids: Type.TRecord<"^.*$", Type.TString>;
46
+ }>;
47
+ /** Canonical graph-native Task card used by tools, delivery, and Coordination. */
48
+ export declare const GraphTaskCardSchema: Type.TObject<{
49
+ id: Type.TString;
50
+ title: Type.TString;
51
+ goal: Type.TString;
52
+ assignee: Type.TString;
53
+ needs: Type.TArray<Type.TString>;
54
+ on_goal_failed: Type.TOptional<Type.TObject<{
55
+ target: Type.TString;
56
+ max_traversals: Type.TInteger;
57
+ }>>;
58
+ status: Type.TEnum<["dependency_waiting", "ready", "in_progress", "blocked", "goal_failed", "goal_achieved", "cancelled"]>;
59
+ state: Type.TUnion<[Type.TObject<{
60
+ kind: Type.TLiteral<"dependency_waiting">;
61
+ prerequisite_task_ids: Type.TArray<Type.TString>;
62
+ }>, Type.TObject<{
63
+ kind: Type.TLiteral<"ready">;
64
+ }>, Type.TObject<{
65
+ kind: Type.TLiteral<"in_progress">;
66
+ attempt_id: Type.TString;
67
+ }>, Type.TObject<{
68
+ kind: Type.TLiteral<"blocked">;
69
+ attempt_id: Type.TString;
70
+ evidence: Type.TString;
71
+ }>, Type.TObject<{
72
+ kind: Type.TLiteral<"goal_achieved">;
73
+ attempt_id: Type.TString;
74
+ }>, Type.TObject<{
75
+ kind: Type.TLiteral<"goal_failed">;
76
+ reason: Type.TLiteral<"criterion_failed">;
77
+ attempt_id: Type.TString;
78
+ }>, Type.TObject<{
79
+ kind: Type.TLiteral<"goal_failed">;
80
+ reason: Type.TLiteral<"failure_edge_exhausted">;
81
+ attempt_id: Type.TString;
82
+ target_task_id: Type.TString;
83
+ traversals: Type.TInteger;
84
+ exhaustion_reason: Type.TEnum<["limit_reached", "target_cancelled"]>;
85
+ }>, Type.TObject<{
86
+ kind: Type.TLiteral<"goal_failed">;
87
+ reason: Type.TEnum<["dependency_failed", "dependency_cancelled"]>;
88
+ prerequisite_task_ids: Type.TArray<Type.TString>;
89
+ }>, Type.TObject<{
90
+ kind: Type.TLiteral<"cancelled">;
91
+ reason: Type.TString;
92
+ }>]>;
93
+ current_context: Type.TString;
94
+ version: Type.TString;
95
+ activation_key: Type.TOptional<Type.TString>;
96
+ accepted_attempt_id: Type.TOptional<Type.TString>;
97
+ current_attempt: Type.TOptional<Type.TObject<{
98
+ id: Type.TString;
99
+ ordinal: Type.TInteger;
100
+ input_attempt_ids: Type.TRecord<"^.*$", Type.TString>;
101
+ }>>;
102
+ attempts_started: Type.TInteger;
103
+ relations: Type.TOptional<Type.TArray<Type.TObject<{
104
+ relation: Type.TEnum<["blocked_by", "parent", "related"]>;
105
+ target_task_id: Type.TString;
106
+ }>>>;
107
+ dependency_state: Type.TOptional<Type.TUnion<[Type.TObject<{
108
+ kind: Type.TLiteral<"ready">;
109
+ active_blocker_ids: Type.TArray<Type.TString>;
110
+ }>, Type.TObject<{
111
+ kind: Type.TLiteral<"waiting">;
112
+ active_blocker_ids: Type.TArray<Type.TString>;
113
+ }>, Type.TObject<{
114
+ kind: Type.TLiteral<"terminal">;
115
+ active_blocker_ids: Type.TArray<Type.TString>;
116
+ }>]>>;
117
+ projection_warnings: Type.TOptional<Type.TArray<Type.TObject<{
118
+ task_id: Type.TString;
119
+ truncated_fields: Type.TArray<Type.TEnum<["title", "goal", "current_context"]>>;
120
+ incomplete_fields: Type.TArray<Type.TEnum<["title", "goal", "current_context"]>>;
121
+ message: Type.TString;
122
+ }>>>;
123
+ }>;
124
+ /** Breaking replacement for task_create. It applies one complete graph revision. */
125
+ export declare const TaskGraphApplyParametersSchema: Type.TObject<{
126
+ operation_id: Type.TString;
127
+ expected_graph_version: Type.TOptional<Type.TString>;
128
+ tasks: Type.TArray<Type.TObject<{
129
+ key: Type.TString;
130
+ title: Type.TString;
131
+ goal: Type.TString;
132
+ assignee: Type.TString;
133
+ needs: Type.TOptional<Type.TArray<Type.TString>>;
134
+ on_goal_failed: Type.TOptional<Type.TObject<{
135
+ target: Type.TString;
136
+ max_traversals: Type.TInteger;
137
+ }>>;
138
+ }>>;
139
+ }>;
140
+ /** One graph-native Task command. Waiting and ready are derived, never authored. */
141
+ export declare const GraphTaskUpdateParametersSchema: Type.TObject<{
142
+ task_id: Type.TString;
143
+ operation_id: Type.TString;
144
+ expected_version: Type.TString;
145
+ transition: Type.TOptional<Type.TEnum<["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]>>;
146
+ current_context: Type.TOptional<Type.TString>;
147
+ evidence: Type.TOptional<Type.TString>;
148
+ }>;
149
+ export type GraphTaskCard = Static<typeof GraphTaskCardSchema>;
150
+ export type GraphTaskStateCard = Static<typeof GraphTaskStateSchema>;
151
+ export type TaskGraphApplyParameters = Static<typeof TaskGraphApplyParametersSchema>;
152
+ export type GraphTaskUpdateParameters = Static<typeof GraphTaskUpdateParametersSchema>;
@@ -0,0 +1,123 @@
1
+ // Ported from Pi Team Bright 0.24.0-rc.4 src/task-authority/graph-control-schemas.ts (MIT). Limits inlined.
2
+ import { Type } from "typebox";
3
+ import { TaskVersionRefSchema } from "./limits.js";
4
+ import { TASK_CARD_GOAL_MAX_LENGTH } from "./limits.js";
5
+ import { TASK_CARD_CONTEXT_MAX_LENGTH, TASK_CARD_TITLE_MAX_LENGTH, TASK_ID_MAX_LENGTH } from "./limits.js";
6
+ const TaskId = Type.String({ minLength: 1, maxLength: TASK_ID_MAX_LENGTH });
7
+ const TaskKey = Type.String({ minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9_-]+$" });
8
+ const WorkerName = Type.String({ minLength: 1, maxLength: 64 });
9
+ const OperationId = Type.String({ minLength: 1, maxLength: 128 });
10
+ export const GraphVersionRefSchema = Type.String({ pattern: "^g_[0-9a-f]{16}$", minLength: 18, maxLength: 18 });
11
+ export const GraphTaskTransitionSchema = Type.Enum(["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]);
12
+ export const GraphFailureEdgeSchema = Type.Object({
13
+ target: TaskKey,
14
+ max_traversals: Type.Integer({ minimum: 1, maximum: 8 }),
15
+ }, { additionalProperties: false });
16
+ export const GraphTaskStateSchema = Type.Union([
17
+ Type.Object({
18
+ kind: Type.Literal("dependency_waiting"),
19
+ prerequisite_task_ids: Type.Array(TaskId, { minItems: 1 }),
20
+ }, { additionalProperties: false }),
21
+ Type.Object({ kind: Type.Literal("ready") }, { additionalProperties: false }),
22
+ Type.Object({ kind: Type.Literal("in_progress"), attempt_id: Type.String({ minLength: 1 }) }, { additionalProperties: false }),
23
+ Type.Object({
24
+ kind: Type.Literal("blocked"),
25
+ attempt_id: Type.String({ minLength: 1 }),
26
+ evidence: Type.String({ minLength: 1 }),
27
+ }, { additionalProperties: false }),
28
+ Type.Object({
29
+ kind: Type.Literal("goal_achieved"),
30
+ attempt_id: Type.String({ minLength: 1 }),
31
+ }, { additionalProperties: false }),
32
+ Type.Object({
33
+ kind: Type.Literal("goal_failed"),
34
+ reason: Type.Literal("criterion_failed"),
35
+ attempt_id: Type.String({ minLength: 1 }),
36
+ }, { additionalProperties: false }),
37
+ Type.Object({
38
+ kind: Type.Literal("goal_failed"),
39
+ reason: Type.Literal("failure_edge_exhausted"),
40
+ attempt_id: Type.String({ minLength: 1 }),
41
+ target_task_id: TaskId,
42
+ traversals: Type.Integer({ minimum: 0 }),
43
+ exhaustion_reason: Type.Enum(["limit_reached", "target_cancelled"]),
44
+ }, { additionalProperties: false }),
45
+ Type.Object({
46
+ kind: Type.Literal("goal_failed"),
47
+ reason: Type.Enum(["dependency_failed", "dependency_cancelled"]),
48
+ prerequisite_task_ids: Type.Array(TaskId, { minItems: 1 }),
49
+ }, { additionalProperties: false }),
50
+ Type.Object({
51
+ kind: Type.Literal("cancelled"),
52
+ reason: Type.String({ minLength: 1 }),
53
+ }, { additionalProperties: false }),
54
+ ]);
55
+ export const GraphAttemptSummarySchema = Type.Object({
56
+ id: Type.String({ minLength: 1 }),
57
+ ordinal: Type.Integer({ minimum: 1 }),
58
+ input_attempt_ids: Type.Record(TaskId, Type.String({ minLength: 1 })),
59
+ }, { additionalProperties: false });
60
+ /** Canonical graph-native Task card used by tools, delivery, and Coordination. */
61
+ export const GraphTaskCardSchema = Type.Object({
62
+ id: TaskId,
63
+ title: Type.String({ minLength: 1, maxLength: TASK_CARD_TITLE_MAX_LENGTH }),
64
+ goal: Type.String({ minLength: 1, maxLength: TASK_CARD_GOAL_MAX_LENGTH }),
65
+ assignee: WorkerName,
66
+ needs: Type.Array(TaskId),
67
+ on_goal_failed: Type.Optional(GraphFailureEdgeSchema),
68
+ status: Type.Enum(["dependency_waiting", "ready", "in_progress", "blocked", "goal_failed", "goal_achieved", "cancelled"]),
69
+ state: GraphTaskStateSchema,
70
+ current_context: Type.String({ minLength: 1, maxLength: TASK_CARD_CONTEXT_MAX_LENGTH }),
71
+ version: TaskVersionRefSchema,
72
+ activation_key: Type.Optional(Type.String({ minLength: 1 })),
73
+ accepted_attempt_id: Type.Optional(Type.String({ minLength: 1 })),
74
+ current_attempt: Type.Optional(GraphAttemptSummarySchema),
75
+ attempts_started: Type.Integer({ minimum: 0 }),
76
+ // Optional compatibility coordinates let generic read-only consumers accept
77
+ // both legacy and graph-native cards without making them graph authority.
78
+ relations: Type.Optional(Type.Array(Type.Object({
79
+ relation: Type.Enum(["blocked_by", "parent", "related"]),
80
+ target_task_id: TaskId,
81
+ }, { additionalProperties: false }))),
82
+ dependency_state: Type.Optional(Type.Union([
83
+ Type.Object({ kind: Type.Literal("ready"), active_blocker_ids: Type.Array(TaskId, { maxItems: 0 }) }, { additionalProperties: false }),
84
+ Type.Object({ kind: Type.Literal("waiting"), active_blocker_ids: Type.Array(TaskId, { minItems: 1 }) }, { additionalProperties: false }),
85
+ Type.Object({ kind: Type.Literal("terminal"), active_blocker_ids: Type.Array(TaskId) }, { additionalProperties: false }),
86
+ ])),
87
+ projection_warnings: Type.Optional(Type.Array(Type.Object({
88
+ task_id: TaskId,
89
+ truncated_fields: Type.Array(Type.Enum(["title", "goal", "current_context"])),
90
+ incomplete_fields: Type.Array(Type.Enum(["title", "goal", "current_context"])),
91
+ message: Type.String({ minLength: 1 }),
92
+ }, { additionalProperties: false }))),
93
+ }, { additionalProperties: false });
94
+ const GraphTaskItemSchema = Type.Object({
95
+ key: TaskKey,
96
+ title: Type.String({ minLength: 1, maxLength: TASK_CARD_TITLE_MAX_LENGTH }),
97
+ goal: Type.String({ minLength: 1, maxLength: TASK_CARD_GOAL_MAX_LENGTH, description: "Desired outcome and external success signal." }),
98
+ assignee: WorkerName,
99
+ needs: Type.Optional(Type.Array(TaskKey, { uniqueItems: true })),
100
+ on_goal_failed: Type.Optional(GraphFailureEdgeSchema),
101
+ }, { additionalProperties: false });
102
+ /** Breaking replacement for task_create. It applies one complete graph revision. */
103
+ export const TaskGraphApplyParametersSchema = Type.Object({
104
+ operation_id: OperationId,
105
+ expected_graph_version: Type.Optional(GraphVersionRefSchema),
106
+ tasks: Type.Array(GraphTaskItemSchema, { minItems: 1 }),
107
+ }, {
108
+ additionalProperties: false,
109
+ description: "Atomically apply the complete assigned Task graph. Omit expected_graph_version only for its first revision.",
110
+ });
111
+ /** One graph-native Task command. Waiting and ready are derived, never authored. */
112
+ export const GraphTaskUpdateParametersSchema = Type.Object({
113
+ task_id: TaskId,
114
+ operation_id: OperationId,
115
+ expected_version: TaskVersionRefSchema,
116
+ transition: Type.Optional(GraphTaskTransitionSchema),
117
+ current_context: Type.Optional(Type.String({ minLength: 1, maxLength: TASK_CARD_CONTEXT_MAX_LENGTH })),
118
+ evidence: Type.Optional(Type.String({ minLength: 1 })),
119
+ }, {
120
+ additionalProperties: false,
121
+ minProperties: 4,
122
+ description: "Apply one explicit Task transition or record current context. dependency_waiting and ready cannot be authored.",
123
+ });
@@ -0,0 +1,12 @@
1
+ import type { TSchema } from 'typebox';
2
+ import type { Result } from './state.ts';
3
+ export declare const TOOL_NAMES: readonly ["team_create", "team_sync", "ensure_worker", "task_graph_apply", "task_read", "task_update", "worker_stop", "team_shutdown", "alert_send"];
4
+ export type ToolName = typeof TOOL_NAMES[number];
5
+ export type Role = 'lead' | 'worker';
6
+ export interface Contract {
7
+ lead: Record<ToolName, TSchema>;
8
+ worker: Partial<Record<ToolName, TSchema>>;
9
+ results: Record<ToolName, TSchema>;
10
+ }
11
+ export declare function contracts(goalMaxLength?: number): Contract;
12
+ export declare function checkResult(contract: Contract, name: ToolName, value: Result): Result;
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Check } from 'typebox/value';
3
+ export const TOOL_NAMES = ['team_create', 'team_sync', 'ensure_worker', 'task_graph_apply', 'task_read', 'task_update', 'worker_stop', 'team_shutdown', 'alert_send'];
4
+ // Generated from the pinned executable rc.7 contract, not the stale agent surface.
5
+ const source = JSON.parse(readFileSync(new URL('./rc7-contract.json', import.meta.url), 'utf8'));
6
+ export function contracts(goalMaxLength = 1600) {
7
+ if (!Number.isSafeInteger(goalMaxLength) || goalMaxLength < 1)
8
+ throw new Error('goalMaxLength must be a positive safe integer');
9
+ const copy = structuredClone(source);
10
+ function patch(value) {
11
+ if (!value || typeof value !== 'object')
12
+ return;
13
+ const node = value;
14
+ if (node.properties && typeof node.properties === 'object') {
15
+ const goal = node.properties.goal;
16
+ if (goal && typeof goal === 'object')
17
+ goal.maxLength = goalMaxLength;
18
+ }
19
+ for (const child of Object.values(node))
20
+ patch(child);
21
+ }
22
+ patch(copy);
23
+ return copy;
24
+ }
25
+ export function checkResult(contract, name, value) {
26
+ if (!Check(contract.results[name], value))
27
+ throw new Error(`PTB ${name} produced a result outside the rc.7 contract: ${JSON.stringify(value)}`);
28
+ return value;
29
+ }
@@ -0,0 +1,59 @@
1
+ import type { AbortRun, InterruptTarget } from './interrupt.ts';
2
+ import { type SubmissionId, type Tx } from '@earendil-works/pi-durable';
3
+ import { type State, type Obligation } from './state.ts';
4
+ /** `kind` and `sender` name the delivery in its queue entry; inputs persisted before they existed lack them. */
5
+ export interface DeliveryInput extends InterruptTarget {
6
+ interrupt?: true;
7
+ epoch: string;
8
+ content: string;
9
+ whenBusy: 'steer' | 'followUp';
10
+ kind?: Obligation['kind'];
11
+ sender?: string;
12
+ }
13
+ /**
14
+ * The structured data of a Team input's `team.notice` entry. A reader names the input from these fields, never from
15
+ * its text: `delivery` is the kind (`update` for a notifier page), `sender` the author, at most SENDER_LIMIT characters.
16
+ * `interrupt` marks an Alert sent to interrupt the recipient's run; the interruption outcome stays in the model-facing content.
17
+ * Entries written before `delivery` existed carry only `source` and `deliveryId` or `pageId`.
18
+ */
19
+ export type TeamNoticeData = {
20
+ source: 'team';
21
+ epoch: string;
22
+ deliveryId?: string;
23
+ pageId?: string;
24
+ delivery?: Obligation['kind'] | 'update';
25
+ sender?: string;
26
+ interrupt?: true;
27
+ };
28
+ export declare const SENDER_LIMIT = 64;
29
+ export declare function eligible(s: State, o: Obligation): boolean;
30
+ export declare function deliveryInput(s: State, o: Obligation): DeliveryInput;
31
+ /** The input a delivery submits. The request ID names the Obligation, so a retry reacquires the same admission. */
32
+ export declare function deliveryDraft(input: DeliveryInput, interruption?: Obligation['interruption']): {
33
+ type: "input";
34
+ content: string;
35
+ whenBusy: "steer" | "followUp";
36
+ requestId: string;
37
+ entry: {
38
+ kind: string;
39
+ data: TeamNoticeData;
40
+ };
41
+ };
42
+ export declare const Deliver: import("@earendil-works/pi-durable").Task<DeliveryInput, {
43
+ phase: "interrupt";
44
+ } | {
45
+ phase: "submit";
46
+ } | {
47
+ phase: "settle";
48
+ submissionId: SubmissionId;
49
+ }, null, object>;
50
+ /** Keep the shared submit/settle mechanics; only the host-owned interruption needs a port. */
51
+ export declare function createDeliver(abortRun: AbortRun): import("@earendil-works/pi-durable").Task<DeliveryInput, {
52
+ phase: "interrupt";
53
+ } | {
54
+ phase: "submit";
55
+ } | {
56
+ phase: "settle";
57
+ submissionId: SubmissionId;
58
+ }, null, object>;
59
+ export declare function scheduleDeliveries(tx: Tx, s: State, delivery: typeof Deliver): Promise<void>;