@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
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,210 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hypercarrier/pi-team-durable
|
|
2
2
|
|
|
3
|
-
|
|
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 and a 100-seed differential oracle run.
|
|
23
|
+
Run 500 or more seeds by hand before a release.
|
|
24
|
+
The SIGKILL crash suite and `bench/e2e.ts` stay in HyperCarrier (private).
|
|
25
|
+
- **Before deploy.** The first open migrates the store to the indexed format. The
|
|
26
|
+
migration is one-way: rollback means restoring a backup. Back up the store, and
|
|
27
|
+
stop every older node process first. 0.1.0-rc.1 stores version-1 documents.
|
|
28
|
+
This version migrates them to version 2, and a downgrade cannot read a migrated
|
|
29
|
+
store.
|
|
30
|
+
|
|
31
|
+
Known issues:
|
|
32
|
+
|
|
33
|
+
1. An Alert is lost when the recipient's model errors. It shows as unanswered. Pi
|
|
34
|
+
Team Bright retries it; this package does not.
|
|
35
|
+
2. `team_sync` can replay pages that an earlier call released. Act only on
|
|
36
|
+
`task_read`, and never call `team_sync` from a nested tool call.
|
|
37
|
+
3. Interrupt: a Worker can interrupt the lead, the run to stop is chosen when the
|
|
38
|
+
Alert is accepted, and the abort has no settle bound.
|
|
39
|
+
4. An Alert can be admitted after its recipient is closed or fenced. After an owner
|
|
40
|
+
Close, a busy Worker can still run one tool call.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install @hypercarrier/pi-team-durable@next
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The package ships compiled JavaScript and type declarations under `dist`.
|
|
49
|
+
Node 22.19 or newer. Install the three pinned peers beside it.
|
|
50
|
+
|
|
51
|
+
`./testing` exports named internals for a host's own tests. It is unstable test support:
|
|
52
|
+
its names can change in any release. Production code imports `./authority` only.
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
`./authority` exports the runtime factory, authority documents, observation
|
|
57
|
+
observer, and structural stop ports. A host adapter registers this runtime.
|
|
58
|
+
|
|
59
|
+
- `ptb.team` owns membership and lifecycle. `ptb.head` holds current graph
|
|
60
|
+
coordinates, readiness, occupancy, attention, and bounded recent references.
|
|
61
|
+
Task family members hold current facts, retained Attempts, cards, and operator
|
|
62
|
+
projections. Immutable graph indexes hold topology. Worker indexes hold
|
|
63
|
+
nonterminal Task IDs. Each changed document is stored as a base.
|
|
64
|
+
- A controller-only conversation holds graph revision diffs and controller
|
|
65
|
+
events. Commands restore the incremental engine from current Task records.
|
|
66
|
+
They commit changed facts, indexes, receipts, obligations, and history together.
|
|
67
|
+
- Operation receipts retain exact result strings per epoch. Native tool-call
|
|
68
|
+
receipts use task scope and retire with the tool. Direct callers use permanent
|
|
69
|
+
family members. Obligations retain their identity after settlement; bounded
|
|
70
|
+
hash partitions index pending and unscheduled keys.
|
|
71
|
+
- Nine lead tools and three Worker tools use the rc.7 contract. Exact conversation
|
|
72
|
+
and generation bindings guard authority. A fork does not acquire an existing
|
|
73
|
+
Team's lead authority.
|
|
74
|
+
- `ptb.worker-anchor` owns a Worker conversation and renews a durable sleep.
|
|
75
|
+
Lead Esc does not end it. Stops use stored target IDs through the node's single
|
|
76
|
+
mark-before-resume fence. Failed confirmation retains the intent.
|
|
77
|
+
- `ptb.deliver` rechecks an obligation, submits with a stable request ID, records
|
|
78
|
+
admission, and waits for settlement. Notices use `team.notice` entries whose
|
|
79
|
+
data (`TeamNoticeData`) names the delivery kind and its sender: an Alert's
|
|
80
|
+
author, or `team-lead` for Tasks and stops. An interrupt Alert's data also
|
|
81
|
+
has `interrupt: true`. A reader names a notice from this data, never from its
|
|
82
|
+
text.
|
|
83
|
+
Interrupt Alerts persist the recipient run ID at acceptance. A separate phase
|
|
84
|
+
asks the host `abortRun` port to stop that exact run, then checkpoints before
|
|
85
|
+
normal admission. Queue identities stay native; no input is re-queued.
|
|
86
|
+
A failed interruption appears as unproved in the notice. Retry never selects
|
|
87
|
+
a replacement run. The node supplies a cancellable wait outside its admission lock.
|
|
88
|
+
- `ptb.notify` reserves an automatic page before admission. A failed lead input
|
|
89
|
+
pauses notification. Explicit sync or a new owner input resumes it.
|
|
90
|
+
- `team_sync` reserves successive bounded pages. It never waits for consumption.
|
|
91
|
+
The final request hook records exact page visibility. A later observer requires
|
|
92
|
+
a matching persisted native assistant entry and successful input settlement
|
|
93
|
+
before it advances the cursor. Compaction and failed runs release unseen pages.
|
|
94
|
+
- The head keeps references to 100 recent journal events. Their immutable
|
|
95
|
+
`ptb.event` entries stay in the lead conversation, so sync page boundaries stay
|
|
96
|
+
unchanged.
|
|
97
|
+
- Observation indexes, page metadata, immutable page bodies, and open proofs
|
|
98
|
+
use separate base-checkpointed documents. Requests check active pages from
|
|
99
|
+
the newest message backward. They do not hash transcript history.
|
|
100
|
+
- The observer retires a proof after its run settles and it processes the
|
|
101
|
+
evidence. It retires a page body only when the page is acknowledged or
|
|
102
|
+
replaced and the contiguous cursor covers it. Unsettled proofs and notifier
|
|
103
|
+
submit/settle checkpoints keep referenced bodies. Unknown notifier ownership
|
|
104
|
+
keeps the body. Page metadata and the exact replay receipts remain.
|
|
105
|
+
- `observations(tx)` and `observationStats(tx)` are diagnostic readers. The
|
|
106
|
+
latter reports current-record counts and UTF-8 JSON bytes. Production hooks
|
|
107
|
+
and the observer read current indexes.
|
|
108
|
+
- `/ptb` reads one acknowledged scan anchor for the current epoch. This anchor
|
|
109
|
+
differs from the runtime reservation anchor in `scanByEpoch`. Cursor advance
|
|
110
|
+
folds eligible waiting-page anchors before removing their waiting entries.
|
|
111
|
+
Body retirement retains the anchor; epoch change removes it.
|
|
112
|
+
- `readTeamPanel` reads current Task projections and recent records.
|
|
113
|
+
`readTeamSummary` reads head indexes. `state.open()` is a full-history diagnostic
|
|
114
|
+
adapter; runtime paths do not use it.
|
|
115
|
+
|
|
116
|
+
The first authority open migrates rc.7 documents in one transaction. It checks
|
|
117
|
+
current facts against the reference controller and writes the new records and
|
|
118
|
+
log. The same transaction raises all six shared PTB document kinds to version 2:
|
|
119
|
+
`ptb.team`, `ptb.graph`, `ptb.journal`, `ptb.outbox`, `ptb.receipts`, and
|
|
120
|
+
`ptb.observation`. `ptb.team` keeps its data. The other five retain only
|
|
121
|
+
`{fence:"ptb-authority-v2"}`. Current commands use the new authority records.
|
|
122
|
+
These occupied addresses prevent old code from creating fresh version-1
|
|
123
|
+
documents. pi-durable rejects old typed reads before mutation. Fresh stores
|
|
124
|
+
receive the same fence. Native pi-durable documents keep their own versions.
|
|
125
|
+
|
|
126
|
+
A migrated operation receipt receives its original projection check on its
|
|
127
|
+
first replay. Downgrade is not supported.
|
|
128
|
+
|
|
129
|
+
The fence covers stopped and reopened stores, not an old process that is still
|
|
130
|
+
running. Before the first open with this build, stop every node process that
|
|
131
|
+
runs older code, and back up the whole store directory. To roll back, restore
|
|
132
|
+
that backup and run the older build on it. A rollback discards all Team work
|
|
133
|
+
accepted after the migration.
|
|
134
|
+
|
|
135
|
+
Known hazard until the next contract change: `team_sync` can replay a stale
|
|
136
|
+
page. This occurred with a direct snapshot call as well as nested calls. Leads
|
|
137
|
+
use `task_read` for any Task state they act on. A direct `team_sync` call does
|
|
138
|
+
not establish current Task state.
|
|
139
|
+
|
|
140
|
+
The host injects model roles: a static `modelRoles` list, or a `catalog(tx,
|
|
141
|
+
lead)` resolver that gives each lead its own roles and default. The AmPi
|
|
142
|
+
node's `default` role captures the authoritative lead model at each Worker
|
|
143
|
+
generation. Unknown roles return a typed refusal.
|
|
144
|
+
The package never reads Pi configuration. The observation commit port resolves
|
|
145
|
+
its store for every hook; a registry can serve multiple stores.
|
|
146
|
+
|
|
147
|
+
The executable contract snapshot comes from Pi Team Bright 0.24.0-rc.7 at
|
|
148
|
+
`382950ce62bd38b6cd9f7fb80b91ef5b9140e15a` (unreleased Alert interrupt support).
|
|
149
|
+
`scripts/rc7-contract.mjs` records source hashes. Pi Team Bright 0.25.1 is the
|
|
150
|
+
contract pin: it ships the same source files, and a test compares their SHA-256
|
|
151
|
+
values with the recorded ones. The tests regenerate the contract from the
|
|
152
|
+
installed devDependency `@hypercarrier/pi-team-bright@0.25.1`.
|
|
153
|
+
|
|
154
|
+
Each role's tool labels and descriptions come from Pi Team Bright's own
|
|
155
|
+
registration. `scripts/pi-registration.mjs` captures it per role through an
|
|
156
|
+
inert host. The parity test compares every durable tool with the installed
|
|
157
|
+
`@hypercarrier/pi-team-bright`. Known gap: Pi's Worker `alert_send` requires
|
|
158
|
+
non-empty `text` and `task_id`; the durable schema does not yet.
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npm ci
|
|
162
|
+
npm test
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The hardening oracle freezes `src/authority` at
|
|
166
|
+
certified P, the 0.1.0-rc.1 port, in `test/oracle/authority`.
|
|
167
|
+
Do not update that copy when optimizing the runtime. The seeded differential
|
|
168
|
+
harness compares exact tool results or error messages, all public Task cards,
|
|
169
|
+
and created `ptb.deliver` inputs after every command. Each runtime has its own
|
|
170
|
+
MemoryStorage Harness. Both schedulers stay paused. The stop port returns fake
|
|
171
|
+
settlement. A synthetic persisted lead run permits snapshot and update reads.
|
|
172
|
+
Delivery execution and observation acknowledgement have separate tests.
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
# From the repository root: N seeds over K processes (defaults: 500 and 4).
|
|
176
|
+
npm run test:oracle -- --seeds 500 --shards 4
|
|
177
|
+
# Replay one failing seed. Failures print its full command trace.
|
|
178
|
+
npm run test:oracle -- --seed 42 --seeds 1 --shards 1
|
|
179
|
+
# Run only the deterministic boundary cases.
|
|
180
|
+
npm run test:oracle -- --targeted-only
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Each seed runs about 70 commands over at most seven Tasks. It combines a fixed
|
|
184
|
+
lifecycle and transition prefix with 30 random commands, then starts a new Team
|
|
185
|
+
epoch. Use `--steps` to change the random command count. A separate deterministic
|
|
186
|
+
sequence covers prerequisite repair, superseded dependents, removed and
|
|
187
|
+
reintroduced keys, stale lineage, blocked Worker capacity, reorder-only
|
|
188
|
+
restatement, and stop notices after Worker reconnection. The normal Vitest suite
|
|
189
|
+
runs these cases and three smoke seeds. This oracle detects behavior drift from the frozen
|
|
190
|
+
runtime; it does not prove that the frozen behavior is correct.
|
|
191
|
+
|
|
192
|
+
Fast tests use MemoryStorage, a fixed clock, inert timers, and faux responses.
|
|
193
|
+
The SIGKILL crash-recovery suite and `bench/e2e.ts` stay in HyperCarrier (private).
|
|
194
|
+
They use the AmPi node's subprocess carrier. This repository's tests do not
|
|
195
|
+
cover crash recovery.
|
|
196
|
+
|
|
197
|
+
The C1 delivery hold remains unported. pi-durable places steers and follow-ups at
|
|
198
|
+
turn boundaries. A stale admitted notice cannot grant authority: Task version and
|
|
199
|
+
Worker generation checks still apply.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
`npm run build` emits `dist`; `npm install` runs it through `prepare`.
|
|
204
|
+
`npm run typecheck`, `npm test` and `npm run verify:package` are the CI checks.
|
|
205
|
+
See [releasing](docs/maintainers/releasing.md) for the release procedure.
|
|
206
|
+
|
|
207
|
+
## Provenance
|
|
208
|
+
|
|
209
|
+
Extracted from HyperCarrier (private) at the certified port, renamed from
|
|
210
|
+
`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,19 @@
|
|
|
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
|
+
/** The label and description Pi Team Bright registers for one role's tool. */
|
|
7
|
+
export interface ToolPresentation {
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Contract {
|
|
12
|
+
lead: Record<ToolName, TSchema>;
|
|
13
|
+
worker: Partial<Record<ToolName, TSchema>>;
|
|
14
|
+
results: Record<ToolName, TSchema>;
|
|
15
|
+
presentation: Record<Role, Partial<Record<ToolName, ToolPresentation>>>;
|
|
16
|
+
}
|
|
17
|
+
export declare function checkSchema(schema: TSchema, value: unknown): boolean;
|
|
18
|
+
export declare function contracts(goalMaxLength?: number): Contract;
|
|
19
|
+
export declare function checkResult(contract: Contract, name: ToolName, value: Result): Result;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Compile } from 'typebox/compile';
|
|
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 = readFileSync(new URL('./rc7-contract.json', import.meta.url), 'utf8');
|
|
6
|
+
const patchedContracts = new Map();
|
|
7
|
+
const validators = new Map();
|
|
8
|
+
export function checkSchema(schema, value) {
|
|
9
|
+
const key = JSON.stringify(schema, (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : value);
|
|
11
|
+
let validator = validators.get(key);
|
|
12
|
+
if (!validator) {
|
|
13
|
+
validator = Compile(JSON.parse(key));
|
|
14
|
+
validators.set(key, validator);
|
|
15
|
+
}
|
|
16
|
+
return validator.Check(value);
|
|
17
|
+
}
|
|
18
|
+
export function contracts(goalMaxLength = 1600) {
|
|
19
|
+
if (!Number.isSafeInteger(goalMaxLength) || goalMaxLength < 1)
|
|
20
|
+
throw new Error('goalMaxLength must be a positive safe integer');
|
|
21
|
+
const cached = patchedContracts.get(goalMaxLength);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return JSON.parse(cached);
|
|
24
|
+
const copy = JSON.parse(source);
|
|
25
|
+
function patch(value) {
|
|
26
|
+
if (!value || typeof value !== 'object')
|
|
27
|
+
return;
|
|
28
|
+
const node = value;
|
|
29
|
+
if (node.properties && typeof node.properties === 'object') {
|
|
30
|
+
const goal = node.properties.goal;
|
|
31
|
+
if (goal && typeof goal === 'object')
|
|
32
|
+
goal.maxLength = goalMaxLength;
|
|
33
|
+
}
|
|
34
|
+
for (const child of Object.values(node))
|
|
35
|
+
patch(child);
|
|
36
|
+
}
|
|
37
|
+
patch(copy);
|
|
38
|
+
patchedContracts.set(goalMaxLength, JSON.stringify(copy));
|
|
39
|
+
return copy;
|
|
40
|
+
}
|
|
41
|
+
export function checkResult(contract, name, value) {
|
|
42
|
+
if (!checkSchema(contract.results[name], value))
|
|
43
|
+
throw new Error(`PTB ${name} produced a result outside the rc.7 contract: ${JSON.stringify(value)}`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|