@earendil-works/pi-durable 0.87.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -1
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +60 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +681 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +30 -0
- package/dist/storage/sqlite/database.d.ts.map +1 -0
- package/dist/storage/sqlite/database.js +2 -0
- package/dist/storage/sqlite/database.js.map +1 -0
- package/dist/storage/sqlite/index.d.ts +4 -0
- package/dist/storage/sqlite/index.d.ts.map +1 -0
- package/dist/storage/sqlite/index.js +3 -0
- package/dist/storage/sqlite/index.js.map +1 -0
- package/dist/storage/sqlite/migrations.d.ts +11 -0
- package/dist/storage/sqlite/migrations.d.ts.map +1 -0
- package/dist/storage/sqlite/migrations.js +111 -0
- package/dist/storage/sqlite/migrations.js.map +1 -0
- package/dist/storage/sqlite/node.d.ts +25 -0
- package/dist/storage/sqlite/node.d.ts.map +1 -0
- package/dist/storage/sqlite/node.js +97 -0
- package/dist/storage/sqlite/node.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +51 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +628 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +525 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +90 -102
- package/docs/pico-v5-handoff.md +493 -253
- package/docs/pico-v5.md +1901 -848
- package/package.json +47 -5
- package/dist/memory-storage.d.ts +0 -37
- package/dist/memory-storage.d.ts.map +0 -1
- package/dist/memory-storage.js +0 -322
- package/dist/memory-storage.js.map +0 -1
package/docs/pico-v5.md
CHANGED
|
@@ -4,9 +4,19 @@ Pico5 is a durable, extensible agent harness. This document is normative.
|
|
|
4
4
|
Pico5 uses existing package types as follows:
|
|
5
5
|
|
|
6
6
|
```ts
|
|
7
|
-
import type { Context, JsonValue } from "@earendil-works/chord";
|
|
8
|
-
import { applyImmutable, type Op } from "@earendil-works/chord/delta";
|
|
9
7
|
import type {
|
|
8
|
+
AttachedReplicatedState,
|
|
9
|
+
Context,
|
|
10
|
+
Draft,
|
|
11
|
+
JsonRepresentation,
|
|
12
|
+
JsonValue,
|
|
13
|
+
ReplicatedState,
|
|
14
|
+
} from "@earendil-works/chord";
|
|
15
|
+
import type { Op } from "@earendil-works/chord/delta";
|
|
16
|
+
import type {
|
|
17
|
+
AssistantMessage,
|
|
18
|
+
CacheRetention,
|
|
19
|
+
DeferredHandle,
|
|
10
20
|
Message,
|
|
11
21
|
Models,
|
|
12
22
|
ModelThinkingLevel,
|
|
@@ -14,11 +24,12 @@ import type {
|
|
|
14
24
|
Tool,
|
|
15
25
|
ToolReference,
|
|
16
26
|
ToolResultMessage,
|
|
27
|
+
Transport,
|
|
17
28
|
UserMessage,
|
|
18
29
|
} from "@earendil-works/pi-ai";
|
|
19
30
|
|
|
20
31
|
type JsonObject = { [key: string]: JsonValue };
|
|
21
|
-
type
|
|
32
|
+
type TaskOutcomeError = { message: string; detail?: JsonValue };
|
|
22
33
|
```
|
|
23
34
|
|
|
24
35
|
Pico5 targets the transcript `SystemMessage` contract from pi-ai PR
|
|
@@ -32,8 +43,8 @@ The core rule is:
|
|
|
32
43
|
|
|
33
44
|
## 1. Terms and invariants
|
|
34
45
|
|
|
35
|
-
- A **Session** owns one mutation line, conversations, entries, tasks,
|
|
36
|
-
and documents.
|
|
46
|
+
- A **Session** owns one mutation line, conversations, entries, tasks,
|
|
47
|
+
submissions, and documents.
|
|
37
48
|
- A **conversation** is a transcript scope. It may fork another conversation.
|
|
38
49
|
- An **entry** is an immutable transcript record.
|
|
39
50
|
- A **task** is a durable state machine attached to one conversation.
|
|
@@ -42,6 +53,10 @@ The core rule is:
|
|
|
42
53
|
- A **definition** is a typed token describing one document or document family.
|
|
43
54
|
- A **source** exposes committed document changes to Chord without exposing a
|
|
44
55
|
mutable object.
|
|
56
|
+
- A **turn** is one assistant response and the tool calls it makes. A **run**
|
|
57
|
+
is the sequence of turns from an admitted input to the final answer; turn
|
|
58
|
+
boundaries (section 6) sit between its turns. A conversation is busy while a
|
|
59
|
+
run is active.
|
|
45
60
|
|
|
46
61
|
Required invariants:
|
|
47
62
|
|
|
@@ -50,13 +65,15 @@ Required invariants:
|
|
|
50
65
|
3. All visible progress is durable. There is no volatile publication path.
|
|
51
66
|
4. External effects do not run inside the Session mutation transaction.
|
|
52
67
|
5. Entries and IDs are immutable and never reused after a committed write.
|
|
53
|
-
6. Document drafts
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
6. Document drafts are fully revoked when their transaction callback settles:
|
|
69
|
+
the Session synchronously prepares or aborts every open change at that point.
|
|
70
|
+
Values assigned into a draft are copied by value and must be strict JSON.
|
|
56
71
|
7. The mutation line remains held through storage settlement and committed-state
|
|
57
|
-
adoption.
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
adoption. Commit/close observers run synchronously only to capture immutable
|
|
73
|
+
state; document-state and watch user callbacks run later, off the line.
|
|
74
|
+
8. An uncertain storage failure is fatal to the open Session. It publishes
|
|
75
|
+
nothing and must be reopened. Preparation and checkpoint failures occur before
|
|
76
|
+
storage admission and roll back normally.
|
|
60
77
|
|
|
61
78
|
## 2. Core records
|
|
62
79
|
|
|
@@ -64,19 +81,37 @@ The concrete JSON representations may add bookkeeping fields, but must preserve
|
|
|
64
81
|
these contracts.
|
|
65
82
|
|
|
66
83
|
```ts
|
|
67
|
-
|
|
68
|
-
type
|
|
69
|
-
|
|
84
|
+
declare const idBrand: unique symbol;
|
|
85
|
+
type Id<Kind extends string, Type = unknown> = number & {
|
|
86
|
+
readonly [idBrand]: {
|
|
87
|
+
readonly kind: Kind;
|
|
88
|
+
readonly type: Type;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type ConversationId = Id<"conversation">;
|
|
92
|
+
type EntryId = Id<"entry">;
|
|
93
|
+
type TaskId<Result = unknown> = Id<"task", Result>;
|
|
94
|
+
type SubmissionId = Id<"submission">;
|
|
95
|
+
type DocumentId = Id<"document">;
|
|
96
|
+
|
|
97
|
+
declare const seqBrand: unique symbol;
|
|
98
|
+
/** Strictly increases between commits; gaps are permitted. */
|
|
99
|
+
type Seq = number & { readonly [seqBrand]: "sequence" };
|
|
100
|
+
const ROOT_CONVERSATION_ID = 1 as ConversationId;
|
|
101
|
+
|
|
102
|
+
type ConversationOwnership =
|
|
103
|
+
| { readonly kind: "ownerless" }
|
|
104
|
+
| { readonly kind: "task"; readonly taskId: TaskId };
|
|
70
105
|
|
|
71
106
|
type ConversationRecord = {
|
|
72
|
-
readonly id:
|
|
107
|
+
readonly id: ConversationId;
|
|
73
108
|
readonly parent?: {
|
|
74
|
-
readonly conversationId:
|
|
75
|
-
readonly at:
|
|
109
|
+
readonly conversationId: ConversationId;
|
|
110
|
+
readonly at: EntryId;
|
|
76
111
|
};
|
|
77
112
|
readonly owner?: {
|
|
78
|
-
readonly conversationId:
|
|
79
|
-
readonly taskId:
|
|
113
|
+
readonly conversationId: ConversationId;
|
|
114
|
+
readonly taskId: TaskId;
|
|
80
115
|
};
|
|
81
116
|
};
|
|
82
117
|
```
|
|
@@ -94,68 +129,126 @@ interface SystemMessage {
|
|
|
94
129
|
}
|
|
95
130
|
```
|
|
96
131
|
|
|
97
|
-
`content` is the base prompt on the leading message and additional
|
|
98
|
-
text on later messages. `sections` is an ordered named patch: a
|
|
99
|
-
replaces a section, while `null` removes it. `toolsRemoved` is
|
|
100
|
-
`toolsAdded` within one message. Replaying every system message
|
|
101
|
-
order yields the effective prompt and tool set.
|
|
132
|
+
In pi-ai, `content` is the base prompt on the leading message and additional
|
|
133
|
+
instruction text on later messages. `sections` is an ordered named patch: a
|
|
134
|
+
string adds or replaces a section, while `null` removes it. `toolsRemoved` is
|
|
135
|
+
applied before `toolsAdded` within one message. Replaying every system message
|
|
136
|
+
in transcript order yields the effective prompt and tool set.
|
|
137
|
+
|
|
138
|
+
Pico always writes `content: ""`. Every prompt part, including the preamble, is
|
|
139
|
+
a named section produced by the registry's system prompt slot (section 7.4).
|
|
102
140
|
|
|
103
141
|
```ts
|
|
104
142
|
type ContextEdit = {
|
|
105
|
-
readonly target:
|
|
143
|
+
readonly target: EntryId;
|
|
106
144
|
} & (
|
|
107
145
|
| { readonly action: "omit"; readonly messages?: never }
|
|
108
146
|
| { readonly action: "replace"; readonly messages: readonly Message[] }
|
|
109
147
|
);
|
|
110
148
|
|
|
111
149
|
type EntryRecord = {
|
|
112
|
-
readonly id:
|
|
113
|
-
readonly conversationId:
|
|
150
|
+
readonly id: EntryId;
|
|
151
|
+
readonly conversationId: ConversationId;
|
|
114
152
|
readonly kind: string;
|
|
115
153
|
readonly model?: readonly Message[];
|
|
116
154
|
readonly data?: JsonValue;
|
|
117
|
-
readonly head?:
|
|
155
|
+
readonly head?: EntryId;
|
|
118
156
|
readonly edits?: readonly ContextEdit[];
|
|
119
|
-
readonly byTaskId?:
|
|
157
|
+
readonly byTaskId?: TaskId;
|
|
120
158
|
};
|
|
121
159
|
|
|
122
160
|
type EntryDraft = Omit<EntryRecord, "id" | "conversationId" | "byTaskId" | "head"> & {
|
|
123
|
-
readonly head?:
|
|
161
|
+
readonly head?: EntryId | "self";
|
|
124
162
|
};
|
|
125
163
|
|
|
126
|
-
type
|
|
127
|
-
readonly id:
|
|
128
|
-
readonly conversationId:
|
|
164
|
+
type SubmissionRecordBase = {
|
|
165
|
+
readonly id: SubmissionId;
|
|
166
|
+
readonly conversationId: ConversationId;
|
|
129
167
|
readonly requestId?: string;
|
|
130
168
|
};
|
|
131
169
|
|
|
132
|
-
type
|
|
133
|
-
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
type SubmissionRecord =
|
|
171
|
+
| (SubmissionRecordBase & {
|
|
172
|
+
readonly type: "input";
|
|
173
|
+
} & (
|
|
174
|
+
| {
|
|
175
|
+
readonly status: "queued";
|
|
176
|
+
readonly entry?: never;
|
|
177
|
+
readonly answer?: never;
|
|
178
|
+
readonly reason?: never;
|
|
179
|
+
readonly detail?: never;
|
|
180
|
+
}
|
|
181
|
+
| {
|
|
182
|
+
readonly status: "placed";
|
|
183
|
+
readonly entry: EntryId;
|
|
184
|
+
readonly answer?: never;
|
|
185
|
+
readonly reason?: never;
|
|
186
|
+
readonly detail?: never;
|
|
187
|
+
}
|
|
188
|
+
| {
|
|
189
|
+
readonly status: "done";
|
|
190
|
+
readonly entry: EntryId;
|
|
191
|
+
readonly answer: EntryId;
|
|
192
|
+
readonly reason?: never;
|
|
193
|
+
readonly detail?: never;
|
|
194
|
+
}
|
|
195
|
+
| {
|
|
196
|
+
readonly status: "unanswered";
|
|
197
|
+
readonly entry?: EntryId;
|
|
198
|
+
readonly answer?: never;
|
|
199
|
+
readonly reason: string;
|
|
200
|
+
readonly detail?: JsonValue;
|
|
201
|
+
}
|
|
202
|
+
))
|
|
203
|
+
| (SubmissionRecordBase & {
|
|
204
|
+
readonly type: "write";
|
|
205
|
+
} & (
|
|
206
|
+
| {
|
|
207
|
+
readonly status: "queued";
|
|
208
|
+
readonly entry?: never;
|
|
209
|
+
readonly answer?: never;
|
|
210
|
+
readonly reason?: never;
|
|
211
|
+
readonly detail?: never;
|
|
212
|
+
}
|
|
213
|
+
| {
|
|
214
|
+
readonly status: "done";
|
|
215
|
+
readonly entry: EntryId;
|
|
216
|
+
readonly answer?: never;
|
|
217
|
+
readonly reason?: never;
|
|
218
|
+
readonly detail?: never;
|
|
219
|
+
}
|
|
220
|
+
| {
|
|
221
|
+
readonly status: "unanswered";
|
|
222
|
+
readonly entry?: never;
|
|
223
|
+
readonly answer?: never;
|
|
224
|
+
readonly reason: string;
|
|
225
|
+
readonly detail?: JsonValue;
|
|
226
|
+
}
|
|
227
|
+
));
|
|
228
|
+
|
|
229
|
+
type SubmissionSettlement =
|
|
230
|
+
| { readonly status: "done"; readonly answer: EntryId }
|
|
231
|
+
| { readonly status: "unanswered"; readonly reason: string; readonly detail?: JsonValue };
|
|
147
232
|
|
|
148
|
-
type
|
|
149
|
-
? Record extends
|
|
233
|
+
type SubmissionCreate = SubmissionRecord extends infer Record
|
|
234
|
+
? Record extends SubmissionRecord
|
|
150
235
|
? Omit<Record, "id">
|
|
151
236
|
: never
|
|
152
237
|
: never;
|
|
153
238
|
```
|
|
154
239
|
|
|
240
|
+
ID brands are compile-time only. IDs and sequences remain ordinary numbers in
|
|
241
|
+
memory, JSON, JSONL, and SQLite. Code applies a brand only at a trusted creation
|
|
242
|
+
or decoding boundary. The distinct sequence brand prevents an entity ID from
|
|
243
|
+
being used as a document commit point.
|
|
244
|
+
|
|
155
245
|
Conversation history parenting and task ownership are separate:
|
|
156
246
|
|
|
157
247
|
- `parent` controls inherited entries and historical documents.
|
|
158
|
-
- `owner`
|
|
248
|
+
- `owner` records task attribution and connects scopes for subtree abort and idle traversal. It is not an access-control capability.
|
|
249
|
+
- Creation always supplies `ConversationOwnership` explicitly. The Session
|
|
250
|
+
derives the persisted owner's conversation from the named task; callers never
|
|
251
|
+
construct the persisted owner pair.
|
|
159
252
|
|
|
160
253
|
A conversation's owner remains recorded after the owning task becomes terminal.
|
|
161
254
|
|
|
@@ -179,9 +272,11 @@ Context derivation:
|
|
|
179
272
|
5. If `H` exists, context entries are `H` followed by non-head entries in the
|
|
180
273
|
range. Otherwise they are the range.
|
|
181
274
|
6. Keep every positional system message and its tool/section changes.
|
|
182
|
-
7.
|
|
183
|
-
|
|
184
|
-
message protocol.
|
|
275
|
+
7. Move each assistant's tool results, found before the next assistant message,
|
|
276
|
+
directly after it in tool-call order, ahead of any intervening user or system
|
|
277
|
+
message. The provider protocol requires results to follow their call.
|
|
278
|
+
8. Synthesize an error result for every call without one, for example after a
|
|
279
|
+
fork cut or at an interrupted tail. Drop tool results with no preceding call.
|
|
185
280
|
9. Exclude model-less entries and assistant messages with `aborted`, `error`, or
|
|
186
281
|
`deferred` stop reasons from future provider requests.
|
|
187
282
|
|
|
@@ -203,21 +298,31 @@ type ModelRef = {
|
|
|
203
298
|
|
|
204
299
|
type UserInput = UserMessage["content"];
|
|
205
300
|
|
|
206
|
-
type
|
|
207
|
-
readonly content: UserInput;
|
|
301
|
+
type SubmissionDraft = {
|
|
208
302
|
readonly requestId?: string;
|
|
209
|
-
|
|
210
|
-
|
|
303
|
+
} & (
|
|
304
|
+
| {
|
|
305
|
+
readonly type: "input";
|
|
306
|
+
readonly content: UserInput;
|
|
307
|
+
readonly whenBusy?: "steer" | "followUp" | "reject";
|
|
308
|
+
readonly entry?: never;
|
|
309
|
+
}
|
|
310
|
+
| {
|
|
311
|
+
readonly type: "write";
|
|
312
|
+
readonly entry: EntryDraft;
|
|
313
|
+
readonly content?: never;
|
|
314
|
+
readonly whenBusy?: never;
|
|
315
|
+
}
|
|
316
|
+
);
|
|
211
317
|
|
|
212
|
-
type
|
|
213
|
-
| { readonly key: string; readonly value: JsonValue }
|
|
214
|
-
| { readonly key: string; readonly remove: true };
|
|
318
|
+
type InputSubmissionDraft = Extract<SubmissionDraft, { readonly type: "input" }>;
|
|
215
319
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
readonly
|
|
320
|
+
/** Runs inside the creating commit, after the conversation and its configuration exist. */
|
|
321
|
+
type ConversationInit = (tx: Tx, conversationId: ConversationId) => void | Promise<void>;
|
|
322
|
+
|
|
323
|
+
type ConversationCreateOptions = {
|
|
324
|
+
readonly ownership: ConversationOwnership;
|
|
325
|
+
readonly init?: ConversationInit;
|
|
221
326
|
};
|
|
222
327
|
|
|
223
328
|
type AnyTask = {
|
|
@@ -232,22 +337,58 @@ type AnyTask = {
|
|
|
232
337
|
};
|
|
233
338
|
};
|
|
234
339
|
|
|
235
|
-
type HarnessOptions = {
|
|
340
|
+
type HarnessOptions<Tool extends ToolRegistration = ToolRegistration> = {
|
|
236
341
|
readonly models: Models; // the pi-ai Models interface
|
|
237
|
-
readonly
|
|
238
|
-
readonly taskKinds?: readonly AnyTask[];
|
|
239
|
-
readonly sections?: readonly SystemSection<JsonValue>[];
|
|
342
|
+
readonly registry: RegistryReader<Tool>; // section 7.1
|
|
240
343
|
readonly now?: () => number;
|
|
241
|
-
readonly root?: Omit<ConversationSpec, "parent">;
|
|
242
344
|
readonly onReport?: (error: unknown) => void;
|
|
243
345
|
};
|
|
244
346
|
|
|
245
|
-
|
|
347
|
+
/** Curated pi-ai request options; absent fields use pi-ai defaults. */
|
|
348
|
+
type ConversationStreamOptions = {
|
|
349
|
+
transport?: Transport;
|
|
350
|
+
timeoutMs?: number;
|
|
351
|
+
/** Provider/SDK retries inside one request attempt. */
|
|
352
|
+
maxRetries?: number;
|
|
353
|
+
maxRetryDelayMs?: number;
|
|
354
|
+
headers?: Record<string, string>;
|
|
355
|
+
metadata?: JsonObject;
|
|
356
|
+
cacheRetention?: CacheRetention;
|
|
357
|
+
deferred?: boolean | { window?: "15m" | "1h" | "24h" };
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/** Durable generation attempt retries; the JSON shape of pi-ai `RetryPolicy`. */
|
|
361
|
+
type ConversationRetryPolicy = {
|
|
362
|
+
enabled: boolean;
|
|
363
|
+
maxRetries: number;
|
|
364
|
+
baseDelayMs: number;
|
|
365
|
+
maxAgentDelayMs?: number;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
type ConversationConfigState = {
|
|
369
|
+
model?: ModelRef;
|
|
370
|
+
thinkingLevel: ModelThinkingLevel;
|
|
371
|
+
activeTools: string[];
|
|
372
|
+
streamOptions?: ConversationStreamOptions;
|
|
373
|
+
retry?: ConversationRetryPolicy;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/** Built-in rewindable configuration document; see below. */
|
|
377
|
+
declare const ConversationConfig: RewindableConversationDocToken<ConversationConfigState>;
|
|
378
|
+
|
|
379
|
+
/** Entry whose `data` has type `D`; `never` means the kind carries no data. */
|
|
380
|
+
type TypedEntry<D extends JsonValue> = Omit<EntryRecord, "data"> &
|
|
381
|
+
([D] extends [never] ? { readonly data?: never } : { readonly data: D });
|
|
382
|
+
|
|
383
|
+
type TypedEntryDraft<D extends JsonValue> = Omit<EntryDraft, "kind" | "data"> &
|
|
384
|
+
([D] extends [never] ? { readonly data?: never } : { readonly data: D });
|
|
385
|
+
|
|
386
|
+
interface Entry<D extends JsonValue = never> {
|
|
246
387
|
readonly kind: string;
|
|
247
|
-
is(entry: EntryRecord | undefined): entry is
|
|
388
|
+
is(entry: EntryRecord | undefined): entry is TypedEntry<D>;
|
|
248
389
|
}
|
|
249
390
|
|
|
250
|
-
function defineEntry<
|
|
391
|
+
function defineEntry<D extends JsonValue = never>(kind: string): Entry<D>;
|
|
251
392
|
|
|
252
393
|
type ContextView = {
|
|
253
394
|
readonly head: EntryRecord | undefined;
|
|
@@ -255,23 +396,42 @@ type ContextView = {
|
|
|
255
396
|
readonly messages: readonly Message[];
|
|
256
397
|
};
|
|
257
398
|
|
|
258
|
-
type
|
|
399
|
+
type SettledSubmissionRecord = SubmissionRecord & {
|
|
259
400
|
readonly status: "done" | "unanswered";
|
|
260
401
|
};
|
|
261
402
|
|
|
262
|
-
interface
|
|
263
|
-
readonly id:
|
|
264
|
-
|
|
265
|
-
wait(context: Context): Promise<
|
|
266
|
-
abort(
|
|
267
|
-
context: Context,
|
|
268
|
-
): Promise<"aborted" | "already_placed" | "not_found">;
|
|
403
|
+
interface Submission {
|
|
404
|
+
readonly id: SubmissionId;
|
|
405
|
+
status(context: Context): Promise<SubmissionRecord>;
|
|
406
|
+
wait(context: Context): Promise<SettledSubmissionRecord>;
|
|
407
|
+
abort(context: Context): Promise<"aborted" | "already_placed" | "settled">;
|
|
269
408
|
}
|
|
270
409
|
|
|
271
410
|
type SettledTask<R> = TaskRecord<JsonValue, JsonValue, R> & {
|
|
272
411
|
readonly state: Extract<TaskState<JsonValue, R>, { status: "terminal" }>;
|
|
273
412
|
};
|
|
274
413
|
|
|
414
|
+
type TaskInspection = {
|
|
415
|
+
readonly record: TaskRecord<JsonValue, JsonValue, JsonValue>;
|
|
416
|
+
readonly state:
|
|
417
|
+
| { readonly kind: "running" }
|
|
418
|
+
| { readonly kind: "ready"; readonly migrates: boolean }
|
|
419
|
+
| { readonly kind: "waiting"; readonly on: readonly TaskId[] }
|
|
420
|
+
| {
|
|
421
|
+
readonly kind: "blocked";
|
|
422
|
+
readonly reason: "missing_task" | "task_too_old" | "migration_failed";
|
|
423
|
+
readonly error?: unknown;
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
type HarnessInspection = {
|
|
428
|
+
readonly scheduling: "paused" | "running" | "closing";
|
|
429
|
+
readonly tasks: readonly TaskInspection[];
|
|
430
|
+
/** Queued and placed submissions in ID order. */
|
|
431
|
+
readonly submissions: readonly SubmissionRecord[];
|
|
432
|
+
readonly registry: readonly RegistryFailure[];
|
|
433
|
+
};
|
|
434
|
+
|
|
275
435
|
type ConversationWatch = WatchHandle<ConversationView>;
|
|
276
436
|
|
|
277
437
|
type HooksOf<K> = K extends Task<infer _I, infer _S, infer _R, infer H>
|
|
@@ -279,9 +439,8 @@ type HooksOf<K> = K extends Task<infer _I, infer _S, infer _R, infer H>
|
|
|
279
439
|
: never;
|
|
280
440
|
|
|
281
441
|
interface Conversation {
|
|
282
|
-
readonly id:
|
|
283
|
-
|
|
284
|
-
write(entry: EntryDraft, context: Context): Promise<InputHandle>;
|
|
442
|
+
readonly id: ConversationId;
|
|
443
|
+
submit(submission: SubmissionDraft, context: Context): Promise<Submission>;
|
|
285
444
|
|
|
286
445
|
getModel(context: Context): Promise<ModelRef | undefined>;
|
|
287
446
|
setModel(model: ModelRef | undefined, context: Context): Promise<void>;
|
|
@@ -289,15 +448,10 @@ interface Conversation {
|
|
|
289
448
|
setThinkingLevel(level: ModelThinkingLevel, context: Context): Promise<void>;
|
|
290
449
|
getActiveTools(context: Context): Promise<readonly string[]>;
|
|
291
450
|
setActiveTools(names: readonly string[], context: Context): Promise<void>;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
setSection<T extends JsonValue>(
|
|
297
|
-
section: SystemSection<T>,
|
|
298
|
-
value: T | undefined,
|
|
299
|
-
context: Context,
|
|
300
|
-
): Promise<void>;
|
|
451
|
+
getStreamOptions(context: Context): Promise<ConversationStreamOptions>;
|
|
452
|
+
setStreamOptions(options: ConversationStreamOptions, context: Context): Promise<void>;
|
|
453
|
+
getRetryPolicy(context: Context): Promise<ConversationRetryPolicy>;
|
|
454
|
+
setRetryPolicy(policy: ConversationRetryPolicy | undefined, context: Context): Promise<void>;
|
|
301
455
|
|
|
302
456
|
commit<T>(
|
|
303
457
|
change: (tx: Tx) => T | Promise<T>,
|
|
@@ -306,67 +460,50 @@ interface Conversation {
|
|
|
306
460
|
context(context: Context): Promise<ContextView>;
|
|
307
461
|
entries(
|
|
308
462
|
query: Omit<EntryQuery, "conversationId">,
|
|
309
|
-
cursor: Cursor | undefined,
|
|
310
463
|
limit: number,
|
|
464
|
+
cursor: Cursor | undefined,
|
|
311
465
|
context: Context,
|
|
312
466
|
): Promise<Page<EntryRecord, Cursor>>;
|
|
313
467
|
fork(
|
|
314
|
-
at:
|
|
315
|
-
|
|
468
|
+
at: EntryId,
|
|
469
|
+
options: ConversationCreateOptions,
|
|
316
470
|
context: Context,
|
|
317
471
|
): Promise<Conversation>;
|
|
318
|
-
collapse(instructions: string | undefined, context: Context): Promise<
|
|
472
|
+
collapse(instructions: string | undefined, context: Context): Promise<TaskId>;
|
|
319
473
|
reset(handoff: string | undefined, context: Context): Promise<void>;
|
|
320
474
|
abort(context: Context): Promise<void>;
|
|
321
475
|
waitForIdle(context: Context): Promise<void>;
|
|
322
|
-
|
|
323
|
-
owner: string,
|
|
324
|
-
task: K,
|
|
325
|
-
handlers: Partial<HooksOf<K>>,
|
|
326
|
-
options?: { readonly subtree?: boolean },
|
|
327
|
-
): () => void;
|
|
476
|
+
viewState(context: Context): Promise<AttachedReplicatedState<ConversationView>>;
|
|
328
477
|
watch(context: Context): Promise<ConversationWatch>;
|
|
329
478
|
}
|
|
330
479
|
|
|
331
480
|
interface Harness extends Session {
|
|
332
481
|
resume(): void;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
hold(): () => void;
|
|
336
|
-
|
|
337
|
-
registerTaskKind(task: AnyTask): () => void;
|
|
338
|
-
registerTool(tool: ToolRegistration): () => void;
|
|
339
|
-
registerSection(section: SystemSection<JsonValue>): () => void;
|
|
340
|
-
hooks<K extends AnyTask>(
|
|
341
|
-
owner: string,
|
|
342
|
-
task: K,
|
|
343
|
-
handlers: Partial<HooksOf<K>>,
|
|
344
|
-
): () => void;
|
|
345
|
-
|
|
346
|
-
root(context: Context): Promise<Conversation>;
|
|
347
|
-
onConversation(listener: (conversation: Conversation) => void): () => void;
|
|
348
|
-
conversation(id: Id, context: Context): Promise<Conversation | undefined>;
|
|
349
|
-
createConversation(
|
|
350
|
-
spec: ConversationSpec & { readonly input?: UserInput },
|
|
482
|
+
|
|
483
|
+
root(
|
|
351
484
|
context: Context,
|
|
485
|
+
options?: { readonly init?: ConversationInit },
|
|
352
486
|
): Promise<Conversation>;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
487
|
+
conversation(id: ConversationId, context: Context): Promise<Conversation | undefined>;
|
|
488
|
+
createConversation(options: ConversationCreateOptions, context: Context): Promise<Conversation>;
|
|
489
|
+
|
|
490
|
+
getTask<R>(id: TaskId<R>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, R> | undefined>;
|
|
491
|
+
inspect(context: Context): Promise<HarnessInspection>;
|
|
492
|
+
submission(id: SubmissionId, context: Context): Promise<Submission | undefined>;
|
|
493
|
+
abortSubmission(
|
|
494
|
+
id: SubmissionId,
|
|
357
495
|
context: Context,
|
|
358
|
-
conversationId?:
|
|
359
|
-
): Promise<"aborted" | "already_placed" | "not_found">;
|
|
360
|
-
abortTask(id:
|
|
361
|
-
|
|
362
|
-
waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
|
|
496
|
+
conversationId?: ConversationId,
|
|
497
|
+
): Promise<"aborted" | "already_placed" | "settled" | "not_found">;
|
|
498
|
+
abortTask(id: TaskId, context: Context): Promise<"marked" | "terminal">;
|
|
499
|
+
waitForTask<R>(id: TaskId<R>, context: Context): Promise<SettledTask<R>>;
|
|
363
500
|
waitForIdle(context: Context): Promise<void>;
|
|
364
501
|
}
|
|
365
502
|
|
|
366
503
|
declare const Harness: {
|
|
367
|
-
open(
|
|
504
|
+
open<Tool extends ToolRegistration>(
|
|
368
505
|
storage: Storage,
|
|
369
|
-
options: HarnessOptions
|
|
506
|
+
options: HarnessOptions<Tool>,
|
|
370
507
|
context: Context,
|
|
371
508
|
): Promise<Harness>;
|
|
372
509
|
};
|
|
@@ -375,79 +512,126 @@ declare const Harness: {
|
|
|
375
512
|
This intentionally retains the useful Pico3 host shape. It removes Pico3's
|
|
376
513
|
fixed `rewindable()`/`sticky()` accessors, namespace router, semantic view events,
|
|
377
514
|
and manual Chord view bridge. Typed Pico5 documents and the structural
|
|
378
|
-
conversation watch replace those surfaces. `
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
`Harness.open()`
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
`
|
|
515
|
+
conversation watch replace those surfaces. `submit()` durably admits either a
|
|
516
|
+
user input or passive entry write and returns one `Submission` that tracks its
|
|
517
|
+
settlement.
|
|
518
|
+
|
|
519
|
+
`Harness.open()` binds the Session to one application-owned registry (section
|
|
520
|
+
7.1). `createRegistry()` pre-registers the built-in task definitions (section 8);
|
|
521
|
+
they cannot be disposed or replaced, and open rejects a registry whose snapshot
|
|
522
|
+
lacks any of them or the built-in `pi` conversation setup.
|
|
523
|
+
Open changes surviving `running` tasks to `pending` and does nothing else to
|
|
524
|
+
task records: it never migrates or terminalizes a task because its definition
|
|
525
|
+
is missing or unmigratable. Such a task stays `pending` and is **blocked**: the
|
|
526
|
+
scheduler skips it and reconsiders it whenever the registry changes (section
|
|
527
|
+
5.4). Any built-in document touched by recovery migrates through its ordinary
|
|
528
|
+
typed access path. Open does not scan or migrate other documents. No handler
|
|
529
|
+
dispatches during open. Applications should register tasks before open so
|
|
530
|
+
recovered work can resume immediately; registration after open also unblocks it.
|
|
531
|
+
|
|
532
|
+
The root conversation always has reserved ID `ROOT_CONVERSATION_ID` (`1`).
|
|
533
|
+
`root(context, { init })` creates it lazily: when the root is absent, one commit
|
|
534
|
+
on the Session line creates the ownerless root, its default configuration, and
|
|
535
|
+
runs `init(tx, rootId)`. When the root already exists, `init` is ignored and no
|
|
536
|
+
write occurs. Reopen finds the same root by its reserved ID. A conversation with
|
|
537
|
+
no configured model produces a durable `no_model` generation failure.
|
|
396
538
|
|
|
397
539
|
`resume()` is idempotent while running and only enables scheduling. It does not
|
|
398
|
-
repeat open-time reconciliation
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
creates
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
540
|
+
repeat open-time reconciliation, and it throws after close. Work that must happen
|
|
541
|
+
before any task runs, such as registration or seeding, happens before
|
|
542
|
+
`resume()`. Calls that ask for progress also enable scheduling, so they never
|
|
543
|
+
wait on a paused Harness: `Conversation.submit()`, `Submission.wait()`,
|
|
544
|
+
`Harness.waitForTask()`, `Harness.waitForIdle()`, and
|
|
545
|
+
`Conversation.waitForIdle()`. Recovered work starts with them. A viewer that only
|
|
546
|
+
reads never enables scheduling.
|
|
547
|
+
|
|
548
|
+
`createConversation({ ownership, init })` and
|
|
549
|
+
`fork(at, { ownership, init })` commit atomically: the conversation, its
|
|
550
|
+
explicitly selected ownership, its configuration, and every write made by `init`.
|
|
551
|
+
A first input is an ordinary `submit()` afterward. A request ID makes a retried
|
|
552
|
+
`submit()` to the same conversation exactly-once; a host that must survive a
|
|
553
|
+
crash between the two calls first finds its conversation again, for example
|
|
554
|
+
through a key written by `init`. Host callers must choose ownerless or task
|
|
555
|
+
ownership; neither the Harness nor a conversation handle infers ownership from
|
|
556
|
+
call context. A new independent conversation receives the default configuration
|
|
557
|
+
before `init` runs. A fork receives the configuration visible at `at` through
|
|
558
|
+
the ordinary `asOf` fork policy; `init` may then override it with
|
|
559
|
+
`tx.doc(ConversationConfig, id)`. Other documents follow their own definitions
|
|
560
|
+
without special handling.
|
|
561
|
+
|
|
562
|
+
Every Harness commit that creates or forks a conversation runs the registry's
|
|
563
|
+
conversation setups (section 7.1) in the same commit, whether through the
|
|
564
|
+
conveniences or through raw `tx.createConversation()` and `tx.forkConversation()`,
|
|
565
|
+
for example inside a tool commit. The built-in `pi` setup runs first: an
|
|
566
|
+
independent conversation gets the default configuration and a fork keeps its
|
|
567
|
+
`asOf` copy, and both get an empty `pi.live`. Applications register setups for
|
|
568
|
+
their own documents the same way. The conveniences add only `init` and its checks,
|
|
569
|
+
which run after every setup. A conversation created by a plain Session has no
|
|
570
|
+
built-in documents; its configuration reads as
|
|
571
|
+
`ConversationConfig.definition.initial()` until something writes it.
|
|
572
|
+
|
|
573
|
+
The built-in conversation configuration document is final at version 1:
|
|
574
|
+
|
|
575
|
+
| field | value |
|
|
576
|
+
|---|---|
|
|
577
|
+
| kind | `pi.conversation.config` |
|
|
578
|
+
| version | `1` (no migration) |
|
|
579
|
+
| scope/history/fork | conversation, `rewindable`, `asOf` |
|
|
580
|
+
| schema | `ConversationConfigState` |
|
|
581
|
+
| `initial()` | `{ thinkingLevel: "off", activeTools: [] }` |
|
|
582
|
+
| checkpoint | complete base on every change |
|
|
583
|
+
| view mount | `docs["pi.conversation.config"]` |
|
|
584
|
+
|
|
585
|
+
It has no prompt sections: prompt text is produced per request by the registry
|
|
586
|
+
(section 7.4). Any code may edit it with
|
|
587
|
+
`tx.doc(ConversationConfig, id)`, including in the same commit as a create or
|
|
588
|
+
fork. The getters return immutable committed values, falling back to
|
|
589
|
+
`initial()` when the document is absent; they never write. `getStreamOptions()`
|
|
590
|
+
returns `{}` and `getRetryPolicy()` returns the default policy
|
|
591
|
+
`{ enabled: true, maxRetries: 3, baseDelayMs: 2000, maxAgentDelayMs: 60000 }`
|
|
592
|
+
when the field is absent; `setRetryPolicy(undefined)` removes the field.
|
|
593
|
+
`streamOptions` are forwarded to every generation request of the conversation;
|
|
594
|
+
`streamOptions.maxRetries` are provider retries inside one request, while `retry`
|
|
595
|
+
governs durable generation attempts (section 8). Each setter performs
|
|
596
|
+
one ordinary Session commit against the document. A setter does not start
|
|
424
597
|
generation or append a system entry. Request preparation later compares the
|
|
425
|
-
desired
|
|
598
|
+
desired configuration with transcript history and appends the required
|
|
426
599
|
positional system baseline or delta.
|
|
427
600
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
601
|
+
Default active tools for a root or independent conversation, including one
|
|
602
|
+
created through `tx.createConversation()` in a Harness commit, are the names of
|
|
603
|
+
every tool registered when its creation commit runs, in registry order.
|
|
604
|
+
`setActiveTools()` rejects the whole operation without a write when a name is
|
|
605
|
+
duplicated or when a name that was not already active in that conversation is
|
|
606
|
+
not registered. The create/fork conveniences check only one thing about `init`
|
|
607
|
+
writes: names that `init` newly activates must be registered. Names that were
|
|
608
|
+
already active are never revalidated, so stale unregistered names survive every
|
|
609
|
+
edit. All other raw document writes are trusted and unchecked, so other writers,
|
|
610
|
+
such as post-tools `addTools`, never fault because of registry movement. Readers tolerate what raw
|
|
611
|
+
writes can produce: request preparation offers the first occurrence of a
|
|
612
|
+
duplicated name, and post-tools `addTools` appends only names not already
|
|
613
|
+
active.
|
|
614
|
+
|
|
615
|
+
`init` runs after the conversation creation, which is a table write, so table
|
|
616
|
+
reads inside it throw `ReadAfterWrite` (section 4). Document access remains
|
|
617
|
+
available.
|
|
618
|
+
|
|
619
|
+
The configuration document records the desired loadout; the registry records
|
|
620
|
+
what this process can execute now. An active name may therefore be unregistered,
|
|
621
|
+
for example during extension reload, after restart before registration, or in a
|
|
622
|
+
fork opened by a process that lacks an extension. The document is never
|
|
623
|
+
rewritten because of registry movement, so a re-registered tool becomes available
|
|
624
|
+
again without host action. Hosts derive current availability as active names
|
|
625
|
+
that are currently registered.
|
|
626
|
+
|
|
627
|
+
A missing tool implementation never fails a request. Request preparation offers
|
|
628
|
+
only active names that are currently registered. If the replayed tool state still
|
|
629
|
+
offers an unregistered name, the appended system delta lists it in
|
|
630
|
+
`toolsRemoved`; when the name is registered again, a later delta adds its current
|
|
631
|
+
declaration. When the model calls a tool that is not offered or whose
|
|
632
|
+
implementation is unavailable at execution time, the tool task appends an error
|
|
633
|
+
tool result with `details: { code: "tool_unavailable" }` stating that the tool is
|
|
634
|
+
not available, and the run continues so the model can react.
|
|
451
635
|
|
|
452
636
|
A `Conversation.commit()` is a Session commit bound to that conversation.
|
|
453
637
|
`tx.createTask()` defaults `TaskOptions.conversationId` to the bound conversation.
|
|
@@ -456,57 +640,74 @@ fork-aware stored history; callers cannot substitute another conversation ID.
|
|
|
456
640
|
Generic Session-wide document operations remain available directly on `Harness`
|
|
457
641
|
because `Harness extends Session`.
|
|
458
642
|
|
|
459
|
-
`fork()` requires a concrete visible parent entry and
|
|
643
|
+
`fork()` requires a concrete visible parent entry and explicit ownership, then
|
|
644
|
+
applies section 3.7.
|
|
460
645
|
`collapse()` returns the newly admitted background collapse task ID, not its
|
|
461
646
|
future summary entry. `reset()` durably admits a passive self-head reset or
|
|
462
647
|
handoff write and then resolves; while busy, placement follows section 6 and may
|
|
463
648
|
occur later. Observe its placement through the conversation watch. An idle wait
|
|
464
649
|
does not guarantee placement of queued passive writes.
|
|
465
650
|
|
|
466
|
-
`
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
non-background tasks
|
|
471
|
-
resolves only after that
|
|
472
|
-
background
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
and therefore not idle. Cancelling an idle
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
`
|
|
484
|
-
creates a user message with the admission timestamp
|
|
485
|
-
`followUp`.
|
|
486
|
-
generation.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
is the explicit queued
|
|
651
|
+
`abortTask()` commits `abortRequested` and the durable foreground-subtree
|
|
652
|
+
cascade, then signals and joins the active run; the scheduler then starts the
|
|
653
|
+
abort invocation. Marking without signalling is internal: the cascade marks
|
|
654
|
+
owned tasks in the same commit. `Conversation.abort()` withdraws queued
|
|
655
|
+
input submissions, marks non-background tasks selected by ordinary ownership
|
|
656
|
+
traversal, signals them, and resolves only after that scope is ordinarily idle.
|
|
657
|
+
Passive writes and background subtrees survive. Conversation idle means no live
|
|
658
|
+
non-background task selected from that conversation. Harness idle applies the
|
|
659
|
+
same traversal from every ownerless conversation root. Pending dependency- or
|
|
660
|
+
deadline-blocked work is still live and therefore not idle. Cancelling an idle
|
|
661
|
+
wait aborts only that waiter.
|
|
662
|
+
|
|
663
|
+
Conversation handles are stateless; compare them by `id`. Hosts discover
|
|
664
|
+
conversations through lookups and scans. An activity view that lists active
|
|
665
|
+
conversations and reports conversations becoming active or idle is specified
|
|
666
|
+
with run control (Package 17); there is no creation listener.
|
|
667
|
+
|
|
668
|
+
`submit()` returns after durable admission, not settlement. An input submission
|
|
669
|
+
creates a user message with the admission timestamp; `whenBusy` defaults to
|
|
670
|
+
`followUp`. A write submission uses the ordered passive path in section 6 and
|
|
671
|
+
never starts generation. `Submission.wait()` settles an input only after its
|
|
672
|
+
answer or terminal failure; it settles a write when the entry is placed or the
|
|
673
|
+
write becomes terminally unplaceable. Cancelling `wait()` only cancels that wait.
|
|
674
|
+
It does not withdraw the submission; `Submission.abort()` is the explicit queued
|
|
675
|
+
withdrawal operation. `Harness.submission()` reacquires a submission after
|
|
676
|
+
reopen; records remain queryable after settlement.
|
|
490
677
|
|
|
491
678
|
`abortTask()` durably requests cancellation and returns `marked` after the mark
|
|
492
679
|
is committed, any active run invocation has joined, and an abort invocation has
|
|
493
|
-
been scheduled
|
|
680
|
+
been scheduled, or a blocked task has been settled as `orphaned` (section 5.4);
|
|
681
|
+
it does not await terminal settlement. `waitForTask()` observes
|
|
494
682
|
the terminal receipt. Aborting an already terminal task returns `terminal`; an
|
|
495
683
|
unknown ID rejects. Explicit task abort includes a background task. Cancelling a
|
|
496
684
|
task or idle wait does not abort work.
|
|
497
685
|
|
|
498
|
-
`
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
686
|
+
`inspect()` returns live work at one point on the Session line, for recovery
|
|
687
|
+
decisions after open, viewers, and diagnostics. It writes nothing, does not
|
|
688
|
+
enable scheduling, and runs no task code. Each pending or running task carries
|
|
689
|
+
its state under the current registry: `running` with an active invocation;
|
|
690
|
+
`ready` when the next scheduling pass would reserve it, with `migrates` when its
|
|
691
|
+
definition is newer and has `migrate`; `waiting` on unfinished dependencies; or
|
|
692
|
+
`blocked` (section 5.4). A migration shows as failed only after the scheduler
|
|
693
|
+
tried it, or when the newer definition has no `migrate`; inspection never runs
|
|
694
|
+
one to find out. Blocked reasons are derived, never stored. Queued and placed
|
|
695
|
+
submissions and the registry's wrapper failures complete the view. Finished
|
|
696
|
+
tasks, settled submissions, transcripts, and documents use their own APIs.
|
|
697
|
+
|
|
698
|
+
`Conversation.viewState()` returns its current structural mount as a disposable
|
|
699
|
+
read-only Chord state. `Conversation.watch()` atomically captures that immutable
|
|
700
|
+
revision and registers for later exact complete-commit frames. Its `WatchHandle`
|
|
701
|
+
uses the same serialized asynchronous, bounded-buffer contract as `watchDoc()`
|
|
702
|
+
in section 9.2. Neither carries semantic events or owns a second persistence
|
|
703
|
+
authority.
|
|
704
|
+
|
|
705
|
+
`close()` seals mutation admission and
|
|
706
|
+
task reservation, signals invocations, and stops future watch deliveries. Outside
|
|
707
|
+
the Session line it lets already-admitted storage commits settle, joins
|
|
708
|
+
task/tool/hook invocations, then closes states and storage. Already-running watch
|
|
709
|
+
callbacks remain caller-owned and may finish independently. Close writes no task
|
|
710
|
+
outcome. Handles belong to that open Harness and must be reacquired after reopen.
|
|
510
711
|
|
|
511
712
|
## 3. Documents
|
|
512
713
|
|
|
@@ -534,12 +735,17 @@ type DocumentSemantics =
|
|
|
534
735
|
| RewindableConversationSemantics
|
|
535
736
|
| { readonly scope: "task" };
|
|
536
737
|
|
|
738
|
+
type CheckpointInfo = {
|
|
739
|
+
/** Deltas already stored after the newest base, excluding this change. */
|
|
740
|
+
readonly deltasSinceBase: number;
|
|
741
|
+
};
|
|
742
|
+
|
|
537
743
|
type CommonDocDefinition<T extends JsonObject> = {
|
|
538
744
|
readonly kind: string;
|
|
539
745
|
readonly version: number;
|
|
540
746
|
initial(): T;
|
|
541
747
|
migrate?(value: JsonObject, fromVersion: number): T;
|
|
542
|
-
checkpointWhen?(value: Readonly<T>, ops: readonly Op[]): boolean;
|
|
748
|
+
checkpointWhen?(value: Readonly<T>, ops: readonly Op[], info: CheckpointInfo): boolean;
|
|
543
749
|
};
|
|
544
750
|
|
|
545
751
|
type DocDefinition<T extends JsonObject> =
|
|
@@ -548,45 +754,100 @@ type DocDefinition<T extends JsonObject> =
|
|
|
548
754
|
type DocFamilyDefinition<T extends JsonObject, I extends JsonValue> =
|
|
549
755
|
Omit<CommonDocDefinition<T>, "initial"> & DocumentSemantics & {
|
|
550
756
|
readonly family: true;
|
|
551
|
-
initial(
|
|
757
|
+
initial(seed: I): T;
|
|
552
758
|
};
|
|
553
759
|
|
|
554
|
-
function defineDoc<T extends JsonObject>(
|
|
555
|
-
definition: CommonDocDefinition<T> & RewindableConversationSemantics,
|
|
556
|
-
): RewindableDocToken<T>;
|
|
557
|
-
function defineDoc<T extends JsonObject>(definition: DocDefinition<T>): DocToken<T>;
|
|
558
|
-
|
|
559
|
-
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
560
|
-
definition: Omit<CommonDocDefinition<T>, "initial"> &
|
|
561
|
-
RewindableConversationSemantics & {
|
|
562
|
-
readonly family: true;
|
|
563
|
-
initial(input: I): T;
|
|
564
|
-
},
|
|
565
|
-
): RewindableDocFamilyToken<T, I>;
|
|
566
|
-
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
567
|
-
definition: DocFamilyDefinition<T, I>,
|
|
568
|
-
): DocFamilyToken<T, I>;
|
|
569
|
-
|
|
570
760
|
declare const docType: unique symbol;
|
|
571
|
-
interface DocToken<T extends JsonObject
|
|
572
|
-
readonly definition:
|
|
761
|
+
interface DocToken<T extends JsonObject, D extends DocDefinition<T>> {
|
|
762
|
+
readonly definition: D;
|
|
573
763
|
readonly [docType]?: T;
|
|
574
764
|
}
|
|
575
|
-
interface
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
765
|
+
interface DocFamilyToken<
|
|
766
|
+
T extends JsonObject,
|
|
767
|
+
I extends JsonValue,
|
|
768
|
+
D extends DocFamilyDefinition<T, I>,
|
|
769
|
+
> {
|
|
770
|
+
readonly definition: D;
|
|
580
771
|
readonly [docType]?: T;
|
|
581
772
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
773
|
+
|
|
774
|
+
type SessionDocToken<T extends JsonObject> = DocToken<
|
|
775
|
+
T,
|
|
776
|
+
CommonDocDefinition<T> & { readonly scope: "session" }
|
|
777
|
+
>;
|
|
778
|
+
type ConversationDocToken<T extends JsonObject> = DocToken<
|
|
779
|
+
T,
|
|
780
|
+
CommonDocDefinition<T> & (LatestConversationSemantics | RewindableConversationSemantics)
|
|
781
|
+
>;
|
|
782
|
+
type RewindableConversationDocToken<T extends JsonObject> = DocToken<
|
|
783
|
+
T,
|
|
784
|
+
CommonDocDefinition<T> & RewindableConversationSemantics
|
|
785
|
+
>;
|
|
786
|
+
type TaskDocToken<T extends JsonObject> = DocToken<
|
|
787
|
+
T,
|
|
788
|
+
CommonDocDefinition<T> & { readonly scope: "task" }
|
|
789
|
+
>;
|
|
790
|
+
|
|
791
|
+
type SessionDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
792
|
+
T,
|
|
793
|
+
I,
|
|
794
|
+
DocFamilyDefinition<T, I> & { readonly scope: "session" }
|
|
795
|
+
>;
|
|
796
|
+
type ConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
797
|
+
T,
|
|
798
|
+
I,
|
|
799
|
+
DocFamilyDefinition<T, I> & (LatestConversationSemantics | RewindableConversationSemantics)
|
|
800
|
+
>;
|
|
801
|
+
type RewindableConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
802
|
+
T,
|
|
803
|
+
I,
|
|
804
|
+
DocFamilyDefinition<T, I> & RewindableConversationSemantics
|
|
805
|
+
>;
|
|
806
|
+
type TaskDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
807
|
+
T,
|
|
808
|
+
I,
|
|
809
|
+
DocFamilyDefinition<T, I> & { readonly scope: "task" }
|
|
810
|
+
>;
|
|
811
|
+
|
|
812
|
+
function defineDoc<T extends JsonObject>(
|
|
813
|
+
definition: CommonDocDefinition<T> & { readonly scope: "session" },
|
|
814
|
+
): SessionDocToken<T>;
|
|
815
|
+
function defineDoc<T extends JsonObject>(
|
|
816
|
+
definition: CommonDocDefinition<T> & LatestConversationSemantics,
|
|
817
|
+
): ConversationDocToken<T>;
|
|
818
|
+
function defineDoc<T extends JsonObject>(
|
|
819
|
+
definition: CommonDocDefinition<T> & RewindableConversationSemantics,
|
|
820
|
+
): RewindableConversationDocToken<T>;
|
|
821
|
+
function defineDoc<T extends JsonObject>(
|
|
822
|
+
definition: CommonDocDefinition<T> & { readonly scope: "task" },
|
|
823
|
+
): TaskDocToken<T>;
|
|
824
|
+
|
|
825
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
826
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & {
|
|
827
|
+
readonly family: true;
|
|
828
|
+
readonly scope: "session";
|
|
829
|
+
initial(seed: I): T;
|
|
830
|
+
},
|
|
831
|
+
): SessionDocFamilyToken<T, I>;
|
|
832
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
833
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & LatestConversationSemantics & {
|
|
834
|
+
readonly family: true;
|
|
835
|
+
initial(seed: I): T;
|
|
836
|
+
},
|
|
837
|
+
): ConversationDocFamilyToken<T, I>;
|
|
838
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
839
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & RewindableConversationSemantics & {
|
|
840
|
+
readonly family: true;
|
|
841
|
+
initial(seed: I): T;
|
|
842
|
+
},
|
|
843
|
+
): RewindableConversationDocFamilyToken<T, I>;
|
|
844
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
845
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & {
|
|
846
|
+
readonly family: true;
|
|
847
|
+
readonly scope: "task";
|
|
848
|
+
initial(seed: I): T;
|
|
849
|
+
},
|
|
850
|
+
): TaskDocFamilyToken<T, I>;
|
|
590
851
|
```
|
|
591
852
|
|
|
592
853
|
Validation rules:
|
|
@@ -612,25 +873,25 @@ case model, tool, inbox, or presentation state.
|
|
|
612
873
|
|
|
613
874
|
### 3.2 Records and lifetimes
|
|
614
875
|
|
|
615
|
-
A persisted document instance has one
|
|
876
|
+
A persisted document instance has one `DocumentRecord`:
|
|
616
877
|
|
|
617
878
|
```ts
|
|
618
879
|
type DocumentRecord = {
|
|
619
|
-
readonly id:
|
|
880
|
+
readonly id: DocumentId; // unique incarnation
|
|
620
881
|
readonly kind: string; // stable definition kind
|
|
621
882
|
readonly key?: string; // families only
|
|
622
883
|
readonly createdAt: Seq; // stamped by the committing storage
|
|
623
884
|
readonly retiredAt?: Seq;
|
|
624
885
|
} & (
|
|
625
886
|
| { readonly scope: { readonly kind: "session" } }
|
|
626
|
-
| ({ readonly scope: { readonly kind: "conversation"; readonly conversationId:
|
|
887
|
+
| ({ readonly scope: { readonly kind: "conversation"; readonly conversationId: ConversationId } } & (
|
|
627
888
|
| { readonly history: "latest"; readonly fork: "current" | "initial" }
|
|
628
889
|
| {
|
|
629
890
|
readonly history: "rewindable";
|
|
630
891
|
readonly fork: "asOf" | "current" | "initial";
|
|
631
892
|
}
|
|
632
893
|
))
|
|
633
|
-
| { readonly scope: { readonly kind: "task"; readonly taskId:
|
|
894
|
+
| { readonly scope: { readonly kind: "task"; readonly taskId: TaskId } }
|
|
634
895
|
);
|
|
635
896
|
|
|
636
897
|
type DocumentCreate = DocumentRecord extends infer Record
|
|
@@ -659,117 +920,121 @@ There is no mutable `session.document()` API.
|
|
|
659
920
|
|
|
660
921
|
```ts
|
|
661
922
|
interface Session extends DocumentObserver {
|
|
662
|
-
commit<T>(
|
|
663
|
-
change: (tx: Tx) => T | Promise<T>,
|
|
664
|
-
context: Context,
|
|
665
|
-
): Promise<T>;
|
|
666
|
-
|
|
923
|
+
commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
|
|
667
924
|
close(context: Context): Promise<void>;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
): Promise<Readonly<T
|
|
674
|
-
|
|
675
|
-
snapshot<T extends JsonObject, I extends JsonValue>(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
): Promise<Readonly<T
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
): Promise<
|
|
687
|
-
|
|
688
|
-
snapshotAsOf<T extends JsonObject, I extends JsonValue>(
|
|
689
|
-
token: RewindableDocFamilyToken<T, I>,
|
|
690
|
-
target: HistoricalFamilyTarget,
|
|
691
|
-
at: Id,
|
|
692
|
-
context: Context,
|
|
693
|
-
): Promise<Readonly<T> | undefined>;
|
|
694
|
-
|
|
695
|
-
documentSource<T extends JsonObject>(
|
|
696
|
-
token: DocToken<T>,
|
|
697
|
-
target: DocTarget,
|
|
698
|
-
context: Context,
|
|
699
|
-
): Promise<DocumentSource<T>>;
|
|
700
|
-
|
|
701
|
-
documentSource<T extends JsonObject, I extends JsonValue>(
|
|
702
|
-
token: DocFamilyToken<T, I>,
|
|
703
|
-
target: FamilyTarget<I>,
|
|
704
|
-
context: Context,
|
|
705
|
-
): Promise<DocumentSource<T>>;
|
|
925
|
+
subscribeCommits(listener: (publication: CommitPublication, context: Context) => void): () => void;
|
|
926
|
+
subscribeClose(listener: () => void): () => void;
|
|
927
|
+
|
|
928
|
+
snapshot<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<Readonly<T> | undefined>;
|
|
929
|
+
snapshot<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<Readonly<T> | undefined>;
|
|
930
|
+
snapshot<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<Readonly<T> | undefined>;
|
|
931
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
932
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
933
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
934
|
+
|
|
935
|
+
snapshotAsOf<T extends JsonObject>(token: RewindableConversationDocToken<T>, conversationId: ConversationId, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
|
|
936
|
+
snapshotAsOf<T extends JsonObject, I extends JsonValue>(token: RewindableConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
|
|
937
|
+
|
|
938
|
+
documentState<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentState<T> | undefined>;
|
|
939
|
+
documentState<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<DocumentState<T> | undefined>;
|
|
940
|
+
documentState<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<DocumentState<T> | undefined>;
|
|
941
|
+
documentState<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentState<T> | undefined>;
|
|
942
|
+
documentState<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
|
|
943
|
+
documentState<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
|
|
706
944
|
}
|
|
707
945
|
|
|
708
946
|
interface Tx {
|
|
709
|
-
conversation(id:
|
|
710
|
-
entry(id:
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
947
|
+
conversation(id: ConversationId): Promise<ConversationRecord | undefined>;
|
|
948
|
+
entry(id: EntryId): Promise<EntryRecord | undefined>;
|
|
949
|
+
/** Undefined when the entry is absent or has another kind. */
|
|
950
|
+
entry<D extends JsonValue>(token: Entry<D>, id: EntryId): Promise<TypedEntry<D> | undefined>;
|
|
951
|
+
task(id: TaskId): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
|
|
952
|
+
scanConversations(query: ConversationQuery, limit: number, cursor?: Cursor): Promise<Page<ConversationRecord, Cursor>>;
|
|
953
|
+
scanEntries(query: EntryQuery, limit: number, cursor?: Cursor): Promise<Page<EntryRecord, Cursor>>;
|
|
954
|
+
scanTasks(query: TaskQuery, limit: number, cursor?: Cursor): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
|
|
955
|
+
|
|
956
|
+
createConversation(options: { readonly ownership: ConversationOwnership }): Promise<ConversationRecord>;
|
|
957
|
+
forkConversation(
|
|
958
|
+
parentConversationId: ConversationId,
|
|
959
|
+
at: EntryId,
|
|
960
|
+
options: { readonly ownership: ConversationOwnership },
|
|
961
|
+
): Promise<ConversationRecord>;
|
|
962
|
+
appendEntry(conversationId: ConversationId, value: EntryDraft): Promise<EntryRecord>;
|
|
963
|
+
/** The token supplies `kind` and types `data`. */
|
|
964
|
+
appendEntry<D extends JsonValue>(
|
|
965
|
+
token: Entry<D>, conversationId: ConversationId, value: TypedEntryDraft<D>,
|
|
966
|
+
): Promise<TypedEntry<D>>;
|
|
720
967
|
createTask<I, S extends { phase: string }, R, H extends object>(
|
|
721
968
|
task: Task<I, S, R, H>, input: I, options?: TaskOptions,
|
|
722
|
-
):
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
doc<T extends JsonObject
|
|
727
|
-
|
|
728
|
-
|
|
969
|
+
): Promise<TaskId<R>>;
|
|
970
|
+
/** Settle a queued or placed submission; only a placed input can be answered. A settled one stays unchanged. */
|
|
971
|
+
settleSubmission(id: SubmissionId, settlement: SubmissionSettlement): void;
|
|
972
|
+
|
|
973
|
+
doc<T extends JsonObject>(token: SessionDocToken<T>): Promise<Draft<T>>;
|
|
974
|
+
doc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId): Promise<Draft<T>>;
|
|
975
|
+
doc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId): Promise<Draft<T>>;
|
|
976
|
+
doc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, seed: I): Promise<Draft<T>>;
|
|
977
|
+
doc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, seed: I): Promise<Draft<T>>;
|
|
978
|
+
doc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, seed: I): Promise<Draft<T>>;
|
|
979
|
+
|
|
980
|
+
retireDoc<T extends JsonObject>(token: SessionDocToken<T>): Promise<void>;
|
|
981
|
+
retireDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId): Promise<void>;
|
|
982
|
+
retireDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId): Promise<void>;
|
|
983
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string): Promise<void>;
|
|
984
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string): Promise<void>;
|
|
985
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string): Promise<void>;
|
|
729
986
|
}
|
|
730
|
-
|
|
731
|
-
type DocTarget =
|
|
732
|
-
| { readonly scope: "session" }
|
|
733
|
-
| { readonly scope: "conversation"; readonly conversationId: Id }
|
|
734
|
-
| { readonly scope: "task"; readonly taskId: Id };
|
|
735
|
-
|
|
736
|
-
type FamilyKey = DocTarget & { readonly key: string };
|
|
737
|
-
|
|
738
|
-
type FamilyTarget<I> = FamilyKey & { readonly initial: I };
|
|
739
|
-
|
|
740
|
-
type HistoricalFamilyTarget = {
|
|
741
|
-
readonly scope: "conversation";
|
|
742
|
-
readonly conversationId: Id;
|
|
743
|
-
readonly key: string;
|
|
744
|
-
};
|
|
745
987
|
```
|
|
746
988
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
989
|
+
ID-creating transaction methods are asynchronous because remote storage may
|
|
990
|
+
allocate globally unique numeric IDs durably. Conversation creation always
|
|
991
|
+
requires explicit ownership; no transaction wrapper injects the executing task.
|
|
992
|
+
For task ownership, the caller supplies only a typed task ID. The Session derives
|
|
993
|
+
the persisted owner conversation from the task's final candidate record.
|
|
994
|
+
|
|
995
|
+
An owner task may be committed or staged earlier in the same transaction. Before
|
|
996
|
+
Storage admission, the Session rejects a missing, terminal, or abort-marked owner,
|
|
997
|
+
including one made terminal or abort-marked later in that transaction. Existing
|
|
998
|
+
owner edges remain valid when their owners terminalize afterward. Conversation
|
|
999
|
+
creation returns an inert record, never an operational handle.
|
|
1000
|
+
`forkConversation()` additionally validates one visible entry and applies section
|
|
1001
|
+
3.7. Only public typed `tx.doc()` is get-or-create. Internal fork copying may create
|
|
1002
|
+
new incarnations directly from stored values without a definition. `tx.doc()`
|
|
1003
|
+
receives the definition token that supplies
|
|
1004
|
+
its static type, initializer, migration, and checkpoint policy. Scope-preserving
|
|
1005
|
+
token overloads require callers to supply only the concrete conversation/task ID
|
|
1006
|
+
and, for a family, its key and creation seed. Ordinary definitions are not
|
|
1007
|
+
registered and ordinary documents are not scanned at open.
|
|
751
1008
|
|
|
752
1009
|
- Existing instances are reconstructed and migrated lazily according to section
|
|
753
|
-
3.6.
|
|
754
|
-
|
|
755
|
-
-
|
|
756
|
-
|
|
757
|
-
- The
|
|
758
|
-
|
|
1010
|
+
3.6. A migration reached through `tx.doc()` is staged in its enclosing
|
|
1011
|
+
transaction.
|
|
1012
|
+
- A missing singleton calls the token's `initial()`. A missing family member calls
|
|
1013
|
+
`initial(seed)`. Creation stores an initial base.
|
|
1014
|
+
- The first acquisition of one logical address is memoized before awaiting. Later
|
|
1015
|
+
acquisitions in that transaction return the same draft; for a missing family,
|
|
1016
|
+
the first call's detached seed wins and later seeds are ignored.
|
|
1017
|
+
- `snapshot()`, `snapshotAsOf()`, `documentState()`, and `watchDoc()` never create
|
|
1018
|
+
or persist migration. They return `undefined` when the requested incarnation is
|
|
1019
|
+
absent and migrate a reconstructed value only in memory.
|
|
1020
|
+
- Task-scoped `tx.doc()` validates against the transaction's latest candidate task
|
|
759
1021
|
record, falling back to committed state. This internal validation is not a
|
|
760
1022
|
caller table read and does not trigger `ReadAfterWrite`. Its conversation is
|
|
761
|
-
derived from the task record
|
|
1023
|
+
derived from the task record.
|
|
1024
|
+
- `retireDoc()` resolves the logical address without creating it. Retirement of
|
|
1025
|
+
an acquired draft persists its final content before retirement. A later
|
|
1026
|
+
`tx.doc()` at that address in the same transaction creates a new incarnation
|
|
1027
|
+
with a new draft and ID.
|
|
762
1028
|
- A terminal candidate rejects later task-document access. Terminal settlement
|
|
763
1029
|
retires both existing task documents and task documents created earlier in the
|
|
764
1030
|
same transaction.
|
|
765
|
-
- `snapshot()` returns
|
|
766
|
-
|
|
767
|
-
|
|
1031
|
+
- `snapshot()` returns the current shareable immutable revision. Mutation of it
|
|
1032
|
+
or any retained descendant is unsupported; callers that need mutable ownership
|
|
1033
|
+
must copy it first.
|
|
1034
|
+
- `documentState()` returns a disposable read-only Chord state bound to one committed incarnation.
|
|
1035
|
+
- `tx.doc()` returns one revocable Astra overlay `Draft<T>` for the transaction.
|
|
768
1036
|
- Historical reads never create documents in the past.
|
|
769
1037
|
|
|
770
|
-
A family initializer input matters only on first creation. Later accesses must
|
|
771
|
-
supply the same logical target; their `initial` value is ignored.
|
|
772
|
-
|
|
773
1038
|
`snapshotAsOf()` is available only for rewindable conversation documents. It
|
|
774
1039
|
validates that `at` is visible through the requested conversation's ancestry,
|
|
775
1040
|
selects the ancestor conversation that owns that entry, then finds the logical
|
|
@@ -784,51 +1049,119 @@ alive at the target commit.
|
|
|
784
1049
|
|
|
785
1050
|
### 3.4 Mutation ownership
|
|
786
1051
|
|
|
787
|
-
|
|
788
|
-
|
|
1052
|
+
Pico uses Chord Delta's Astra-immutable transaction shape directly:
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
interface Prepared<T extends object> {
|
|
1056
|
+
readonly base: T;
|
|
1057
|
+
readonly value: T;
|
|
1058
|
+
readonly ops: readonly Op[];
|
|
1059
|
+
readonly baseRevision: number;
|
|
1060
|
+
abort(): void;
|
|
1061
|
+
}
|
|
1062
|
+
interface Change<T extends object> {
|
|
1063
|
+
readonly state: Draft<T>;
|
|
1064
|
+
prepare(): Prepared<T>;
|
|
1065
|
+
abort(): void;
|
|
1066
|
+
}
|
|
1067
|
+
interface Tracker<T extends object> {
|
|
1068
|
+
readonly value: T;
|
|
1069
|
+
readonly revision: number;
|
|
1070
|
+
beginChange(): Change<T>;
|
|
1071
|
+
prepareReplace(value: T): Prepared<T>;
|
|
1072
|
+
adopt(prepared: Prepared<T>): void;
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Each loaded document owns one tracker whose `value` is its current immutable
|
|
1077
|
+
revision. Immutability is a trusted ownership contract, not runtime freezing:
|
|
1078
|
+
a revision and its descendants must never be mutated after transfer to the
|
|
1079
|
+
tracker. `prepare()` revokes the draft, emits detached self-contained
|
|
1080
|
+
operations, and computes `value` with the optimized immutable applier before
|
|
1081
|
+
Storage admission. Unchanged subtrees are structurally shared with `base`.
|
|
1082
|
+
Operation placement payloads may also be shared with `value`; mutating either a
|
|
1083
|
+
prepared operation or an immutable revision is unsupported. Astra preserves an
|
|
1084
|
+
empty operation batch for changes it proves are no-ops, but Pico performs no
|
|
1085
|
+
additional whole-value equality pass for nonempty structural batches.
|
|
1086
|
+
|
|
1087
|
+
`abort()` is idempotent. `adopt()` accepts only a prepared result from that
|
|
1088
|
+
tracker at its current revision, then performs only a synchronous pointer swap
|
|
1089
|
+
to the already-computed immutable `value`. It performs no diffing, application,
|
|
1090
|
+
allocation, or callback. The Session line permits at most one open change per
|
|
1091
|
+
tracker and ensures no result becomes stale between preparation, Storage
|
|
1092
|
+
settlement, and adoption.
|
|
1093
|
+
|
|
1094
|
+
The first `tx.doc()` acquisition calls `tracker.beginChange()` and memoizes that
|
|
1095
|
+
change's draft for the rest of the possibly async Session callback.
|
|
789
1096
|
|
|
790
1097
|
Transaction behavior:
|
|
791
1098
|
|
|
792
1099
|
```text
|
|
793
1100
|
begin transaction
|
|
794
|
-
acquire document
|
|
795
|
-
mutate
|
|
796
|
-
callback
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1101
|
+
acquire and memoize document changes by logical address
|
|
1102
|
+
mutate revocable Astra overlay drafts
|
|
1103
|
+
callback settles
|
|
1104
|
+
seal Tx; synchronously prepare or abort every open change, revoking every draft
|
|
1105
|
+
if any acquisition is pending: abort open changes, reject, then drain and abort it
|
|
1106
|
+
callback fails with no pending acquisition
|
|
1107
|
+
abort every open change; persist and publish nothing
|
|
1108
|
+
callback succeeds with no pending acquisition
|
|
1109
|
+
prepare every open change -> immutable next revision + self-contained Chord Op[]
|
|
1110
|
+
Session evaluates each required/ordinary document write exactly once
|
|
1111
|
+
prepare every affected loaded conversation mount revision
|
|
1112
|
+
Storage.commit persists the atomic batch while the Session line remains held
|
|
803
1113
|
storage succeeds
|
|
804
|
-
|
|
805
|
-
adopt committed baselines and enqueue value/ops publication while holding the line
|
|
1114
|
+
adopt every prepared change by pointer swap and enqueue immutable revision/ops publication
|
|
806
1115
|
release the line; invoke listeners later
|
|
807
|
-
|
|
808
|
-
poison Session
|
|
1116
|
+
storage fails
|
|
1117
|
+
abort every prepared change, poison Session, and publish nothing
|
|
809
1118
|
```
|
|
810
1119
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1120
|
+
A pending acquisition that resolves after sealing never exposes a draft; its
|
|
1121
|
+
change is aborted and its promise rejects. The Session observes every such
|
|
1122
|
+
settlement before releasing the line. Initializer, migration, and replacement
|
|
1123
|
+
roots come from caller code: the Session copies each one into exclusive kernel
|
|
1124
|
+
ownership, rejecting any value that is not strict JSON, before it becomes an
|
|
1125
|
+
immutable tracker revision. Loaded and fork-copy roots are already detached
|
|
1126
|
+
strict JSON from Storage and enter the tracker without another copy. Chord's
|
|
1127
|
+
`track()` and `prepareReplace()` take ownership in O(1) without traversal, so
|
|
1128
|
+
every root they receive must come from one of these sources. Migration callbacks
|
|
1129
|
+
never receive a live tracker revision.
|
|
1130
|
+
|
|
1131
|
+
Preparation, validation, checkpoint, or mounted-view preparation failure occurs
|
|
1132
|
+
before Storage admission and rolls back normally. The Session performs no
|
|
1133
|
+
strict-JSON walk of prepared operations or selected bases: roots are checked on
|
|
1134
|
+
entry and Chord checks every draft placement, so every revision, operation
|
|
1135
|
+
payload, and base is strict JSON by construction. Tracker branding and `baseRevision` enforce ownership and staleness; the
|
|
1136
|
+
Session never substitutes caller-created prepared values. The prepared immutable
|
|
1137
|
+
candidate itself becomes the adopted and published value. Storage receives that complete value only when the
|
|
1138
|
+
Session selects a base; otherwise it receives only the prepared operation batch.
|
|
1139
|
+
An existing current-version document with an empty batch writes and publishes
|
|
1140
|
+
nothing. A nonempty structural batch whose final value is deeply equal to its
|
|
1141
|
+
base remains a valid durable change and publication. Creation and required
|
|
1142
|
+
version transitions still write a base when their prepared batch is empty; an
|
|
1143
|
+
equal-value version base does not emit a watch update.
|
|
1144
|
+
|
|
1145
|
+
Values assigned into a draft are copied immediately by value. Repeated
|
|
1146
|
+
placements are independent. Chord checks each placement while copying it and
|
|
1147
|
+
throws at the offending assignment, before the draft changes, when the value is
|
|
1148
|
+
not strict JSON: `undefined` array elements or nested object values, non-finite
|
|
1149
|
+
numbers, functions, symbols, bigints, accessors, symbol keys, sparse arrays, or
|
|
1150
|
+
objects whose prototype is neither `Object.prototype` nor `null`. Assigning
|
|
1151
|
+
`undefined` directly to an object property deletes that property. Draft reads, draft writes, and all `Tx` operations
|
|
1152
|
+
reject after the callback settles. The prepared immutable value remains readable
|
|
1153
|
+
by Session-owned checkpoint and Storage preparation.
|
|
821
1154
|
|
|
822
1155
|
```ts
|
|
823
|
-
let escaped: LiveState
|
|
1156
|
+
let escaped: Draft<LiveState>;
|
|
824
1157
|
await session.commit(async tx => {
|
|
825
|
-
escaped = await tx.doc(LiveDoc,
|
|
1158
|
+
escaped = await tx.doc(LiveDoc, conversationId);
|
|
826
1159
|
});
|
|
827
|
-
escaped.message = message; //
|
|
1160
|
+
escaped.message = message; // throws: the draft was revoked
|
|
828
1161
|
```
|
|
829
1162
|
|
|
830
|
-
|
|
831
|
-
|
|
1163
|
+
Fire-and-forget work that mutates a draft before the owner callback settles may
|
|
1164
|
+
silently enter that transaction and is unsupported.
|
|
832
1165
|
|
|
833
1166
|
### 3.5 Bases and checkpoints
|
|
834
1167
|
|
|
@@ -838,21 +1171,26 @@ For an ordinary later mutation, the definition alone decides whether the
|
|
|
838
1171
|
storage record is a base:
|
|
839
1172
|
|
|
840
1173
|
```ts
|
|
841
|
-
const useBase = definition.checkpointWhen?.(candidateValue, ops) ?? false;
|
|
1174
|
+
const useBase = definition.checkpointWhen?.(candidateValue, ops, { deltasSinceBase }) ?? false;
|
|
842
1175
|
```
|
|
843
1176
|
|
|
844
|
-
The Session evaluates this predicate exactly once after tracker
|
|
845
|
-
|
|
846
|
-
|
|
1177
|
+
The Session evaluates this predicate exactly once after tracker preparation.
|
|
1178
|
+
`deltasSinceBase` counts the deltas already stored after the incarnation's newest
|
|
1179
|
+
base, excluding the change being evaluated. Storage reports it when it
|
|
1180
|
+
materializes the current value; the Session advances it after each adopted write
|
|
1181
|
+
and resets it after every base, so evaluation performs no Storage read.
|
|
1182
|
+
Creation and version transitions require bases and do not call it. The Session
|
|
1183
|
+
then gives Storage only the selected representation:
|
|
847
1184
|
|
|
848
1185
|
```text
|
|
849
1186
|
required or predicate true -> base with complete value
|
|
850
|
-
otherwise -> delta with
|
|
1187
|
+
otherwise -> delta with the prepared Chord operation batch
|
|
851
1188
|
```
|
|
852
1189
|
|
|
853
|
-
A
|
|
854
|
-
|
|
855
|
-
|
|
1190
|
+
A Chord root-replacement operation remains a delta unless the definition selected
|
|
1191
|
+
a checkpoint; it does not authorize reclamation. Predicate failure aborts the
|
|
1192
|
+
prepared transaction before Storage admission. Storage executes no definition
|
|
1193
|
+
code and never receives an unused complete candidate with a selected delta.
|
|
856
1194
|
|
|
857
1195
|
- For Session, task, and latest conversation documents, a committed base permits
|
|
858
1196
|
physical reclamation of older records.
|
|
@@ -863,6 +1201,12 @@ an unused complete candidate with a selected delta.
|
|
|
863
1201
|
- Storage does not count encoded bytes, compare against `initial()`, or invent
|
|
864
1202
|
checkpoints.
|
|
865
1203
|
|
|
1204
|
+
A definition can bound replay directly:
|
|
1205
|
+
|
|
1206
|
+
```ts
|
|
1207
|
+
checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 31
|
|
1208
|
+
```
|
|
1209
|
+
|
|
866
1210
|
A high-churn live document can checkpoint when it becomes empty:
|
|
867
1211
|
|
|
868
1212
|
```ts
|
|
@@ -883,20 +1227,26 @@ no migrate -> reject older stored version
|
|
|
883
1227
|
```
|
|
884
1228
|
|
|
885
1229
|
`migrate()` is pure and returns a complete current-version value. Migration is
|
|
886
|
-
access-driven: `tx.doc()`, `snapshot()`, `snapshotAsOf()`, `
|
|
1230
|
+
access-driven: `tx.doc()`, `snapshot()`, `snapshotAsOf()`, `documentState()`, and
|
|
887
1231
|
`watchDoc()` reconstruct and migrate through the token supplied to that call.
|
|
888
1232
|
Harness open does not sweep ordinary documents.
|
|
889
1233
|
|
|
890
|
-
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1234
|
+
- Read-only access migrates only in memory and never writes. It may cache a
|
|
1235
|
+
tracker over that migrated immutable revision together with the older stored
|
|
1236
|
+
version marker; the next successful `tx.doc()` access still writes the required
|
|
1237
|
+
current-version base. A cached tracker serves only tokens of the version its value was
|
|
1238
|
+
materialized for. Access with any other version, such as a token from reloaded
|
|
1239
|
+
extension code, drops it and reloads the stored value, so each token migrates
|
|
1240
|
+
from the stored version or rejects a newer one. Migration always starts from a detached stored value.
|
|
1241
|
+
`tx.doc()` stages migration in its enclosing transaction; callback failure
|
|
1242
|
+
persists nothing, and later draft edits coalesce into one final required base.
|
|
894
1243
|
- Rewindable history is not rewritten. Current and historical reconstructed
|
|
895
1244
|
values are migrated after replay.
|
|
896
|
-
- The first
|
|
897
|
-
current-version base
|
|
898
|
-
- A fork
|
|
899
|
-
|
|
1245
|
+
- The first `tx.doc()` transaction after any stored-version migration writes a
|
|
1246
|
+
required current-version base, even when the migrated JSON is deeply equal.
|
|
1247
|
+
- A fork obtains the selected stored value/version from Storage rather than a
|
|
1248
|
+
typed migrated tracker cache. The child copies that stored pair and migrates on
|
|
1249
|
+
later typed access.
|
|
900
1250
|
- Unaccessed documents and documents with unavailable definitions preserve their stored instances,
|
|
901
1251
|
versions, and bytes.
|
|
902
1252
|
|
|
@@ -909,12 +1259,12 @@ appended later entries. Document state at `E` is the final state of the commit
|
|
|
909
1259
|
containing `E`. Different document states require separate commits.
|
|
910
1260
|
|
|
911
1261
|
Each conversation document follows the history/fork policy persisted in its
|
|
912
|
-
|
|
1262
|
+
`DocumentRecord`:
|
|
913
1263
|
|
|
914
1264
|
| conversation setting | child value |
|
|
915
1265
|
|---|---|
|
|
916
1266
|
| `fork: "asOf"` | parent value at `E`'s commit |
|
|
917
|
-
| `fork: "current"` | parent value when the fork commit runs |
|
|
1267
|
+
| `fork: "current"` | committed parent value selected when the fork commit runs |
|
|
918
1268
|
| `fork: "initial"` | no copied instance; initializer on first child access |
|
|
919
1269
|
|
|
920
1270
|
`current` and `asOf` copy logically present conversation singleton and family
|
|
@@ -924,33 +1274,65 @@ values become independent child instances with new IDs and initial bases.
|
|
|
924
1274
|
supplied definition. Task documents and tasks are never copied. Session documents
|
|
925
1275
|
remain shared and are not rewindable.
|
|
926
1276
|
|
|
1277
|
+
Fork copying reads committed stored values rather than typed tracker caches. A
|
|
1278
|
+
transaction that creates a fork therefore rejects if it also writes one of the
|
|
1279
|
+
parent's `fork: "current"` documents; commit the parent change first so the fork
|
|
1280
|
+
has one unambiguous stored source revision.
|
|
1281
|
+
|
|
1282
|
+
Forks stage backend-side `document.copy` commands carrying the child create
|
|
1283
|
+
record and an exact source incarnation/point. Storage materializes each source
|
|
1284
|
+
and persists its stored value/version as the child's independent initial base;
|
|
1285
|
+
remote Storage performs this server-side. Every copy reads committed pre-batch
|
|
1286
|
+
source state independent of write-array order. A selected source may not be
|
|
1287
|
+
created, changed, or retired in the same batch. Copying remains atomic with the
|
|
1288
|
+
conversation, overrides, registry writes, and other mutations.
|
|
1289
|
+
|
|
1290
|
+
An unaccessed copy retains only its descriptor in Session memory. Typed access
|
|
1291
|
+
inside the creating transaction lazily reads the detached source, migrates when
|
|
1292
|
+
required, and replaces the copy with one ordinary child create containing the
|
|
1293
|
+
final prepared value. Definition-free copies publish explicit `document.copy`
|
|
1294
|
+
metadata rather than a value. That metadata announces Storage-backed initial
|
|
1295
|
+
state and is never interpreted as a document value. Document states, watches,
|
|
1296
|
+
and mounted views hydrate by capturing their baseline and subscription
|
|
1297
|
+
atomically on the Session line: a later commit already present becomes the
|
|
1298
|
+
baseline, while one committed after registration is delivered. Publications
|
|
1299
|
+
and watches are convergence mechanisms, not audit streams. A mounted aggregate
|
|
1300
|
+
must acquire all of its document baselines and commit subscription in one
|
|
1301
|
+
Session-line operation so it never exposes a mixture from one commit.
|
|
1302
|
+
|
|
927
1303
|
## 4. Transactions and storage ownership
|
|
928
1304
|
|
|
929
1305
|
A Session commit callback may be asynchronous. It owns the Session mutation
|
|
930
1306
|
line through callback execution, preparation, storage settlement, committed
|
|
931
|
-
baseline adoption, and publication enqueue.
|
|
1307
|
+
baseline adoption, and publication enqueue. Commit observers run on the
|
|
1308
|
+
line; document-state and watch user callbacks run later.
|
|
932
1309
|
External model, process, tool, network, and human effects run outside it.
|
|
1310
|
+
`subscribeCommits()` observes complete immutable publications synchronously on
|
|
1311
|
+
the line after adoption. `subscribeClose()` observes close synchronously when it
|
|
1312
|
+
begins, after admission is sealed; the Harness stops watches and signals task
|
|
1313
|
+
invocations there. Both return idempotent disposers; their listeners must not
|
|
1314
|
+
throw, block, or call Session APIs. Document-state subscribers and watch
|
|
1315
|
+
listeners still run later, off the line.
|
|
933
1316
|
|
|
934
1317
|
```ts
|
|
935
1318
|
await session.commit(async tx => {
|
|
936
|
-
const
|
|
937
|
-
const live = await tx.doc(LiveDoc,
|
|
1319
|
+
const conversation = await tx.conversation(conversationId); // table read
|
|
1320
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
938
1321
|
|
|
939
|
-
|
|
940
|
-
live.message
|
|
941
|
-
tx.
|
|
1322
|
+
await tx.appendEntry(conversationId, message); // first table write
|
|
1323
|
+
delete live.message; // document mutation remains valid
|
|
1324
|
+
await tx.createTask(Follow, { after: message.id }); // further table writes are fine
|
|
942
1325
|
}, context);
|
|
943
1326
|
```
|
|
944
1327
|
|
|
945
|
-
Mutation admission occurs on the Session line before a commit callback
|
|
946
|
-
get-or-create acquisition starts. Closing seals mutation admission and task
|
|
1328
|
+
Mutation admission occurs on the Session line before a commit callback starts. Closing seals mutation admission and task
|
|
947
1329
|
reservation. Already-admitted commits settle before storage closes. Once a
|
|
948
1330
|
commit is admitted, caller cancellation does not interrupt storage settlement or
|
|
949
1331
|
undo the commit. Cancelling a close wait does not reopen admission.
|
|
950
1332
|
|
|
951
1333
|
Table rules:
|
|
952
1334
|
|
|
953
|
-
- Tables are conversations, entries, tasks, and
|
|
1335
|
+
- Tables are conversations, entries, tasks, and submissions.
|
|
954
1336
|
- Table reads are allowed before the first table write.
|
|
955
1337
|
- Any table read after the first table write throws `ReadAfterWrite`.
|
|
956
1338
|
- Document access and read-your-writes remain available after table writes.
|
|
@@ -972,10 +1354,10 @@ Storage ownership:
|
|
|
972
1354
|
```ts
|
|
973
1355
|
type TaskOutcome<R> =
|
|
974
1356
|
| { readonly status: "completed"; readonly result: R }
|
|
975
|
-
| { readonly status: "failed"; readonly error:
|
|
1357
|
+
| { readonly status: "failed"; readonly error: TaskOutcomeError; readonly result?: R }
|
|
976
1358
|
| { readonly status: "aborted"; readonly reason?: string; readonly result?: R }
|
|
977
1359
|
| { readonly status: "orphaned"; readonly reason: string }
|
|
978
|
-
| { readonly status: "faulted"; readonly error:
|
|
1360
|
+
| { readonly status: "faulted"; readonly error: TaskOutcomeError };
|
|
979
1361
|
|
|
980
1362
|
type TaskState<S, R> =
|
|
981
1363
|
| { readonly status: "pending"; readonly checkpoint: S }
|
|
@@ -983,12 +1365,12 @@ type TaskState<S, R> =
|
|
|
983
1365
|
| { readonly status: "terminal"; readonly outcome: TaskOutcome<R> };
|
|
984
1366
|
|
|
985
1367
|
type TaskRecord<I, S, R> = {
|
|
986
|
-
readonly id:
|
|
987
|
-
readonly conversationId:
|
|
1368
|
+
readonly id: TaskId<R>;
|
|
1369
|
+
readonly conversationId: ConversationId;
|
|
988
1370
|
readonly kind: string;
|
|
989
1371
|
readonly version: number;
|
|
990
1372
|
readonly input: I;
|
|
991
|
-
readonly after: readonly
|
|
1373
|
+
readonly after: readonly TaskId[];
|
|
992
1374
|
readonly background: boolean;
|
|
993
1375
|
readonly abortRequested: boolean;
|
|
994
1376
|
} & (
|
|
@@ -1006,6 +1388,9 @@ type RunningTask<I, S, R> = TaskRecord<I, S, R> & {
|
|
|
1006
1388
|
readonly state: Extract<TaskState<S, R>, { status: "running" }>;
|
|
1007
1389
|
};
|
|
1008
1390
|
|
|
1391
|
+
/** State a task commits for itself: a replacement checkpoint or its terminal outcome. */
|
|
1392
|
+
type NextTaskState<S, R> = Extract<TaskState<S, R>, { status: "running" | "terminal" }>;
|
|
1393
|
+
|
|
1009
1394
|
interface HookRunner<H extends object> {
|
|
1010
1395
|
each<K extends keyof H>(name: K, invoke: (handler: H[K]) => void | Promise<void>): Promise<void>;
|
|
1011
1396
|
}
|
|
@@ -1016,19 +1401,32 @@ type PhaseHandler<I, P, S, R, H extends object> = (
|
|
|
1016
1401
|
context: Context,
|
|
1017
1402
|
) => Promise<void>;
|
|
1018
1403
|
|
|
1019
|
-
interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver {
|
|
1020
|
-
readonly taskId:
|
|
1021
|
-
readonly conversationId:
|
|
1404
|
+
interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver, DocumentReader {
|
|
1405
|
+
readonly taskId: TaskId<R>;
|
|
1406
|
+
readonly conversationId: ConversationId;
|
|
1022
1407
|
readonly signal: AbortSignal;
|
|
1408
|
+
/** Registry snapshot of the current phase; refreshed at every phase boundary. */
|
|
1409
|
+
readonly registry: RegistrySnapshot<ToolRegistration>;
|
|
1410
|
+
readonly models: Models;
|
|
1023
1411
|
readonly hooks: HookRunner<H>;
|
|
1024
1412
|
|
|
1025
1413
|
commit(
|
|
1026
|
-
change: (
|
|
1414
|
+
change: (
|
|
1415
|
+
tx: Tx,
|
|
1416
|
+
current: RunningTask<I, S, R>,
|
|
1417
|
+
) => NextTaskState<S, R> | undefined | Promise<NextTaskState<S, R> | undefined>,
|
|
1027
1418
|
context: Context,
|
|
1028
1419
|
): Promise<void>;
|
|
1029
1420
|
|
|
1030
1421
|
memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
|
|
1031
1422
|
memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
|
|
1423
|
+
conversation(id: ConversationId, context: Context): Promise<ConversationHandle | undefined>;
|
|
1424
|
+
/** Committed raw active transcript and model context, optionally cut off at `at`. */
|
|
1425
|
+
context(conversationId: ConversationId, context: Context, at?: EntryId): Promise<ContextView>;
|
|
1426
|
+
/** The Harness clock. */
|
|
1427
|
+
now(): number;
|
|
1428
|
+
/** Forward a non-fatal failure to `HarnessOptions.onReport`. */
|
|
1429
|
+
report(error: unknown): void;
|
|
1032
1430
|
sleep(until: number, context: Context): Promise<void>;
|
|
1033
1431
|
}
|
|
1034
1432
|
|
|
@@ -1039,7 +1437,7 @@ type TaskDefinition<I, S extends { phase: string }, R, H extends object> = {
|
|
|
1039
1437
|
readonly phases: {
|
|
1040
1438
|
[P in S["phase"]]: PhaseHandler<I, Extract<S, { phase: P }>, S, R, H>;
|
|
1041
1439
|
};
|
|
1042
|
-
abort(task:
|
|
1440
|
+
abort(task: RunningTask<I, S, R>, runtime: TaskRuntime<I, S, R, H>, context: Context): Promise<void>;
|
|
1043
1441
|
migrate?(input: JsonValue, checkpoint: JsonValue, fromVersion: number): {
|
|
1044
1442
|
input: I;
|
|
1045
1443
|
checkpoint: S;
|
|
@@ -1051,11 +1449,9 @@ interface Task<I, S extends { phase: string }, R, H extends object> {
|
|
|
1051
1449
|
readonly definition: TaskDefinition<I, S, R, H>;
|
|
1052
1450
|
}
|
|
1053
1451
|
|
|
1054
|
-
declare const taskResultType: unique symbol;
|
|
1055
|
-
type TaskRef<R> = { readonly id: Id; readonly [taskResultType]?: R };
|
|
1056
1452
|
type TaskOptions = {
|
|
1057
|
-
readonly conversationId?:
|
|
1058
|
-
readonly after?: readonly
|
|
1453
|
+
readonly conversationId?: ConversationId;
|
|
1454
|
+
readonly after?: readonly TaskId[];
|
|
1059
1455
|
readonly background?: boolean;
|
|
1060
1456
|
};
|
|
1061
1457
|
|
|
@@ -1064,34 +1460,98 @@ function defineTask<I, S extends { phase: string }, R, H extends object = {}>(
|
|
|
1064
1460
|
): Task<I, S, R, H>;
|
|
1065
1461
|
```
|
|
1066
1462
|
|
|
1463
|
+
`TaskId<R>` is the numeric task ID itself with an erased result-type brand. It
|
|
1464
|
+
replaces a separate task-reference wrapper: `createTask()` returns `TaskId<R>`,
|
|
1465
|
+
typed waits infer `R` from that ID, and result-agnostic operations accept
|
|
1466
|
+
`TaskId<unknown>`. A task's `conversationId` is immutable after creation; a
|
|
1467
|
+
replacement that moves an existing task to another conversation rejects before
|
|
1468
|
+
Storage admission so persisted conversation-owner edges cannot become stale.
|
|
1469
|
+
|
|
1067
1470
|
The phase map is exhaustive and phase-narrowed. A handler may perform several
|
|
1068
1471
|
commits around one effect, but each durable checkpoint is a full replacement.
|
|
1069
1472
|
`TaskRuntime.commit()` rereads and gates the current durable task on the Session
|
|
1070
|
-
line before invoking its callback.
|
|
1071
|
-
|
|
1473
|
+
line before invoking its callback. It rejects when the invocation has ended, the
|
|
1474
|
+
Harness is closing, the task is terminal, or a run invocation's task carries an
|
|
1475
|
+
abort mark. Its `tx.createTask()` defaults to the task's conversation, and every
|
|
1476
|
+
entry it appends records the task as `byTaskId`. When the
|
|
1477
|
+
callback returns a state, the runtime replaces the task's state in the same
|
|
1478
|
+
commit, so the checkpoint or outcome is atomic with the callback's entries,
|
|
1479
|
+
documents, and child tasks and is type-checked against the task's checkpoint and
|
|
1480
|
+
result types. Returning nothing leaves the state unchanged. A terminal state
|
|
1481
|
+
drops the memos. `pending` is never returned; only reconciliation and handover
|
|
1482
|
+
write it.
|
|
1483
|
+
|
|
1484
|
+
`Tx` has no task replacement operation. A task changes only its own state,
|
|
1485
|
+
through its runtime. The scheduler owns reservation, reconciliation, handover,
|
|
1486
|
+
faults, and orphaning; `abortTask()` owns abort marks. Other code
|
|
1487
|
+
stops a task with `abortTask()` and reads its result with `waitForTask()`.
|
|
1488
|
+
|
|
1489
|
+
```ts
|
|
1490
|
+
// Intent, effect, outcome.
|
|
1491
|
+
prepare: async (task, runtime, context) => {
|
|
1492
|
+
await runtime.commit(() => ({ status: "running", checkpoint: { phase: "charge", key: newKey() } }), context);
|
|
1493
|
+
},
|
|
1494
|
+
charge: async (task, runtime, context) => {
|
|
1495
|
+
const receipt = await payments.charge(task.state.checkpoint.key); // idempotent by key
|
|
1496
|
+
await runtime.commit(async (tx, current) => {
|
|
1497
|
+
const entry = await tx.appendEntry(current.conversationId, receiptEntry(receipt));
|
|
1498
|
+
return { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
|
|
1499
|
+
}, context);
|
|
1500
|
+
},
|
|
1501
|
+
// The abort handler decides the outcome; returning without one faults the task.
|
|
1502
|
+
abort: async (task, runtime, context) => {
|
|
1503
|
+
await payments.cancel(task.state.checkpoint);
|
|
1504
|
+
await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted", reason: "user" } }), context);
|
|
1505
|
+
},
|
|
1506
|
+
```
|
|
1507
|
+
`memo(name, candidate)` is one gated commit; `memo(name)` reads the committed
|
|
1508
|
+
record. `sleep(until)` compares against the Harness `now` clock and rejects when
|
|
1509
|
+
the invocation is signalled or its context is cancelled. Watches acquired through
|
|
1510
|
+
the runtime stop when the invocation ends. `snapshot()`/`snapshotAsOf()` read
|
|
1511
|
+
committed documents, for example to supply `PromptInput.read` (section 7.4).
|
|
1512
|
+
`context()` captures its bounds on the Session line and derives the view from
|
|
1513
|
+
immutable entries off the line, like `Conversation.context()`. Like every runtime
|
|
1514
|
+
operation, these reject after the invocation ends.
|
|
1072
1515
|
|
|
1073
1516
|
Reservation durably changes `pending` to `running`. One invocation runs phase
|
|
1074
|
-
handlers in sequence; checkpoint commits retain `running`.
|
|
1075
|
-
|
|
1517
|
+
handlers in sequence; checkpoint commits retain `running`. Before every phase,
|
|
1518
|
+
including the first, the scheduler runs one step callback on the Session line.
|
|
1519
|
+
It reads the committed task and synchronously applies the first matching rule
|
|
1520
|
+
below to the phase that just returned. It writes any fault or handover in the
|
|
1521
|
+
same commit, together with the Harness cleanup for a scheduler-written outcome
|
|
1522
|
+
(section 5.4), which may await document access inside that callback. It ends
|
|
1523
|
+
the invocation there when a rule stops it. A runtime commit the
|
|
1524
|
+
invocation queued earlier therefore either lands before the step and counts, or
|
|
1525
|
+
reaches the line after it and rejects. Before the first phase only rules 1–3
|
|
1526
|
+
apply:
|
|
1076
1527
|
|
|
1077
1528
|
1. Terminal: stop.
|
|
1078
1529
|
2. Session closing: stop; preserve the checkpoint and any abort mark for reopen.
|
|
1079
1530
|
3. Run mode with a durable abort mark: end and join the run invocation, then
|
|
1080
1531
|
dispatch a fresh abort invocation.
|
|
1081
1532
|
4. Uncaught error: write terminal `faulted`.
|
|
1082
|
-
5. Checkpoint changed, including progress within the same phase:
|
|
1083
|
-
|
|
1533
|
+
5. Checkpoint changed, including progress within the same phase: refresh the
|
|
1534
|
+
registry snapshot and either hand over (section 5.4) or invoke the phase
|
|
1535
|
+
handler in the same task invocation.
|
|
1084
1536
|
6. Checkpoint unchanged: write terminal `faulted` because no durable progress
|
|
1085
1537
|
was made.
|
|
1086
1538
|
|
|
1539
|
+
An abort invocation runs its abort handler once. After it settles, a step
|
|
1540
|
+
applies rules 1, 2, and 4; a handler that returns without a terminal outcome
|
|
1541
|
+
faults the task. When Storage rejects a step's fault or handover write, the
|
|
1542
|
+
invocation still ends; the task stays `running` and the next reservation runs it
|
|
1543
|
+
again.
|
|
1544
|
+
|
|
1087
1545
|
On open, running-task reconciliation changes surviving `running` tasks back to
|
|
1088
|
-
`pending`, preserving their checkpoint and abort mark. Task migration
|
|
1089
|
-
|
|
1090
|
-
|
|
1546
|
+
`pending`, preserving their checkpoint and abort mark. Task migration runs at
|
|
1547
|
+
reservation, atomically with `pending -> running`. One callback handles every
|
|
1548
|
+
supported older version. A task whose definition is missing, older than the
|
|
1549
|
+
stored version, or fails migration stays `pending` and blocked until a fitting
|
|
1550
|
+
definition is registered or the task is aborted (section 5.4).
|
|
1091
1551
|
`close()` marks the runtime closing, seals admission and reservation, signals
|
|
1092
1552
|
invocations, and stops watches. Outside the Session line it settles admitted
|
|
1093
|
-
commits and joins invocations
|
|
1094
|
-
|
|
1553
|
+
commits and joins invocations before closing storage. Already-running watch callbacks remain
|
|
1554
|
+
caller-owned. Later runtime commits reject, and close writes no task outcome. Closing
|
|
1095
1555
|
starts no fresh phase or abort invocation. It does not set abort marks,
|
|
1096
1556
|
terminalize tasks, retire task documents, or publish document retirement. The
|
|
1097
1557
|
hosting layer withdraws services and
|
|
@@ -1130,12 +1590,13 @@ A terminal transition atomically:
|
|
|
1130
1590
|
1. Writes the terminal task record.
|
|
1131
1591
|
2. Appends any result entries.
|
|
1132
1592
|
3. Retires all documents scoped to that task.
|
|
1133
|
-
4. Resolves any
|
|
1593
|
+
4. Resolves any submissions settled by the task.
|
|
1134
1594
|
|
|
1135
1595
|
The execution checkpoint and memos disappear from the terminal representation.
|
|
1136
1596
|
Terminal records remain queryable for dependencies, waiters, inspection, and
|
|
1137
1597
|
reopen. A normal run becomes eligible when every `after` task is terminal. An abort
|
|
1138
|
-
mark bypasses dependencies so pending work can always reach its abort handler
|
|
1598
|
+
mark bypasses dependencies so pending work can always reach its abort handler,
|
|
1599
|
+
or its `orphaned` settlement when its definition is unavailable.
|
|
1139
1600
|
|
|
1140
1601
|
### 5.4 Scheduler, abort, and ownership
|
|
1141
1602
|
|
|
@@ -1160,59 +1621,143 @@ the durable mark on reopen. Cancelling one caller's `Context` only cancels
|
|
|
1160
1621
|
that call or wait; it does not durably abort shared work unless the invoked API
|
|
1161
1622
|
commits an abort mark.
|
|
1162
1623
|
|
|
1163
|
-
A task may create owned conversations.
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1624
|
+
A task may create owned conversations. Conversations are durable scopes; tasks
|
|
1625
|
+
are the units of live work counted by idle and marked by abort. History parents
|
|
1626
|
+
are irrelevant to ownership traversal.
|
|
1627
|
+
|
|
1628
|
+
Ordinary traversal starts at an explicitly addressed conversation, visits its
|
|
1629
|
+
tasks, and follows conversations owned by each non-background task. It follows
|
|
1630
|
+
owner edges after the owner becomes terminal, but a background owner is a
|
|
1631
|
+
boundary: ordinary traversal skips that task and its complete owned subtree.
|
|
1632
|
+
Direct conversation operations start inside that conversation regardless of its
|
|
1633
|
+
owner. Directly aborting a live background task includes that task and follows
|
|
1634
|
+
its ordinary owned subtree; nested background owners remain boundaries. Full
|
|
1635
|
+
teardown crosses every boundary, marks every live task, and must seal new
|
|
1636
|
+
admission while it gathers the complete indexed ownership subtree.
|
|
1637
|
+
|
|
1638
|
+
`Conversation.abort()` withdraws queued inputs and marks live non-background
|
|
1639
|
+
tasks selected by ordinary traversal. `Conversation.waitForIdle()` waits until
|
|
1640
|
+
that traversal contains no live non-background task. Harness idle performs the
|
|
1641
|
+
same traversal from every ownerless conversation root rather than globally
|
|
1642
|
+
counting tasks, so ordinary work below a background owner does not block it.
|
|
1643
|
+
Explicit `waitForTask()` waits for its referenced task regardless of the task's
|
|
1644
|
+
background flag.
|
|
1645
|
+
|
|
1646
|
+
An abort mark atomically and idempotently cascades to foreground-owned work,
|
|
1647
|
+
including conversations, tasks, and submissions staged in the same transaction.
|
|
1648
|
+
Terminal outcomes `failed`, `faulted`, `orphaned`, and `aborted` record the same
|
|
1649
|
+
durable cancellation intent; `completed` does not. Conversation records and
|
|
1650
|
+
owner edges are never retired with the task. Active invocations are signalled
|
|
1651
|
+
after commit, and a terminal receipt guarantees durable cancellation intent,
|
|
1652
|
+
not descendant quiescence.
|
|
1653
|
+
|
|
1654
|
+
Every run invocation start resolves the task's definition by `TaskRecord.kind`
|
|
1655
|
+
from the scheduler's current registry snapshot:
|
|
1176
1656
|
|
|
1177
|
-
|
|
1657
|
+
```text
|
|
1658
|
+
definition missing -> stay pending, blocked: missing_task
|
|
1659
|
+
stored version newer than it -> stay pending, blocked: task_too_old
|
|
1660
|
+
stored version older than it -> migrate(input, checkpoint, storedVersion)
|
|
1661
|
+
failure -> stay pending, blocked: migration_failed
|
|
1662
|
+
success -> commit migrated record + running atomically
|
|
1663
|
+
equal version -> commit running
|
|
1664
|
+
```
|
|
1178
1665
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1666
|
+
An abort invocation resolves the definition the same way. When the definition
|
|
1667
|
+
can take the task, its abort handler runs; otherwise the task is orphaned as
|
|
1668
|
+
described below.
|
|
1669
|
+
|
|
1670
|
+
A failed migration is reported once through `onReport` and retried only after
|
|
1671
|
+
the registry resolves a different definition object for the task's kind.
|
|
1672
|
+
|
|
1673
|
+
The Harness never terminalizes a task merely because registry code is missing or
|
|
1674
|
+
incompatible, neither at open nor later. A blocked task without an abort mark
|
|
1675
|
+
keeps its durable record unchanged, remains live, still blocks ordinary idle
|
|
1676
|
+
waits, and is reconsidered whenever the registry changes. The blocked reason is derived
|
|
1677
|
+
runtime state, not a persisted task status. Document migration remains
|
|
1678
|
+
access-driven.
|
|
1679
|
+
|
|
1680
|
+
Aborting a blocked task cannot run its abort handler, because that code is
|
|
1681
|
+
missing or cannot take the task. The Harness therefore settles it as terminal
|
|
1682
|
+
`orphaned` instead of `aborted`: `aborted` means the task's own abort handler
|
|
1683
|
+
ran and decided the outcome, while `orphaned` means no task code ran, so external
|
|
1684
|
+
effects the task started may remain uncleaned. The orphaned `reason` is the
|
|
1685
|
+
blocked reason (`missing_task`, `task_too_old`, or `migration_failed`). When
|
|
1686
|
+
`abortTask()` finds no active invocation and the current snapshot
|
|
1687
|
+
cannot take the task, the marking commit settles it directly; otherwise the
|
|
1688
|
+
scheduler settles it when it would reserve the abort invocation. Only an abort (direct, by
|
|
1689
|
+
conversation, or by cascade) orphans a task; a missing definition alone never
|
|
1690
|
+
does. The orphaning commit performs the cleanup the task's code cannot: affected
|
|
1691
|
+
input submissions become unanswered with the reason, any matching active run
|
|
1692
|
+
control is cleared, and task-scoped documents retire. No transcript entry is
|
|
1693
|
+
written; the terminal task record and unanswered submissions carry the reason.
|
|
1694
|
+
Faulting a run task performs the same control/submission cleanup with a
|
|
1695
|
+
`faulted` outcome.
|
|
1696
|
+
|
|
1697
|
+
The scheduler knows nothing about runs or task kinds. The Harness, which owns
|
|
1698
|
+
submissions, run control, and the built-in tasks, gives it one hook that the
|
|
1699
|
+
scheduler calls in the same commit for every terminal outcome it writes itself
|
|
1700
|
+
(`faulted` and `orphaned`). The hook ignores tasks whose kind is not a built-in
|
|
1701
|
+
run kind, so it never creates `pi.live` elsewhere. It settles the run when
|
|
1702
|
+
`pi.live.run` names the task (section 8): its inputs become `unanswered` with reason `faulted`
|
|
1703
|
+
(detail: the error message) or the blocked reason, and `run` and `generation`
|
|
1704
|
+
are removed. Outcomes a task commits for itself do their own settlement.
|
|
1705
|
+
|
|
1706
|
+
At every normal phase boundary (section 5.1, rule 5), the step refreshes the
|
|
1707
|
+
invocation's registry snapshot. If the task definition resolved by name is a different object
|
|
1708
|
+
than the one the invocation started with and the new definition can reserve the
|
|
1709
|
+
task (same version, or a higher version with `migrate`), the invocation hands
|
|
1710
|
+
over: the step commits the task back to `pending` with its checkpoint, memos,
|
|
1711
|
+
and abort mark and ends the invocation, and the next reservation starts a fresh invocation under the new
|
|
1712
|
+
definition, applying the reservation rules above. When the definition is missing
|
|
1713
|
+
or cannot reserve the task, the invocation keeps running under its old
|
|
1714
|
+
definition, reports once through `onReport` per resolved definition, and
|
|
1715
|
+
reconsiders at its next boundary. A handler that
|
|
1716
|
+
never settles never hands over.
|
|
1717
|
+
|
|
1718
|
+
## 6. Submissions and inbox
|
|
1719
|
+
|
|
1720
|
+
Submission records back awaitable host objects. Admission is Harness-internal;
|
|
1721
|
+
run tasks settle the inputs they answer with `tx.settleSubmission()`. The inbox
|
|
1722
|
+
itself is an ordered conversation document containing tagged items:
|
|
1181
1723
|
|
|
1182
1724
|
```ts
|
|
1183
1725
|
type InboxItem =
|
|
1184
|
-
| { readonly id:
|
|
1185
|
-
| { readonly id:
|
|
1726
|
+
| { readonly id: SubmissionId; readonly mode: "steer" | "followUp"; readonly message: Message }
|
|
1727
|
+
| { readonly id: SubmissionId; readonly mode: "write"; readonly entry: EntryDraft };
|
|
1186
1728
|
```
|
|
1187
1729
|
|
|
1188
|
-
|
|
1189
|
-
currently responsible for the
|
|
1190
|
-
undefined` defines `busy`; get-or-create
|
|
1191
|
-
value remains
|
|
1192
|
-
another.
|
|
1730
|
+
Run control lives in the built-in live document `pi.live` (section 8). Its
|
|
1731
|
+
optional `run` value names the task currently responsible for the run and its
|
|
1732
|
+
placed input-submission IDs. `run !== undefined` defines `busy`; get-or-create
|
|
1733
|
+
of the idle document does not. The value remains while generation, tools, and
|
|
1734
|
+
post-tools hand work to one another. The ID list is mutable state because a
|
|
1735
|
+
boundary adds placed steering inputs to an active run; every terminal path
|
|
1736
|
+
settles exactly the listed inputs.
|
|
1193
1737
|
|
|
1194
1738
|
Admission and terminal transitions:
|
|
1195
1739
|
|
|
1196
|
-
| action |
|
|
1740
|
+
| action | submission state | other writes |
|
|
1197
1741
|
|---|---|---|
|
|
1198
|
-
| idle
|
|
1199
|
-
| busy
|
|
1200
|
-
| idle
|
|
1201
|
-
| busy
|
|
1202
|
-
| boundary places user item | `placed`, with entry | add ID to current/successor
|
|
1742
|
+
| idle input submission | `placed`, with user entry | create run controller/generation |
|
|
1743
|
+
| busy input submission | `queued` | append steer/follow-up inbox item |
|
|
1744
|
+
| idle write submission | `done`, with entry | append entry; no run |
|
|
1745
|
+
| busy write submission | `queued` | append write inbox item |
|
|
1746
|
+
| boundary places user item | `placed`, with entry | add ID to current/successor run |
|
|
1203
1747
|
| boundary places write | `done`, with entry | append entry |
|
|
1204
|
-
|
|
|
1205
|
-
|
|
|
1748
|
+
| run answers | input `done`, with required answer entry | clear/hand off run controller |
|
|
1749
|
+
| run fails or aborts | input `unanswered`, with reason | clear/hand off run controller |
|
|
1206
1750
|
| withdraw queued item | `unanswered`, reason `aborted` | remove inbox item |
|
|
1207
1751
|
| stale item | `unanswered`, reason `stale` | remove inbox item |
|
|
1208
1752
|
|
|
1209
|
-
`requestId` deduplicates within one conversation before any write
|
|
1210
|
-
with `whenBusy: "reject"`
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1753
|
+
`requestId` deduplicates within one conversation before any write; reusing one
|
|
1754
|
+
for the other submission type rejects. A busy input with `whenBusy: "reject"`
|
|
1755
|
+
writes no record and reports `ConversationBusy`. Before an idle input places its
|
|
1756
|
+
own entry, it runs a final boundary to drain older eligible queued items. A
|
|
1757
|
+
`Submission` waits until `done` or `unanswered`; abort withdraws only a still-
|
|
1758
|
+
queued submission, reports `already_placed` for a placed input, and reports
|
|
1759
|
+
`settled` for any terminal submission. Conversation abort withdraws queued steer/follow-up submissions but keeps writes
|
|
1760
|
+
for later placement.
|
|
1216
1761
|
|
|
1217
1762
|
Boundary selection is deterministic by item ID:
|
|
1218
1763
|
|
|
@@ -1221,35 +1766,234 @@ Boundary selection is deterministic by item ID:
|
|
|
1221
1766
|
| `postTools` | all | first/all by mode | none |
|
|
1222
1767
|
| `final` | all | first/all by mode | first/all by mode |
|
|
1223
1768
|
|
|
1224
|
-
A queued self-head write cuts older pending user items: those
|
|
1225
|
-
stale, the write is placed, and the current
|
|
1769
|
+
A queued self-head write cuts older pending user items: those submissions become
|
|
1770
|
+
stale, the write is placed, and the current run terminates. Other head writes
|
|
1226
1771
|
whose target predates the caller's newest known head are stale.
|
|
1227
1772
|
|
|
1228
1773
|
At ordinary `postTools`, generation continues even with no queued trigger;
|
|
1229
1774
|
selected steer IDs join that continuation. A terminating/handoff post-tools
|
|
1230
|
-
boundary uses final behavior instead. At `final`, the current
|
|
1231
|
-
|
|
1775
|
+
boundary uses final behavior instead. At `final`, the current run's placed
|
|
1776
|
+
input submissions settle first; selected user IDs start one successor generation. Writes
|
|
1232
1777
|
never trigger generation by themselves. A final boundary without continuation
|
|
1233
1778
|
or user triggers leaves the conversation idle.
|
|
1234
1779
|
|
|
1235
1780
|
Selected and stale items are removed positionally while retained item order is
|
|
1236
|
-
preserved.
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1781
|
+
preserved. Chord's Astra operation generator must express scattered removals
|
|
1782
|
+
without carrying retained values; IDs are not substituted for positional inbox
|
|
1783
|
+
semantics.
|
|
1784
|
+
|
|
1785
|
+
## 7. Registry, hooks, tools, and system prompt
|
|
1786
|
+
|
|
1787
|
+
### 7.1 Registry
|
|
1240
1788
|
|
|
1241
|
-
|
|
1789
|
+
Extension code reaches the Harness through one application-owned registry. The
|
|
1790
|
+
registry is process-local, may outlive a Harness, and is the only place tools,
|
|
1791
|
+
tool wrappers, hooks, tasks, and the system prompt are registered. Nothing
|
|
1792
|
+
in it is persisted; durable state stays in conversations, entries, tasks, and
|
|
1793
|
+
documents.
|
|
1242
1794
|
|
|
1243
|
-
|
|
1795
|
+
```ts
|
|
1796
|
+
type DocumentReader = Pick<Session, "snapshot" | "snapshotAsOf">;
|
|
1797
|
+
|
|
1798
|
+
interface Registration {
|
|
1799
|
+
/** Idempotent; removes exactly the registrations this token covers. */
|
|
1800
|
+
dispose(): void;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
type ToolWrapper<Tool extends ToolRegistration> = (tool: Tool) => Tool;
|
|
1804
|
+
|
|
1805
|
+
type HookScope = {
|
|
1806
|
+
readonly conversationId: ConversationId;
|
|
1807
|
+
/** Also match conversations owned, transitively, by tasks of this conversation. */
|
|
1808
|
+
readonly subtree?: boolean;
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
type PromptInput<Tool extends ToolRegistration> = {
|
|
1812
|
+
readonly conversationId: ConversationId;
|
|
1813
|
+
/** Active and registered tools in configured order, as offered in this request. */
|
|
1814
|
+
readonly tools: readonly Tool[];
|
|
1815
|
+
/** Sections already in effect after replaying the active transcript. */
|
|
1816
|
+
readonly shown: Readonly<Record<string, string>>;
|
|
1817
|
+
readonly model?: ModelRef;
|
|
1818
|
+
readonly thinkingLevel: ModelThinkingLevel;
|
|
1819
|
+
/** Committed document reads. */
|
|
1820
|
+
readonly read: DocumentReader;
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
type PromptSection<Tool extends ToolRegistration> = {
|
|
1824
|
+
readonly key: string;
|
|
1825
|
+
render(
|
|
1826
|
+
input: PromptInput<Tool>,
|
|
1827
|
+
context: Context,
|
|
1828
|
+
): string | undefined | Promise<string | undefined>;
|
|
1829
|
+
/** Default true: wrap the text as `<key>\n...\n</key>`. */
|
|
1830
|
+
readonly tag?: boolean;
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
type PromptSectionWrapper<Tool extends ToolRegistration> = (
|
|
1834
|
+
section: PromptSection<Tool>,
|
|
1835
|
+
) => PromptSection<Tool>;
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Stages the documents every new conversation gets, inside the creating commit, after fork copies and before host
|
|
1839
|
+
* `init`. Table reads throw; a fork (`conversation.parent`) already holds its copied documents. A throw fails the
|
|
1840
|
+
* creation.
|
|
1841
|
+
*/
|
|
1842
|
+
type ConversationSetup = (
|
|
1843
|
+
tx: Tx,
|
|
1844
|
+
conversation: ConversationRecord,
|
|
1845
|
+
registry: RegistrySnapshot<ToolRegistration>,
|
|
1846
|
+
) => void | Promise<void>;
|
|
1847
|
+
|
|
1848
|
+
type RegistryFailure = {
|
|
1849
|
+
readonly kind: "tool" | "section";
|
|
1850
|
+
/** Tool name or section key. */
|
|
1851
|
+
readonly name: string;
|
|
1852
|
+
readonly error: unknown;
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
interface RegistryReader<Tool extends ToolRegistration = ToolRegistration> {
|
|
1856
|
+
/** Immutable view of the whole current registry. */
|
|
1857
|
+
snapshot(): RegistrySnapshot<Tool>;
|
|
1858
|
+
/** Called synchronously after every publication; wakes the scheduler to reconsider blocked tasks. */
|
|
1859
|
+
subscribe(listener: () => void): () => void;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
interface RegistrySnapshot<Tool extends ToolRegistration> {
|
|
1863
|
+
/** Composed tools in registry order; a tool whose wrapper failed is absent. */
|
|
1864
|
+
tools(): readonly Tool[];
|
|
1865
|
+
tool(name: string): Tool | undefined;
|
|
1866
|
+
/** Base tool names in registry order, including tools whose wrappers fail. */
|
|
1867
|
+
toolNames(): readonly string[];
|
|
1868
|
+
task(name: string): AnyTask | undefined;
|
|
1869
|
+
/** Hooks registered for tasks with `task`'s name, in registry order. */
|
|
1870
|
+
hooks<K extends AnyTask>(task: K): readonly {
|
|
1871
|
+
readonly handlers: Partial<HooksOf<K>>;
|
|
1872
|
+
readonly scope?: HookScope;
|
|
1873
|
+
}[];
|
|
1874
|
+
/** Composed sections in registry order; a section whose wrapper failed is absent. */
|
|
1875
|
+
sections(): readonly PromptSection<Tool>[];
|
|
1876
|
+
/** Wrapper failures of this state; the Harness reports them where it uses them. */
|
|
1877
|
+
failures(): readonly RegistryFailure[];
|
|
1878
|
+
/** Conversation setups in registry order, the built-in `pi` setup first. */
|
|
1879
|
+
conversationSetups(): readonly { readonly key: string; readonly setup: ConversationSetup }[];
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
interface Registry<Tool extends ToolRegistration = ToolRegistration> extends RegistryReader<Tool> {
|
|
1883
|
+
readonly tools: {
|
|
1884
|
+
add(tool: Tool): Registration;
|
|
1885
|
+
wrap(name: string, key: string, wrapper: ToolWrapper<Tool>): Registration;
|
|
1886
|
+
list(): readonly Tool[];
|
|
1887
|
+
};
|
|
1888
|
+
readonly hooks: {
|
|
1889
|
+
add<K extends AnyTask>(
|
|
1890
|
+
task: K,
|
|
1891
|
+
handlers: Partial<HooksOf<K>>,
|
|
1892
|
+
options?: { readonly scope?: HookScope; readonly key?: string },
|
|
1893
|
+
): Registration;
|
|
1894
|
+
};
|
|
1895
|
+
readonly tasks: {
|
|
1896
|
+
add(task: AnyTask): Registration;
|
|
1897
|
+
list(): readonly AnyTask[];
|
|
1898
|
+
};
|
|
1899
|
+
readonly conversations: {
|
|
1900
|
+
setup(key: string, setup: ConversationSetup): Registration;
|
|
1901
|
+
};
|
|
1902
|
+
readonly systemPrompt: {
|
|
1903
|
+
section(
|
|
1904
|
+
key: string,
|
|
1905
|
+
render: PromptSection<Tool>["render"],
|
|
1906
|
+
options?: { readonly tag?: boolean },
|
|
1907
|
+
): Registration;
|
|
1908
|
+
wrap(key: string, wrapperKey: string, wrapper: PromptSectionWrapper<Tool>): Registration;
|
|
1909
|
+
sections(): readonly PromptSection<Tool>[];
|
|
1910
|
+
};
|
|
1911
|
+
batch(register: () => void): Registration;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function createRegistry<Tool extends ToolRegistration = ToolRegistration>(): Registry<Tool>;
|
|
1915
|
+
```
|
|
1916
|
+
|
|
1917
|
+
The `Tool` parameter lets applications attach metadata such as prompt snippets to
|
|
1918
|
+
their tools; the Harness only relies on `ToolRegistration`. Pi-ai declarations
|
|
1919
|
+
derived from a registered tool keep only pi-ai `Tool` fields (`toToolDeclaration`),
|
|
1920
|
+
so application metadata never enters the transcript.
|
|
1921
|
+
|
|
1922
|
+
Registration rules:
|
|
1923
|
+
|
|
1924
|
+
- `createRegistry()` starts with the built-in task definitions of section 8 and
|
|
1925
|
+
the built-in `pi` conversation setup registered first. They have no
|
|
1926
|
+
`Registration`, so they cannot be disposed, and the duplicate rule rejects
|
|
1927
|
+
another task with a built-in name or another `pi` setup. `tasks.list()`
|
|
1928
|
+
includes them. Hooks register against built-in tasks like any other task.
|
|
1929
|
+
Setup keys are unique and ordered like section keys.
|
|
1930
|
+
- Tool names, section keys, and task names are unique among published
|
|
1931
|
+
registrations. Tool wrapper keys are unique per tool name, section wrapper
|
|
1932
|
+
keys per section key, and hook keys per task name. Duplicates reject.
|
|
1933
|
+
Section keys match `^[a-z][a-z0-9_-]*$`.
|
|
1934
|
+
- Keyed slots are ordered by the position at which their key was first
|
|
1935
|
+
registered. The registry remembers that position forever, so re-registering an
|
|
1936
|
+
existing key, as a reload does, keeps its position; a new key appends. Hooks
|
|
1937
|
+
without a key append.
|
|
1938
|
+
- Registered objects are immutable and wrappers are pure: a wrapper returns a
|
|
1939
|
+
new object and never mutates its input.
|
|
1940
|
+
- Every call outside `batch()` publishes immediately. Publication is synchronous,
|
|
1941
|
+
invokes no extension callback, and notifies subscribers.
|
|
1942
|
+
|
|
1943
|
+
`batch(register)` stages every registration and disposal made while `register`
|
|
1944
|
+
runs and publishes once when it returns. It validates only the final staged
|
|
1945
|
+
state, so order inside the block does not matter: disposing an old tool and
|
|
1946
|
+
adding its replacement is a gapless reload. If `register` throws, returns a
|
|
1947
|
+
thenable, or the final state is invalid, nothing is published, staged disposals
|
|
1948
|
+
are rolled back, and `batch()` throws. Only registrations made synchronously
|
|
1949
|
+
inside `register` belong to the batch; a registration made later by a detached
|
|
1950
|
+
continuation is an ordinary immediate registration. Calling `batch()` inside a
|
|
1951
|
+
batch is a programming error and throws. The returned `Registration` covers every
|
|
1952
|
+
registration added in the batch that was not disposed inside it.
|
|
1953
|
+
|
|
1954
|
+
A snapshot is a plain immutable value; nothing is released. The scheduler takes
|
|
1955
|
+
one per phase-handler invocation and passes it through the runtime; handlers and
|
|
1956
|
+
hooks read that snapshot and never take their own. At every normal phase boundary
|
|
1957
|
+
the scheduler takes a fresh one (section 5.4). A tool task keeps the composed tool
|
|
1958
|
+
it pinned until execution settles. Different phases may observe different
|
|
1959
|
+
registry states; nothing requires one run to see a single registry state across
|
|
1960
|
+
its generation, tool, and post-tools tasks. Host operations, such as the create
|
|
1961
|
+
conveniences, take one snapshot inside their commit.
|
|
1962
|
+
|
|
1963
|
+
```ts
|
|
1964
|
+
// ToolTask is the built-in tool task's token; its hooks are listed in section 7.2.
|
|
1965
|
+
const registration = registry.batch(() => {
|
|
1966
|
+
registry.tools.add(grep);
|
|
1967
|
+
registry.hooks.add(ToolTask, { beforeTool: audit }, { key: "audit" });
|
|
1968
|
+
});
|
|
1969
|
+
// later, reload: publish the replacement at once
|
|
1970
|
+
registry.batch(() => {
|
|
1971
|
+
registration.dispose();
|
|
1972
|
+
registry.tools.add(grepV2);
|
|
1973
|
+
registry.hooks.add(ToolTask, { beforeTool: audit2 }, { key: "audit" });
|
|
1974
|
+
});
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
The registry does not track which running work still uses a disposed
|
|
1978
|
+
registration. Work that already started keeps using the code it took, so an
|
|
1979
|
+
extension that frees resources right after `dispose()` can make a still-running
|
|
1980
|
+
call fail with an ordinary error result. Extensions that need graceful disposal
|
|
1981
|
+
manage their resources' lifetime themselves, for example by reference counting.
|
|
1982
|
+
|
|
1983
|
+
### 7.2 Hooks
|
|
1244
1984
|
|
|
1245
1985
|
A hook is a typed question asked by a task before it commits a decision. Hooks
|
|
1246
|
-
are declared by task
|
|
1247
|
-
for
|
|
1248
|
-
|
|
1986
|
+
are declared by task definition and registered through `registry.hooks.add()`
|
|
1987
|
+
Session-wide or, with `scope`, for one conversation and optionally its owned
|
|
1988
|
+
subtree. Subtree matching follows conversation ownership, not history parents.
|
|
1989
|
+
Registration is typed by the task token, but dispatch matches the task's name,
|
|
1990
|
+
so hooks survive a reload of their task; keeping hook signatures compatible
|
|
1991
|
+
across task versions is the task author's responsibility. Hooks run in registry
|
|
1992
|
+
order off the line; a crash before the consuming commit may rerun them. Abort
|
|
1993
|
+
errors always propagate.
|
|
1249
1994
|
|
|
1250
1995
|
| hook | composition | ordinary throw |
|
|
1251
1996
|
|---|---|---|
|
|
1252
|
-
| system instructions | all; draft changes compose; last tool override wins | roll back that handler, report, continue |
|
|
1253
1997
|
| `beforeRequest` | replacement chain | report, continue |
|
|
1254
1998
|
| `afterResponse` | all observers | report, continue |
|
|
1255
1999
|
| `onYield` | first continuation wins | report, continue |
|
|
@@ -1261,7 +2005,7 @@ the consuming commit may rerun them. Abort errors always propagate.
|
|
|
1261
2005
|
Hooks use task memos for durable first-writer-wins decisions. There is no public
|
|
1262
2006
|
semantic event channel; current UI status is document state.
|
|
1263
2007
|
|
|
1264
|
-
### 7.
|
|
2008
|
+
### 7.3 Tools
|
|
1265
2009
|
|
|
1266
2010
|
```ts
|
|
1267
2011
|
type ToolControl = {
|
|
@@ -1277,20 +2021,20 @@ type ToolExecutionResult = {
|
|
|
1277
2021
|
readonly control?: ToolControl;
|
|
1278
2022
|
};
|
|
1279
2023
|
|
|
1280
|
-
interface
|
|
1281
|
-
readonly id:
|
|
1282
|
-
|
|
2024
|
+
interface ConversationHandle {
|
|
2025
|
+
readonly id: ConversationId;
|
|
2026
|
+
submit(submission: InputSubmissionDraft, context: Context): Promise<Submission>;
|
|
1283
2027
|
abort(context: Context): Promise<void>;
|
|
1284
2028
|
waitForIdle(context: Context): Promise<void>;
|
|
1285
2029
|
}
|
|
1286
2030
|
|
|
1287
|
-
interface ToolExecutionApi extends DocumentObserver {
|
|
1288
|
-
readonly taskId:
|
|
1289
|
-
readonly conversationId:
|
|
2031
|
+
interface ToolExecutionApi extends DocumentObserver, DocumentReader {
|
|
2032
|
+
readonly taskId: TaskId;
|
|
2033
|
+
readonly conversationId: ConversationId;
|
|
1290
2034
|
readonly callId: string;
|
|
1291
|
-
|
|
2035
|
+
output(chunk: string | Uint8Array): void;
|
|
2036
|
+
details(value: JsonValue, context: Context): Promise<void>;
|
|
1292
2037
|
commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
|
|
1293
|
-
progress(value: JsonObject, context: Context): Promise<void>;
|
|
1294
2038
|
memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
|
|
1295
2039
|
memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
|
|
1296
2040
|
createTask<I, S extends { phase: string }, R, H extends object>(
|
|
@@ -1298,18 +2042,15 @@ interface ToolExecutionApi extends DocumentObserver {
|
|
|
1298
2042
|
input: I,
|
|
1299
2043
|
options: Omit<TaskOptions, "conversationId">,
|
|
1300
2044
|
context: Context,
|
|
1301
|
-
): Promise<
|
|
1302
|
-
getTask<R>(
|
|
1303
|
-
waitForTask<R>(
|
|
1304
|
-
|
|
1305
|
-
spec: Omit<ConversationSpec, "parent"> & { readonly inherit?: boolean },
|
|
1306
|
-
context: Context,
|
|
1307
|
-
): Promise<OwnedConversation>;
|
|
2045
|
+
): Promise<TaskId<R>>;
|
|
2046
|
+
getTask<R>(id: TaskId<R>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, R> | undefined>;
|
|
2047
|
+
waitForTask<R>(id: TaskId<R>, context: Context): Promise<SettledTask<R>>;
|
|
2048
|
+
conversation(id: ConversationId, context: Context): Promise<ConversationHandle | undefined>;
|
|
1308
2049
|
}
|
|
1309
2050
|
|
|
1310
2051
|
type ToolRegistration = Tool & {
|
|
1311
2052
|
readonly replay?: "safe" | "unsafe";
|
|
1312
|
-
readonly
|
|
2053
|
+
readonly outputLimits?: {
|
|
1313
2054
|
readonly maxBytes?: number;
|
|
1314
2055
|
readonly maxLines?: number;
|
|
1315
2056
|
readonly retain?: "head" | "tail";
|
|
@@ -1322,57 +2063,138 @@ type ToolRegistration = Tool & {
|
|
|
1322
2063
|
};
|
|
1323
2064
|
```
|
|
1324
2065
|
|
|
1325
|
-
Omitted `replay` is `unsafe`. Omitted
|
|
1326
|
-
`retain: "head"`.
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2066
|
+
Omitted `replay` is `unsafe`. Omitted `outputLimits` are 50 KiB, 2,000 lines,
|
|
2067
|
+
and `retain: "head"`.
|
|
2068
|
+
|
|
2069
|
+
A running tool reports two things to the UI, mirroring the two halves of its
|
|
2070
|
+
final result:
|
|
2071
|
+
|
|
2072
|
+
- `output(chunk)` appends running text output, like stdout. If `execute()` omits
|
|
2073
|
+
`content`, the final retained output becomes one text content item; no output
|
|
2074
|
+
becomes an empty content list.
|
|
2075
|
+
- `details(value)` replaces the running details with a complete JSON value; it
|
|
2076
|
+
does not merge keys. If `execute()` omits `details`, the last value becomes the
|
|
2077
|
+
final `details`, so a renderer handles one details shape from the first
|
|
2078
|
+
update through the final result.
|
|
2079
|
+
|
|
2080
|
+
Neither is sent to the model while the tool runs. `output()` synchronously
|
|
2081
|
+
accepts UTF-8 output into that invocation-owned bounded buffer and throws after
|
|
2082
|
+
invocation end. Throttled commits publish the retained output, dropped
|
|
2083
|
+
byte/line counts, and the current details in the tool presentation document. Explicit
|
|
1331
2084
|
text in explicit result content is bounded by the same limits before transcript
|
|
1332
2085
|
persistence; non-text content is retained as declared by its pi-ai type.
|
|
1333
2086
|
|
|
1334
|
-
`
|
|
1335
|
-
|
|
1336
|
-
|
|
2087
|
+
`output()` never spills complete output to a file because spilling requires a
|
|
2088
|
+
filesystem, which may be remote or unavailable. A tool that must preserve
|
|
2089
|
+
complete output spills through the `ExecutionEnv` or `FileSystem` it was given,
|
|
2090
|
+
such as shell execution with spill capture, and reports the resulting path in its
|
|
2091
|
+
details.
|
|
2092
|
+
|
|
2093
|
+
The `details()` promise resolves after the corresponding or coalesced document
|
|
2094
|
+
commit. During normal settlement, accepted output updates drain before
|
|
1337
2095
|
the tool-result entry and terminal task record commit. Abort and close obey
|
|
1338
2096
|
invocation and Session admission gates: uncommitted buffered updates may be
|
|
1339
|
-
discarded, while admitted commits settle.
|
|
1340
|
-
|
|
1341
|
-
follows the fatal Session rule.
|
|
1342
|
-
|
|
1343
|
-
Tools are
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
2097
|
+
discarded, while admitted commits settle. Cancellation, callback, tracker
|
|
2098
|
+
preparation, and checkpoint failures occur before Storage admission and do not
|
|
2099
|
+
poison the Session. An uncertain Storage failure follows the fatal Session rule.
|
|
2100
|
+
|
|
2101
|
+
Tools are registry entries (section 7.1) with name, description, JSON schema,
|
|
2102
|
+
replay policy, and execute function. `registry.tools.wrap(name, key, wrapper)`
|
|
2103
|
+
decorates a tool without replacing it. Each snapshot composes the current base
|
|
2104
|
+
tool with its wrappers in registry order; a wrapper never captures a base, so
|
|
2105
|
+
reloading the base keeps its wrappers. A wrapper that throws or returns a tool
|
|
2106
|
+
with a different name makes that tool absent from the snapshot (fail closed:
|
|
2107
|
+
not offered, and calls produce `tool_unavailable`) and is reported. A wrapper
|
|
2108
|
+
without a base contributes nothing. The composite supplies the declaration,
|
|
2109
|
+
argument validation, replay policy, and execution.
|
|
2110
|
+
|
|
2111
|
+
A tool call is accepted only if it was offered in the request's effective
|
|
2112
|
+
system/tool history. A call to a tool that is not offered or has no registered
|
|
2113
|
+
implementation produces the `tool_unavailable` error result described in section
|
|
2114
|
+
2.2 instead of failing the run. The tool task resolves the composed tool once
|
|
2115
|
+
from its snapshot before argument validation and pins it until execution
|
|
2116
|
+
settles, even across later snapshot refreshes. Arguments must satisfy both the offered
|
|
2117
|
+
declaration and the pinned implementation's schema; they are validated before
|
|
2118
|
+
and after `beforeTool` hooks.
|
|
1347
2119
|
|
|
1348
2120
|
After hooks and validation, the tool task durably records the final call and
|
|
1349
2121
|
resolved replay policy before execution. Recovery does not rerun `beforeTool`
|
|
1350
|
-
and
|
|
2122
|
+
and passes the same stored arguments to `execute()`. A replay-safe tool may
|
|
2123
|
+
reconstruct a submission from those arguments when that transformation is pure.
|
|
2124
|
+
Random values, timestamps, mutable document/configuration reads, or other derived
|
|
2125
|
+
inputs that must remain stable are first captured in a durable memo, checkpoint,
|
|
2126
|
+
or task input. A background supervisor receives the final submission draft in
|
|
2127
|
+
its own durable input so it can finish independently. Recovery does not let a
|
|
2128
|
+
changed registry declaration alter the stored replay policy.
|
|
2129
|
+
|
|
2130
|
+
A tool-acquired conversation handle accepts only input submissions; tools use
|
|
2131
|
+
ordinary transaction writes for passive entries.
|
|
1351
2132
|
|
|
1352
2133
|
A tool executes in a durable task. It may:
|
|
1353
2134
|
|
|
1354
|
-
-
|
|
2135
|
+
- publish bounded running output and details to its presentation document;
|
|
1355
2136
|
- commit memos;
|
|
1356
2137
|
- create and wait for tasks;
|
|
1357
|
-
- create owned conversations
|
|
2138
|
+
- atomically create or fork explicitly owned conversations through `commit()`;
|
|
1358
2139
|
- observe documents for which it has a token/reference;
|
|
1359
2140
|
- mutate authorized documents through `commit()`;
|
|
1360
2141
|
- return bounded model content and separate diagnostic details.
|
|
1361
2142
|
|
|
1362
|
-
Tool operations use the invoking task's admission and
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
the
|
|
2143
|
+
Tool operations use the invoking task's admission and invocation-lifetime gates.
|
|
2144
|
+
Task creation defaults to that task's conversation. Trusted document and
|
|
2145
|
+
conversation access follows sections 3.3 and 9.2; there is no additional subtree
|
|
2146
|
+
authorization layer. Possession of a Session-global typed ID is sufficient in
|
|
2147
|
+
trusted code.
|
|
2148
|
+
|
|
2149
|
+
Tools have no conversation-creation convenience method. They create or fork a
|
|
2150
|
+
conversation inside `commit()`, state its ownership explicitly, atomically stage
|
|
2151
|
+
any related document/task writes, and receive only the inert record. After that
|
|
2152
|
+
commit settles, `conversation(id)` acquires an invocation-bound operational
|
|
2153
|
+
handle for submission, explicit abort, and idle waits without changing
|
|
2154
|
+
ownership. A commit callback must not call `conversation()` or use a previously
|
|
2155
|
+
acquired handle: nested Session operations and external effects are forbidden
|
|
2156
|
+
while the mutation line is held. A handle's operations reject after the
|
|
2157
|
+
invocation ends. A `Submission` returned by its `submit()` is invocation-bound in
|
|
2158
|
+
the same way; the admitted submission remains durable after those methods reject.
|
|
2159
|
+
Invocation-owned document watches stop when the invocation ends.
|
|
2160
|
+
|
|
2161
|
+
A foreground subagent conversation is explicitly owned by its tool task. One
|
|
2162
|
+
transaction creates or forks the child and records its durable registry mapping;
|
|
2163
|
+
after settlement the tool reacquires the child, submits with the registered
|
|
2164
|
+
request ID, and waits for that submission's result. Aborting or abnormally
|
|
2165
|
+
terminalizing the tool task cascades through the owned scope.
|
|
2166
|
+
|
|
2167
|
+
A background subagent is provisioned in one transaction. After deduplicating by
|
|
2168
|
+
its durable registry key, the tool stages a background supervisor task `B`, a
|
|
2169
|
+
child conversation `C` explicitly owned by `B`, and the registry mapping from the
|
|
2170
|
+
semantic name to `C` plus its stable request ID. `B` may be staged earlier in the
|
|
2171
|
+
same transaction and used immediately as `C`'s owner. Its durable input contains
|
|
2172
|
+
the exact submission draft and the registry location/key; it need not contain
|
|
2173
|
+
`C`'s not-yet-created ID. Once scheduled, `B` resolves `C` from the mapping,
|
|
2174
|
+
verifies the immutable owner edge, and performs normal `Conversation.submit()`.
|
|
2175
|
+
A crash before admission makes `B` retry; a crash after admission returns the
|
|
2176
|
+
existing request-ID-deduplicated `Submission`. The initiating tool may race the
|
|
2177
|
+
same submit for lower latency and wait only for the durable admission receipt.
|
|
2178
|
+
The supervisor may complete after setup: its terminal record retains
|
|
2179
|
+
`background`, so ancestor ordinary abort and idle traversal continue to stop at
|
|
2180
|
+
its owned scope.
|
|
2181
|
+
|
|
2182
|
+
Applications may maintain a conversation document mapping semantic subagent
|
|
2183
|
+
names to durable conversation IDs and application-minted submission request IDs.
|
|
2184
|
+
Such a live registry uses `fork: "initial"` so children do not inherit the
|
|
2185
|
+
parent's agent list and its update is not a selected fork source. Conversation
|
|
2186
|
+
creation/forking, supervisor creation when applicable, and the registry mapping
|
|
2187
|
+
commit atomically. The supervisor input, tool's durable final arguments, or a
|
|
2188
|
+
durable checkpoint must retain enough information to reconstruct the exact
|
|
2189
|
+
submission draft; the registry itself need not duplicate that payload. Initial
|
|
2190
|
+
or later input then uses the registered stable request ID with the full
|
|
2191
|
+
`Conversation.submit()` state machine. A crash before admission leaves a durable
|
|
2192
|
+
request to submit; a crash after admission retries the same request and receives
|
|
2193
|
+
the existing `Submission`. A read-only lookup by conversation/request ID can
|
|
2194
|
+
report absence or return the durable queued/placed/done/unanswered receipt.
|
|
2195
|
+
Submission admission therefore needs no private transaction shortcut and does
|
|
2196
|
+
not move onto `Tx`. Kernel ownership indexes independently drive abort and idle
|
|
2197
|
+
traversal.
|
|
1376
2198
|
|
|
1377
2199
|
A tool result may request `addTools`, `terminate`, or `handoff`. Post-tools
|
|
1378
2200
|
applies added tool names to configured loadout, uses a final boundary for
|
|
@@ -1380,27 +2202,19 @@ terminate/handoff, and writes a headed handoff entry when requested.
|
|
|
1380
2202
|
|
|
1381
2203
|
On reopen, a tool reruns only when both its stored intent policy and the current
|
|
1382
2204
|
registered declaration say `safe`. A current `unsafe` declaration may veto a
|
|
1383
|
-
stored-safe replay; a current-safe declaration never upgrades stored unsafe.
|
|
1384
|
-
|
|
2205
|
+
stored-safe replay; a current-safe declaration never upgrades stored unsafe. A
|
|
2206
|
+
tool with no current registration is treated as `unsafe`. Every other orphaned
|
|
2207
|
+
effect produces an interrupted result containing the
|
|
1385
2208
|
durable partial output. Completed, failed, and aborted
|
|
1386
2209
|
tool terminal outcomes retain their tool-result entry ID for post-tools.
|
|
1387
2210
|
|
|
1388
|
-
### 7.
|
|
1389
|
-
|
|
1390
|
-
```ts
|
|
1391
|
-
interface SystemSection<T extends JsonValue = JsonValue> {
|
|
1392
|
-
readonly key: string;
|
|
1393
|
-
render(value: T): string;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
function defineSystemSection<T extends JsonValue>(definition: {
|
|
1397
|
-
readonly key: string;
|
|
1398
|
-
render(value: T): string;
|
|
1399
|
-
}): SystemSection<T>;
|
|
1400
|
-
```
|
|
2211
|
+
### 7.4 System prompt and dynamic tools
|
|
1401
2212
|
|
|
1402
|
-
Pico
|
|
1403
|
-
|
|
2213
|
+
Pico has no durable prompt sections. The registry's system prompt slot produces
|
|
2214
|
+
the desired sections for each request; the transcript's `pi.system` entries are
|
|
2215
|
+
the only durable record of what the model saw. Pico stores prompt and tool
|
|
2216
|
+
changes directly as PR #9548 `SystemMessage` values at their transcript
|
|
2217
|
+
positions, always with empty `content`:
|
|
1404
2218
|
|
|
1405
2219
|
```ts
|
|
1406
2220
|
type SystemEntry = EntryRecord & {
|
|
@@ -1410,8 +2224,8 @@ type SystemEntry = EntryRecord & {
|
|
|
1410
2224
|
|
|
1411
2225
|
const baseline: SystemMessage = {
|
|
1412
2226
|
role: "system",
|
|
1413
|
-
content:
|
|
1414
|
-
sections: {
|
|
2227
|
+
content: "",
|
|
2228
|
+
sections: { preamble: renderedPreamble, cwd: renderedCwd },
|
|
1415
2229
|
toolsAdded: allEffectiveTools,
|
|
1416
2230
|
timestamp: now,
|
|
1417
2231
|
};
|
|
@@ -1426,10 +2240,51 @@ const delta: SystemMessage = {
|
|
|
1426
2240
|
};
|
|
1427
2241
|
```
|
|
1428
2242
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2243
|
+
Generation preparation takes these steps against its phase snapshot:
|
|
2244
|
+
|
|
2245
|
+
1. Read the committed configuration and replay the active transcript's system
|
|
2246
|
+
messages into the shown sections and offered tools.
|
|
2247
|
+
2. Compute the desired tools: active names that the snapshot resolves, in
|
|
2248
|
+
configured order, as composed by their wrappers.
|
|
2249
|
+
3. Render every registered section, in registry order, with `PromptInput`: the
|
|
2250
|
+
conversation, those tools, the shown sections, the configured model and
|
|
2251
|
+
thinking level, and a `DocumentReader` for committed documents (the task
|
|
2252
|
+
runtime). The results are the desired sections.
|
|
2253
|
+
4. Compare desired sections and tool declarations with the replayed state and
|
|
2254
|
+
append one positional `pi.system` entry when they differ.
|
|
2255
|
+
|
|
2256
|
+
Preparation does not recheck the transcript before appending: only the Harness
|
|
2257
|
+
writes to a busy conversation, through submissions, run tasks, and boundaries,
|
|
2258
|
+
so the transcript it read is still current (section 12).
|
|
2259
|
+
|
|
2260
|
+
Model and thinking level are request options, not prompt state.
|
|
2261
|
+
|
|
2262
|
+
Registered sections are the only source of prompt text; there is no separate
|
|
2263
|
+
builder. `systemPrompt.section(key, render, { tag })` adds a section, and
|
|
2264
|
+
`systemPrompt.wrap(key, wrapperKey, wrapper)` decorates one, composed per
|
|
2265
|
+
snapshot like tool wrappers. Sections render in registry order: the position at
|
|
2266
|
+
which each key was first registered. A section whose `render` returns
|
|
2267
|
+
`undefined` is omitted, which is how a section varies by conversation. With
|
|
2268
|
+
`tag` omitted or true, text is wrapped as `<key>\n...\n</key>`. A section that
|
|
2269
|
+
throws keeps its shown text, if any, and is reported; the request is still sent.
|
|
2270
|
+
Errors thrown after the generation invocation is cancelled propagate; an abort
|
|
2271
|
+
error of the section's own, such as its fetch timing out, is an ordinary failure. With no registered sections, the desired section set is
|
|
2272
|
+
empty.
|
|
2273
|
+
|
|
2274
|
+
A minimal prompt is one untagged section:
|
|
2275
|
+
|
|
2276
|
+
```ts
|
|
2277
|
+
registry.systemPrompt.section("preamble", () => "You are a helpful assistant.", { tag: false });
|
|
2278
|
+
```
|
|
2279
|
+
|
|
2280
|
+
Sections read per-conversation data through `input.read`. For example, a
|
|
2281
|
+
coding agent keeps its own conversation document with the agent kind, preamble,
|
|
2282
|
+
and working directory; its preamble and cwd sections render from that document,
|
|
2283
|
+
and its AGENTS.md and skills sections return `undefined` for subagent
|
|
2284
|
+
conversations.
|
|
2285
|
+
Renderers must be deterministic for equal inputs: any change in rendered text,
|
|
2286
|
+
such as an embedded timestamp, appends a system delta and invalidates provider
|
|
2287
|
+
prompt caches.
|
|
1433
2288
|
|
|
1434
2289
|
Replay applies messages in transcript order. Non-empty `content` appends
|
|
1435
2290
|
instructions. A section string adds or replaces that name without moving an
|
|
@@ -1437,74 +2292,64 @@ existing section; `null` removes it, and a later re-addition appends it to the
|
|
|
1437
2292
|
ordered section map. Within one message, tool removals happen before additions,
|
|
1438
2293
|
so a same-name replacement gets the new declaration and position.
|
|
1439
2294
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
section in desired order. This makes order-only changes and deletion/re-addition
|
|
1447
|
-
between requests replay exactly; merely restating equal values is insufficient.
|
|
2295
|
+
Preparation compares both values and order. If values can be patched without
|
|
2296
|
+
changing order, it emits the minimal patch. If shown and desired section order
|
|
2297
|
+
differ, one commit appends two `pi.system` entries: the first removes every
|
|
2298
|
+
shown section with `null`, and the second re-adds every desired section in
|
|
2299
|
+
desired order. This makes order-only changes and deletion/re-addition between
|
|
2300
|
+
requests replay exactly; merely restating equal values is insufficient.
|
|
1448
2301
|
|
|
1449
2302
|
A PR #9548 `SystemMessage` is always a patch, not a reset: it cannot remove
|
|
1450
2303
|
previous `content` or restore section order merely by restating current values.
|
|
1451
|
-
Therefore, when a head removes the previous request-visible baseline,
|
|
2304
|
+
Therefore, when a head removes the previous request-visible baseline, which is
|
|
2305
|
+
the case when the active context has a head marker and no `pi.system` entry
|
|
2306
|
+
follows that marker, the new
|
|
1452
2307
|
`pi.system` entry adds `ContextEdit` omissions for every earlier `pi.system`
|
|
1453
2308
|
entry still retained after the cut. Its own message is then a complete baseline
|
|
1454
|
-
containing
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
`SystemMessage.sections` remain
|
|
1467
|
-
changes. Pi-ai decides whether to
|
|
1468
|
-
|
|
1469
|
-
stored transcript for provider
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
If old extension code ignores cancellation and never settles, graceful in-process
|
|
1500
|
-
reload cannot complete. The host must terminate that isolated worker/process
|
|
1501
|
-
before opening the Session under the new generation. Old and new generations
|
|
1502
|
-
must never own the same Session concurrently.
|
|
1503
|
-
|
|
1504
|
-
Generation-pinned registries that drain old callbacks while routing new work to
|
|
1505
|
-
new code, plus explicit compatible task takeover, are possible future work. Safe
|
|
1506
|
-
forced takeover of arbitrary non-cooperative JavaScript requires worker/process
|
|
1507
|
-
isolation and is not a v1 promise.
|
|
2309
|
+
containing every desired section in order and every effective tool declaration.
|
|
2310
|
+
Model-context replay sees the new baseline instead of the omitted retained
|
|
2311
|
+
deltas. Preparation writes this baseline even when it restates the replayed
|
|
2312
|
+
values, so every later preparation finds a `pi.system` entry after the marker.
|
|
2313
|
+
Head rebaselining takes precedence over ordinary order/value patching.
|
|
2314
|
+
Without a head cut, an order mismatch uses the two-entry remove/re-add sequence
|
|
2315
|
+
above; only when order already matches does preparation emit the minimal changed
|
|
2316
|
+
values, `null` removals, and tool additions/removals. A changed tool declaration
|
|
2317
|
+
is removed and re-added in the same message. When the offered tool order differs
|
|
2318
|
+
from the desired order, the message removes every offered tool and re-adds the
|
|
2319
|
+
desired tools in order.
|
|
2320
|
+
|
|
2321
|
+
The rendered strings stored in historical `SystemMessage.sections` remain
|
|
2322
|
+
authoritative even if the current renderer changes. Pi-ai decides whether to
|
|
2323
|
+
send the messages positionally to a capable provider or fold them into one
|
|
2324
|
+
leading system message; Pico does not rewrite its stored transcript for provider
|
|
2325
|
+
compatibility.
|
|
2326
|
+
|
|
2327
|
+
### 7.5 Extension reload
|
|
2328
|
+
|
|
2329
|
+
Extension code is reloaded in process through the registry. The host publishes
|
|
2330
|
+
the new registrations and disposes the old ones in one `batch()`. The Harness
|
|
2331
|
+
keeps running; no close or reopen is required.
|
|
2332
|
+
|
|
2333
|
+
- New phase invocations and tool tasks use the new registrations immediately;
|
|
2334
|
+
running invocations see them at their next phase boundary.
|
|
2335
|
+
- Work already running keeps its snapshot until its phase handler settles, and a
|
|
2336
|
+
pinned tool keeps its implementation until its execution settles. Replacement
|
|
2337
|
+
never signals or interrupts it. Resources the old code uses are the
|
|
2338
|
+
extension's responsibility (section 7.1).
|
|
2339
|
+
- A task definition replaced by name hands over at its next normal phase boundary
|
|
2340
|
+
(section 5.4). A task definition must increase its version when the meaning
|
|
2341
|
+
of persisted input or checkpoint state changes and migrate supported older
|
|
2342
|
+
state; a failed migration blocks the task rather than terminalizing it.
|
|
2343
|
+
- Keyed registrations re-registered under the same key keep their position, so a
|
|
2344
|
+
reload does not reorder hooks, tools, or sections.
|
|
2345
|
+
- Document definitions are passed explicitly to typed access and need no
|
|
2346
|
+
registration; changed tokens take effect on their next access.
|
|
2347
|
+
|
|
2348
|
+
If old extension code ignores cancellation and never settles, new work already
|
|
2349
|
+
uses the replacement. Harness close still joins every invocation. Safe forced termination of arbitrary
|
|
2350
|
+
non-cooperative JavaScript requires worker/process isolation; that host
|
|
2351
|
+
terminates the process and reopens the Session from durable state. Old and new
|
|
2352
|
+
Harness instances must never own the same Session concurrently.
|
|
1508
2353
|
|
|
1509
2354
|
## 8. Built-in tasks
|
|
1510
2355
|
|
|
@@ -1512,10 +2357,10 @@ The initial implementation provides:
|
|
|
1512
2357
|
|
|
1513
2358
|
| kind | responsibility |
|
|
1514
2359
|
|---|---|
|
|
1515
|
-
| generation | prepare system/loadout, request or poll model, retry, classify response |
|
|
1516
|
-
| tool | validate, hook, execute, persist
|
|
1517
|
-
| post-tools | wait for tools, apply controls, run boundary, continue generation |
|
|
1518
|
-
| collapse | select a transcript range, summarize, append a headed summary |
|
|
2360
|
+
| `pi.generation` | prepare system/loadout, request or poll model, retry, classify response |
|
|
2361
|
+
| `pi.tool` | validate, hook, execute, persist output and details, append result |
|
|
2362
|
+
| `pi.post-tools` | wait for tools, apply controls, run boundary, continue generation |
|
|
2363
|
+
| `pi.collapse` | select a transcript range, summarize, append a headed summary |
|
|
1519
2364
|
|
|
1520
2365
|
Generation uses `HarnessOptions.models` without a Pico-specific model adapter. It
|
|
1521
2366
|
resolves `models.getModel(ref.provider, ref.modelId)`, builds a pi-ai `Context`
|
|
@@ -1536,152 +2381,284 @@ Bounded output records whether content was truncated and any retained file path.
|
|
|
1536
2381
|
Compaction changes model context by appending a summary entry with a head. It
|
|
1537
2382
|
does not delete transcript history.
|
|
1538
2383
|
|
|
2384
|
+
### 8.1 Built-in entries
|
|
2385
|
+
|
|
2386
|
+
Built-in entry kinds carry no `data`; each is exported as an `Entry` token.
|
|
2387
|
+
|
|
2388
|
+
| kind | `model` | written by |
|
|
2389
|
+
|---|---|---|
|
|
2390
|
+
| `pi.user` | `[UserMessage]`, timestamp from the Harness clock at admission or placement | submissions |
|
|
2391
|
+
| `pi.assistant` | `[AssistantMessage]` with any stop reason | generation |
|
|
2392
|
+
| `pi.system` | `[SystemMessage]` with `content: ""` (section 7.4) | generation preparation |
|
|
2393
|
+
| `pi.tool-result` | `[ToolResultMessage]` | tool tasks |
|
|
2394
|
+
|
|
2395
|
+
Every provider result becomes a `pi.assistant` entry: answers, failed attempts
|
|
2396
|
+
with their error text and usage, and converted partials with stop reason
|
|
2397
|
+
`aborted`. Context derivation (section 2.1, rule 9) keeps failed and aborted
|
|
2398
|
+
messages out of later requests, so no separate usage or notice kind exists.
|
|
2399
|
+
|
|
2400
|
+
### 8.2 Live document
|
|
2401
|
+
|
|
2402
|
+
```ts
|
|
2403
|
+
type LiveState = {
|
|
2404
|
+
/** Run control (section 6); present exactly while the conversation is busy. */
|
|
2405
|
+
run?: { taskId: TaskId; inputs: SubmissionId[] };
|
|
2406
|
+
/** Presentation of the current generation attempt. */
|
|
2407
|
+
generation?: {
|
|
2408
|
+
attempt: number;
|
|
2409
|
+
/** Committed throttled partial of the in-flight response. */
|
|
2410
|
+
message?: JsonRepresentation<AssistantMessage>;
|
|
2411
|
+
/** Durable backoff before the next attempt. */
|
|
2412
|
+
retry?: { at: number; error: string };
|
|
2413
|
+
/** Provider-side deferred response being polled. */
|
|
2414
|
+
deferred?: { pollAt: number };
|
|
2415
|
+
};
|
|
2416
|
+
};
|
|
2417
|
+
```
|
|
2418
|
+
|
|
2419
|
+
| field | value |
|
|
2420
|
+
|---|---|
|
|
2421
|
+
| kind | `pi.live` |
|
|
2422
|
+
| version | `1` |
|
|
2423
|
+
| scope/history/fork | conversation, `latest`, `initial` |
|
|
2424
|
+
| `initial()` | `{}` |
|
|
2425
|
+
| checkpoint | complete base whenever `generation` is absent |
|
|
2426
|
+
| view mount | `docs["pi.live"]` |
|
|
2427
|
+
| created | with every Harness conversation (section 2.2) |
|
|
2428
|
+
|
|
2429
|
+
Nothing is in flight at every turn boundary and while idle, so the stored delta
|
|
2430
|
+
chain spans at most one generation, including its retries and deferred polls, or
|
|
2431
|
+
one tool round, and each base is small. Tool progress adds its own condition with the tool task.
|
|
2432
|
+
|
|
2433
|
+
Tool progress and compaction status join this document with the tool and
|
|
2434
|
+
collapse tasks. Partials are normalized to strict JSON before assignment. Every
|
|
2435
|
+
terminal path of a run task removes `run` and `generation` in the commit that
|
|
2436
|
+
settles the run's inputs. `tx.settleSubmission()` stages each input's new status
|
|
2437
|
+
and resolves the transaction's latest candidate submission record, falling back
|
|
2438
|
+
to committed state, during assembly, like task-document validation (section 3.3),
|
|
2439
|
+
so it is not a caller table read and works after the commit's first table write.
|
|
2440
|
+
|
|
2441
|
+
### 8.3 Generation
|
|
2442
|
+
|
|
2443
|
+
```ts
|
|
2444
|
+
type GenerationInput = {};
|
|
2445
|
+
type GenerationCheckpoint =
|
|
2446
|
+
| { phase: "prepare"; attempt: number }
|
|
2447
|
+
| {
|
|
2448
|
+
phase: "request";
|
|
2449
|
+
attempt: number;
|
|
2450
|
+
model: ModelRef;
|
|
2451
|
+
thinkingLevel: ModelThinkingLevel;
|
|
2452
|
+
streamOptions: ConversationStreamOptions;
|
|
2453
|
+
/** Newest entry included in the request. */
|
|
2454
|
+
cutoff: EntryId;
|
|
2455
|
+
}
|
|
2456
|
+
| { phase: "retry"; attempt: number; until: number }
|
|
2457
|
+
| { phase: "poll"; attempt: number; model: ModelRef; handle: DeferredHandle; pollAt: number };
|
|
2458
|
+
type GenerationResult = { entryId: EntryId };
|
|
2459
|
+
```
|
|
2460
|
+
|
|
2461
|
+
`pi.generation` is version 1 and starts at `{ phase: "prepare", attempt: 1 }`.
|
|
2462
|
+
The run's inputs live in `pi.live.run`, not in the task input.
|
|
2463
|
+
|
|
2464
|
+
- `prepare` runs section 7.4 against the committed configuration. When no model
|
|
2465
|
+
is configured or `models.getModel()` does not know it, the task fails with
|
|
2466
|
+
`no_model`. Otherwise one commit appends the planned `pi.system` entries and
|
|
2467
|
+
moves to `request` with the new tail as `cutoff` and the configuration's model,
|
|
2468
|
+
thinking level, and stream options.
|
|
2469
|
+
- `request` and `poll` resolve the checkpoint's model through
|
|
2470
|
+
`models.getModel()`; an unknown model fails the task with `no_model`, like
|
|
2471
|
+
`prepare`. `request` converts a leftover partial (below) before it resolves
|
|
2472
|
+
the model.
|
|
2473
|
+
- `request` first converts a committed partial left in `pi.live` by an
|
|
2474
|
+
interrupted attempt into an aborted `pi.assistant` entry. It then streams the
|
|
2475
|
+
model context through `cutoff` with the invocation signal, the thinking level
|
|
2476
|
+
as `reasoning` (omitted for `off`), and the pinned `streamOptions`,
|
|
2477
|
+
committing throttled partials. Recovery resends the same messages with the
|
|
2478
|
+
same pinned model, thinking level, and stream options.
|
|
2479
|
+
- Before classifying, the handler stops the partial throttle and awaits any
|
|
2480
|
+
partial commit in flight, so no stale partial lands after the outcome. The
|
|
2481
|
+
terminal message is classified in one commit that also clears the partial:
|
|
2482
|
+
- `stop`/`length`: append the answer, settle the run's inputs `done`, remove
|
|
2483
|
+
`run` and `generation`, and complete with `{ entryId }`. The same commit
|
|
2484
|
+
applies the final boundary (section 6).
|
|
2485
|
+
- `toolUse`: append the assistant entry and continue through the tool chain.
|
|
2486
|
+
- `error` that `isRetryableAssistantError()` accepts while the conversation's
|
|
2487
|
+
retry policy allows another attempt (`enabled` and `attempt <= maxRetries`,
|
|
2488
|
+
so `maxRetries` counts retries after the first attempt): append the error entry and move to
|
|
2489
|
+
`retry` with `until = now + retryDelayMs(policy, attempt)`.
|
|
2490
|
+
- any other `error`, or `aborted` without an abort mark: append the error
|
|
2491
|
+
entry, settle the inputs `unanswered` with `model_error`, remove `run` and
|
|
2492
|
+
`generation`, and fail.
|
|
2493
|
+
- `deferred`: move to `poll` with `pollAt = now + (handle.pollAfterMs ?? 5000)`.
|
|
2494
|
+
- `retry` sleeps until `until`, then returns to `prepare` with the next attempt,
|
|
2495
|
+
so configuration changes made during the backoff apply.
|
|
2496
|
+
- `poll` sleeps until `pollAt` and calls `models.fetchDeferred()`. A still
|
|
2497
|
+
deferred result moves `pollAt` strictly later; any other result is classified
|
|
2498
|
+
as above.
|
|
2499
|
+
- The abort handler calls `models.cancelDeferred()` in `poll` when the model is
|
|
2500
|
+
known (a failure is reported), converts a committed partial, settles the inputs `unanswered` with
|
|
2501
|
+
`aborted`, removes `run` and `generation`, and ends `aborted`.
|
|
2502
|
+
|
|
2503
|
+
Input submissions settle `unanswered` with one of these reasons: `no_model`,
|
|
2504
|
+
`model_error` (detail: provider error text), `aborted`, `faulted` (detail: error
|
|
2505
|
+
message), or an orphaning blocked reason (section 5.4). Fault and orphan
|
|
2506
|
+
settlement discard a committed partial without writing a transcript entry.
|
|
2507
|
+
|
|
1539
2508
|
|
|
1540
2509
|
## 9. Document observation and Chord
|
|
1541
2510
|
|
|
1542
|
-
### 9.1 Document
|
|
2511
|
+
### 9.1 Document state
|
|
2512
|
+
|
|
2513
|
+
Chord's existing replicated-state layer exposes this source-adoption contract:
|
|
1543
2514
|
|
|
1544
2515
|
```ts
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
readonly
|
|
2516
|
+
interface ReplicatedStateSourceFrame<T> {
|
|
2517
|
+
readonly cursor: number;
|
|
2518
|
+
readonly value: T;
|
|
2519
|
+
readonly ops: readonly Op[];
|
|
2520
|
+
readonly context: Context;
|
|
1548
2521
|
}
|
|
1549
2522
|
|
|
2523
|
+
interface ReplicatedStateSourceAttachment<T> {
|
|
2524
|
+
/** Fixed immutable snapshot captured at the atomic attachment boundary. */
|
|
2525
|
+
readonly snapshot: { readonly value: T; readonly cursor: number };
|
|
2526
|
+
/** Install the sole listener and synchronously drain every buffered frame. */
|
|
2527
|
+
activate(listener: (frame: ReplicatedStateSourceFrame<T>) => void): void;
|
|
2528
|
+
dispose(): void;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
interface ReplicatedStateSource<T> {
|
|
2532
|
+
/** Atomically capture a snapshot and begin buffering every later commit. */
|
|
2533
|
+
attach(): ReplicatedStateSourceAttachment<T>;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
interface ReplicatedStateSourceOptions {
|
|
2537
|
+
readonly onError?: (error: Error) => void;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
interface AttachedReplicatedState<T> extends ReplicatedState<T> {
|
|
2541
|
+
readonly value: T;
|
|
2542
|
+
dispose(): void;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function replicatedState<T>(
|
|
2546
|
+
source: ReplicatedStateSource<T>,
|
|
2547
|
+
options?: ReplicatedStateSourceOptions,
|
|
2548
|
+
): AttachedReplicatedState<T>;
|
|
2549
|
+
|
|
2550
|
+
type DocumentState<T extends JsonObject> =
|
|
2551
|
+
AttachedReplicatedState<Readonly<T> | null>;
|
|
2552
|
+
|
|
1550
2553
|
type WatchEnd =
|
|
1551
2554
|
| { readonly reason: "stopped" | "cancelled" | "session_closed" | "retired" }
|
|
1552
2555
|
| { readonly reason: "listener_error"; readonly error: Error };
|
|
1553
2556
|
|
|
1554
2557
|
interface WatchHandle<T> {
|
|
1555
|
-
/**
|
|
2558
|
+
/** Acquisition revision before start; latest delivered immutable revision afterward. */
|
|
1556
2559
|
readonly value: T;
|
|
1557
2560
|
/** Installs the sole serialized asynchronous listener. */
|
|
1558
|
-
start(listener: (ops: readonly Op[], context: Context) => Promise<void>): void;
|
|
1559
|
-
/**
|
|
1560
|
-
stop():
|
|
1561
|
-
/** Settles
|
|
2561
|
+
start(listener: (value: T, ops: readonly Op[], context: Context) => Promise<void>): void;
|
|
2562
|
+
/** Idempotently stops future callbacks and returns this watch's terminal result. */
|
|
2563
|
+
stop(): Promise<WatchEnd>;
|
|
2564
|
+
/** Settles when the watch terminates; an already-running callback remains caller-owned. */
|
|
1562
2565
|
readonly closed: Promise<WatchEnd>;
|
|
1563
2566
|
}
|
|
1564
2567
|
|
|
1565
2568
|
type DocumentWatch<T extends JsonObject> = WatchHandle<Readonly<T> | null>;
|
|
1566
2569
|
|
|
1567
2570
|
interface DocumentObserver {
|
|
1568
|
-
watchDoc<T extends JsonObject>(token:
|
|
1569
|
-
watchDoc<T extends JsonObject
|
|
2571
|
+
watchDoc<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
2572
|
+
watchDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
2573
|
+
watchDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
2574
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
2575
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
2576
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1570
2577
|
}
|
|
1571
2578
|
```
|
|
1572
2579
|
|
|
1573
|
-
`
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
function documentReplicatedState<T extends JsonObject>(
|
|
1582
|
-
source: DocumentSource<T>,
|
|
1583
|
-
context: Context,
|
|
1584
|
-
): Promise<ReplicatedDocument<T>>;
|
|
1585
|
-
```
|
|
2580
|
+
`documentState()` attaches Pico's committed document stream to Chord before
|
|
2581
|
+
returning. The returned state is already hydrated with the exact shareable
|
|
2582
|
+
immutable revision, has no mutation methods, and publishes every later exact
|
|
2583
|
+
committed revision and operation batch without another tracker, value copy, or
|
|
2584
|
+
re-diff. Disposing it unregisters only that observation; Pico remains the sole
|
|
2585
|
+
document mutator.
|
|
1586
2586
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
and ends that incarnation's stream. The service may then withdraw itself; if it
|
|
2587
|
+
State and watch acquisition never create; absent lookup returns `undefined`.
|
|
2588
|
+
Successful acquisition binds one concrete incarnation. Retirement publishes a
|
|
2589
|
+
JSON `null` root replacement and ends that incarnation's stream. If the state
|
|
1591
2590
|
remains exposed, consumers see `null`, never stale state. A later recreation
|
|
1592
|
-
requires acquiring a new
|
|
2591
|
+
requires acquiring a new state or watch.
|
|
1593
2592
|
|
|
1594
|
-
Each
|
|
1595
|
-
does not persist or expose that sequence through `WatchHandle`.
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2593
|
+
Each document state assigns its own in-memory contiguous Chord delivery sequence.
|
|
2594
|
+
Pico does not persist or expose that sequence through `WatchHandle`. States and watches retain immutable revisions independently of the loaded tracker
|
|
2595
|
+
cache. Reopen creates a new state lifetime and hydration.
|
|
2596
|
+
|
|
2597
|
+
Internally, state attachment uses Chord's synchronous atomic snapshot-and-register
|
|
2598
|
+
boundary: the snapshot includes every commit before attachment, and every later
|
|
2599
|
+
exact frame is buffered until activation. This prevents a snapshot from being
|
|
2600
|
+
paired with operations based on a newer unseen revision.
|
|
1599
2601
|
|
|
1600
2602
|
### 9.2 `watchDoc`
|
|
1601
2603
|
|
|
1602
|
-
Tasks, hooks, and tools may observe any document for which their code
|
|
1603
|
-
and
|
|
2604
|
+
Tasks, hooks, and tools may observe any existing document for which their code
|
|
2605
|
+
has a token and owner/key. There is no additional subtree permission system inside trusted
|
|
1604
2606
|
Session code.
|
|
1605
2607
|
|
|
1606
|
-
`watchDoc()`
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
2608
|
+
On the Session line, `watchDoc()` resolves one existing concrete incarnation,
|
|
2609
|
+
captures its current immutable tracker revision in O(1), and registers for every
|
|
2610
|
+
later exact committed revision and operation batch. It returns `undefined` when
|
|
2611
|
+
absent. Before `start()`, `watch.value` remains the acquisition revision while
|
|
2612
|
+
later frames buffer.
|
|
1611
2613
|
|
|
1612
2614
|
```ts
|
|
1613
|
-
const watch = await api.watchDoc(JobOutputDoc,
|
|
1614
|
-
|
|
2615
|
+
const watch = await api.watchDoc(JobOutputDoc, producerTaskId, context);
|
|
2616
|
+
if (watch === undefined) return;
|
|
1615
2617
|
try {
|
|
1616
|
-
await initializeConsumer(value, context);
|
|
1617
|
-
watch.start(async (ops, deliveryContext) => {
|
|
1618
|
-
|
|
1619
|
-
await consume(value, deliveryContext);
|
|
2618
|
+
await initializeConsumer(watch.value, context);
|
|
2619
|
+
watch.start(async (value, ops, deliveryContext) => {
|
|
2620
|
+
await consume(value, ops, deliveryContext);
|
|
1620
2621
|
});
|
|
1621
2622
|
} catch (error) {
|
|
1622
|
-
watch.stop();
|
|
1623
|
-
await watch.closed;
|
|
2623
|
+
await watch.stop();
|
|
1624
2624
|
throw error;
|
|
1625
2625
|
}
|
|
1626
2626
|
```
|
|
1627
2627
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
`
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
`applyImmutable(previousPublishedValue, ops)`; it never retains mutable
|
|
1663
|
-
`Tracker.state`, `Tracker.target`, or a borrowed storage candidate. Watches may
|
|
1664
|
-
share the immutable publication value and operation payloads. A mutable consumer
|
|
1665
|
-
must detach them before using mutable `apply()` or `track()`.
|
|
1666
|
-
|
|
1667
|
-
A watch remains bound to its original incarnation. Retirement replaces the
|
|
1668
|
-
pending suffix with `[["r", null]]`. Before start, `value` remains the original
|
|
1669
|
-
snapshot and the terminal reset becomes the first delivered batch. While active,
|
|
1670
|
-
the reset follows any in-flight callback. Successful terminal delivery closes as
|
|
1671
|
-
`retired`; recreation requires another watch.
|
|
1672
|
-
|
|
1673
|
-
`stop()` is idempotent, unregisters the handle, discards pending batches,
|
|
1674
|
-
prevents another callback from starting, and signals the watch delivery context.
|
|
1675
|
-
An in-flight callback is allowed to settle; `closed` resolves only afterward.
|
|
1676
|
-
The acquisition `Context` governs the watch lifetime. Cancellation during
|
|
1677
|
-
acquisition cleans up any registration before rejecting; an already-admitted
|
|
1678
|
-
get-or-create commit still settles and is not undone. Cancellation immediately
|
|
1679
|
-
after successful acquisition may therefore return an already-stopped handle,
|
|
1680
|
-
whose `start()` throws and whose `closed` reports `cancelled`. Invocation
|
|
1681
|
-
termination and Session close stop owned watches similarly. Listener rejection
|
|
1682
|
-
is reported, discards pending work, and closes only that watch as
|
|
1683
|
-
`listener_error`. The first termination reason wins, and every late rejection is
|
|
1684
|
-
observed.
|
|
2628
|
+
`start()` installs the sole listener and schedules delivery; it never invokes
|
|
2629
|
+
user code inline. Each callback receives the exact immutable committed value and
|
|
2630
|
+
the exact operation batch that produced it. Immediately before invocation,
|
|
2631
|
+
`watch.value` advances to that value. The watch awaits the listener before
|
|
2632
|
+
starting its next callback, but Session commits never wait for callback
|
|
2633
|
+
settlement.
|
|
2634
|
+
|
|
2635
|
+
A watch retains at most 100 pending committed frames, excluding the frame already
|
|
2636
|
+
being delivered. Adding frame 101 replaces the complete undelivered suffix with
|
|
2637
|
+
one self-contained root replacement `[["r", newestValue]]`, using the newest
|
|
2638
|
+
exact immutable revision and that commit's Context. Later exact frames follow
|
|
2639
|
+
that replacement normally. Another overflow replaces the pending suffix again.
|
|
2640
|
+
No serialized-byte measurement, value copy, operation replay, or re-diff occurs.
|
|
2641
|
+
This is convergent observation, not an audit stream; consumers requiring every
|
|
2642
|
+
transition must persist those facts separately.
|
|
2643
|
+
|
|
2644
|
+
`watch.value` and every previously returned revision remain stable forever under
|
|
2645
|
+
the trusted immutability contract. Consumers must not mutate them or retained
|
|
2646
|
+
descendants. The selected commit Context's values are preserved without inheriting the
|
|
2647
|
+
producer's cancellation. A callback owns its own asynchronous work.
|
|
2648
|
+
|
|
2649
|
+
A watch remains bound to its original incarnation. Retirement queues the exact
|
|
2650
|
+
terminal `[["r", null]]` frame, or folds it into an overflow replacement with
|
|
2651
|
+
value `null`. After that callback settles, the watch closes as `retired` and never
|
|
2652
|
+
follows a replacement incarnation.
|
|
2653
|
+
|
|
2654
|
+
`stop()` is idempotent. It synchronously unregisters, discards pending frames,
|
|
2655
|
+
prevents another callback from starting, and returns the common terminal promise.
|
|
2656
|
+
An already-running callback is not aborted or joined and remains caller-owned.
|
|
2657
|
+
The acquisition `Context` governs watch lifetime. Cancellation during acquisition
|
|
2658
|
+
cleans up before rejecting; later cancellation and Session close stop future
|
|
2659
|
+
delivery similarly. Listener failure discards pending work and closes only that
|
|
2660
|
+
watch as `listener_error`. The first termination reason wins, and every late
|
|
2661
|
+
rejection is observed.
|
|
1685
2662
|
|
|
1686
2663
|
### 9.3 Conversation view
|
|
1687
2664
|
|
|
@@ -1702,54 +2679,57 @@ through their own Chord services, not automatically mounted.
|
|
|
1702
2679
|
The mount consumes one complete Session commit and publishes one Chord batch:
|
|
1703
2680
|
|
|
1704
2681
|
```text
|
|
1705
|
-
document op ["s", ["message"], value]
|
|
1706
|
-
-> view op ["s", ["docs", "pi.live", "message"], value]
|
|
2682
|
+
document op ["s", ["generation", "message"], value]
|
|
2683
|
+
-> view op ["s", ["docs", "pi.live", "generation", "message"], value]
|
|
1707
2684
|
```
|
|
1708
2685
|
|
|
1709
2686
|
Entry appends/head changes and every changed mounted document are included in
|
|
1710
|
-
the same publication.
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2687
|
+
the same publication. Before Storage admission, the Session derives the mounted
|
|
2688
|
+
operation batch and prepares each affected loaded mount's next immutable revision
|
|
2689
|
+
with the optimized immutable applier. Failure rolls back normally. After Storage
|
|
2690
|
+
success, finalization only installs the prepared mount pointers/cursors and
|
|
2691
|
+
enqueues publication. Mounted document revisions may be structurally shared
|
|
2692
|
+
because they obey the same trusted immutability contract. The mount performs no
|
|
2693
|
+
semantic projection and owns no second persistence authority. A Chord adapter
|
|
2694
|
+
assigns a contiguous in-memory delivery sequence per view source lifetime.
|
|
2695
|
+
|
|
2696
|
+
`Conversation.viewState()` exposes the mount directly as a disposable read-only
|
|
2697
|
+
Chord state for facets and UI services. `Conversation.watch()` exposes the same
|
|
2698
|
+
mount through Package 12's serialized exact-frame watch with bounded pending
|
|
2699
|
+
frames and full-value overflow replacements. An empty mounted operation batch
|
|
2700
|
+
creates no revision; a redundant nonempty batch remains a real publication.
|
|
2701
|
+
Neither API adds another tracker, persistence authority, or semantic event
|
|
2702
|
+
envelope.
|
|
1723
2703
|
|
|
1724
2704
|
### 9.4 Agent-mode notifications
|
|
1725
2705
|
|
|
1726
2706
|
The Session kernel and Chord structural sources do not maintain a semantic event
|
|
1727
2707
|
journal. Coding-agent JSON/RPC compatibility uses a thin agent-mode adapter
|
|
1728
|
-
derived from each uncoalesced committed publication before per-watch
|
|
1729
|
-
|
|
2708
|
+
derived from each uncoalesced committed publication before any per-watch
|
|
2709
|
+
overflow replacement. It owns no tracker or persistence and emits notifications only after
|
|
1730
2710
|
the commit that makes them true.
|
|
1731
2711
|
|
|
1732
2712
|
The adapter protocol covers run start/settlement, committed assistant progress,
|
|
1733
|
-
message entry settlement, tool intent/progress/result,
|
|
2713
|
+
message entry settlement, tool intent/progress/result, submission queue/outcome,
|
|
1734
2714
|
retry/deferred/compaction state, configuration changes, and faults. One commit
|
|
1735
2715
|
may produce an ordered batch. Progress notifications represent Pico's throttled
|
|
1736
2716
|
durable partials, not every raw provider frame. The exact legacy `AgentEvent`
|
|
1737
2717
|
wire format is not preserved.
|
|
1738
2718
|
|
|
1739
2719
|
Notifications have no hydration or replay contract. A consumer requiring a
|
|
1740
|
-
complete lifecycle subscribes before admitting the
|
|
2720
|
+
complete lifecycle subscribes before admitting the submission; a late or reconnecting
|
|
1741
2721
|
consumer hydrates structural state and history instead. Product adapters apply
|
|
1742
2722
|
these rules:
|
|
1743
2723
|
|
|
1744
2724
|
- TUI hydrates and renders `ConversationView`, then applies structural updates;
|
|
1745
2725
|
notifications may drive transient animation but are not its authority.
|
|
1746
|
-
- Print awaits its
|
|
2726
|
+
- Print awaits its input `Submission` and prints that submission's answer.
|
|
1747
2727
|
- JSON/RPC expose correlated commands plus the ordered agent notification
|
|
1748
2728
|
protocol, with transport backpressure and disconnect policy owned by that
|
|
1749
2729
|
adapter.
|
|
1750
2730
|
|
|
1751
2731
|
This adapter is allowed even though a public Session-kernel semantic stream is a
|
|
1752
|
-
non-goal. It must not derive notifications from a lossy, reset
|
|
2732
|
+
non-goal. It must not derive notifications from a lossy, overflow-reset watch
|
|
1753
2733
|
when complete subscribed lifecycle delivery is promised.
|
|
1754
2734
|
|
|
1755
2735
|
## 10. Storage contract
|
|
@@ -1764,20 +2744,30 @@ type Page<T, C> = {
|
|
|
1764
2744
|
|
|
1765
2745
|
type Cursor = Readonly<Record<string, JsonValue>>;
|
|
1766
2746
|
|
|
2747
|
+
type ConversationQuery = {
|
|
2748
|
+
readonly ownerConversationId?: ConversationId;
|
|
2749
|
+
readonly ownerTaskId?: TaskId;
|
|
2750
|
+
};
|
|
2751
|
+
|
|
1767
2752
|
type EntryQuery = {
|
|
1768
|
-
readonly conversationId:
|
|
1769
|
-
readonly minEntryId?:
|
|
1770
|
-
readonly maxEntryId?:
|
|
2753
|
+
readonly conversationId: ConversationId;
|
|
2754
|
+
readonly minEntryId?: EntryId; // inclusive
|
|
2755
|
+
readonly maxEntryId?: EntryId; // inclusive
|
|
1771
2756
|
};
|
|
1772
2757
|
|
|
1773
2758
|
type TaskQuery = {
|
|
1774
|
-
readonly conversationId?:
|
|
2759
|
+
readonly conversationId?: ConversationId;
|
|
1775
2760
|
readonly kind?: string;
|
|
1776
2761
|
readonly status?: "pending" | "running" | "terminal";
|
|
1777
2762
|
readonly abortRequested?: boolean;
|
|
1778
2763
|
readonly background?: boolean;
|
|
1779
2764
|
};
|
|
1780
2765
|
|
|
2766
|
+
type SubmissionQuery = {
|
|
2767
|
+
readonly conversationId?: ConversationId;
|
|
2768
|
+
readonly status?: SubmissionRecord["status"];
|
|
2769
|
+
};
|
|
2770
|
+
|
|
1781
2771
|
type DocumentPoint = Seq | "current";
|
|
1782
2772
|
|
|
1783
2773
|
type DocumentAddress = {
|
|
@@ -1796,66 +2786,84 @@ type DocumentContent =
|
|
|
1796
2786
|
| { readonly version: number; readonly kind: "base"; readonly value: JsonObject }
|
|
1797
2787
|
| { readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
|
|
1798
2788
|
|
|
1799
|
-
type StoredDocumentRecord =
|
|
1800
|
-
| { readonly seq: Seq; readonly version: number; readonly kind: "base"; readonly value: JsonObject }
|
|
1801
|
-
| { readonly seq: Seq; readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
|
|
1802
|
-
|
|
1803
2789
|
type StoredDocument = {
|
|
1804
2790
|
readonly record: DocumentRecord;
|
|
1805
|
-
readonly
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2791
|
+
readonly version: number;
|
|
2792
|
+
readonly value: JsonObject;
|
|
2793
|
+
/** Deltas replayed after the selected base to materialize `value`. */
|
|
2794
|
+
readonly deltasSinceBase: number;
|
|
1809
2795
|
};
|
|
1810
2796
|
|
|
1811
2797
|
type StorageWrite =
|
|
1812
2798
|
| { readonly type: "conversation"; readonly value: ConversationRecord }
|
|
1813
2799
|
| { readonly type: "entry"; readonly value: EntryRecord }
|
|
1814
2800
|
| { readonly type: "task"; readonly value: TaskRecord<JsonValue, JsonValue, JsonValue> }
|
|
1815
|
-
| { readonly type: "
|
|
2801
|
+
| { readonly type: "submission"; readonly value: SubmissionRecord }
|
|
1816
2802
|
| {
|
|
1817
2803
|
readonly type: "document.create";
|
|
1818
2804
|
readonly record: DocumentCreate;
|
|
1819
2805
|
readonly content: Extract<DocumentContent, { kind: "base" }>;
|
|
1820
2806
|
}
|
|
2807
|
+
| {
|
|
2808
|
+
readonly type: "document.copy";
|
|
2809
|
+
readonly record: DocumentCreate;
|
|
2810
|
+
readonly source: { readonly id: DocumentId; readonly at: DocumentPoint };
|
|
2811
|
+
}
|
|
1821
2812
|
| {
|
|
1822
2813
|
readonly type: "document.change";
|
|
1823
|
-
readonly id:
|
|
2814
|
+
readonly id: DocumentId;
|
|
1824
2815
|
readonly content: DocumentContent;
|
|
1825
2816
|
}
|
|
1826
|
-
| { readonly type: "document.retire"; readonly id:
|
|
2817
|
+
| { readonly type: "document.retire"; readonly id: DocumentId };
|
|
1827
2818
|
|
|
1828
2819
|
/**
|
|
1829
2820
|
* Trusts the owning Session to supply semantically valid records, references,
|
|
1830
2821
|
* ancestry, and transitions. Enforces atomicity, global ID ownership, immutable
|
|
1831
|
-
* conversation/entry creation, document
|
|
1832
|
-
* values; Session serializes commits.
|
|
2822
|
+
* conversation/entry creation, document record consistency, and detached
|
|
2823
|
+
* values; Session serializes commits. Sequences strictly increase but may have
|
|
2824
|
+
* gaps. Once commit() resolves, later reads through that Storage observe it.
|
|
1833
2825
|
*/
|
|
1834
2826
|
interface Storage {
|
|
1835
2827
|
commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
|
|
1836
|
-
|
|
2828
|
+
/** Allocate from the one global numeric namespace; the generic brand is compile-time only. */
|
|
2829
|
+
mintId<I extends Id<string>>(): Promise<I>;
|
|
1837
2830
|
|
|
1838
|
-
conversation(id:
|
|
1839
|
-
scanConversations(cursor: Cursor | undefined,
|
|
2831
|
+
conversation(id: ConversationId, context: Context): Promise<ConversationRecord | undefined>;
|
|
2832
|
+
scanConversations(query: ConversationQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<ConversationRecord, Cursor>>;
|
|
1840
2833
|
|
|
1841
|
-
entry(id:
|
|
1842
|
-
|
|
1843
|
-
|
|
2834
|
+
entry(id: EntryId, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
|
|
2835
|
+
entry(conversationId: ConversationId, id: EntryId, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
|
|
2836
|
+
findLatestHeadMarker(conversationId: ConversationId, atOrBeforeEntryId: EntryId | undefined, context: Context): Promise<(EntryRecord & { readonly head: EntryId }) | undefined>;
|
|
2837
|
+
scanEntries(query: EntryQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<EntryRecord, Cursor>>;
|
|
1844
2838
|
|
|
1845
|
-
task(id:
|
|
1846
|
-
scanTasks(query: TaskQuery, cursor: Cursor | undefined,
|
|
2839
|
+
task(id: TaskId, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
|
|
2840
|
+
scanTasks(query: TaskQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
|
|
1847
2841
|
|
|
1848
|
-
|
|
1849
|
-
|
|
2842
|
+
submission(id: SubmissionId, context: Context): Promise<SubmissionRecord | undefined>;
|
|
2843
|
+
scanSubmissions(query: SubmissionQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<SubmissionRecord, Cursor>>;
|
|
2844
|
+
submissionByRequest(conversationId: ConversationId, requestId: string, context: Context): Promise<SubmissionRecord | undefined>;
|
|
1850
2845
|
|
|
1851
2846
|
findDocument(address: DocumentAddress, at: DocumentPoint, context: Context): Promise<DocumentRecord | undefined>;
|
|
1852
|
-
document(id:
|
|
1853
|
-
scanDocuments(query: DocumentQuery, cursor: Cursor | undefined,
|
|
2847
|
+
document(id: DocumentId, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined>;
|
|
2848
|
+
scanDocuments(query: DocumentQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<DocumentRecord, Cursor>>;
|
|
1854
2849
|
|
|
1855
2850
|
close(context: Context): Promise<void>;
|
|
1856
2851
|
}
|
|
1857
2852
|
```
|
|
1858
2853
|
|
|
2854
|
+
`StorageRejected` means a batch was rejected before any durable effect and is
|
|
2855
|
+
guaranteed not to have committed. Session rolls such a batch back normally;
|
|
2856
|
+
unknown failures after Storage admission remain fatal because their commit state
|
|
2857
|
+
is uncertain. Backends use `StorageRejected` for deterministic `document.copy`
|
|
2858
|
+
source, replay, and consistency failures only when rollback is guaranteed.
|
|
2859
|
+
|
|
2860
|
+
A `document.copy` reads committed pre-batch source state independent of command
|
|
2861
|
+
order. The source must be an alive conversation document at the selected point,
|
|
2862
|
+
and kind/key/history/fork must match the child create record. Storage persists
|
|
2863
|
+
one independent complete child base at the source's stored version. A batch may
|
|
2864
|
+
not create, change, or retire a selected source. Later source changes,
|
|
2865
|
+
reclamation, retirement, or backend reopen cannot affect the child.
|
|
2866
|
+
|
|
1859
2867
|
Cursors are backend-owned JSON objects. Callers only round-trip them to the same
|
|
1860
2868
|
scan on the same storage; cross-storage or cross-query use is unsupported. The
|
|
1861
2869
|
Session owns the mutation line, so storage implementations do not add a second
|
|
@@ -1868,8 +2876,14 @@ inclusive ID range in newest-first order while applying every conversation
|
|
|
1868
2876
|
ancestry cap. With no bounds it pages complete visible history. To read context
|
|
1869
2877
|
through entry `E`, find the marker at or before `E`, then scan from
|
|
1870
2878
|
`marker?.head` through `E`. For current context the upper bound is omitted.
|
|
1871
|
-
|
|
1872
|
-
|
|
2879
|
+
Conversation owner filters are indexed and conjunctive. They support ownership
|
|
2880
|
+
traversal without an all-conversation scan; application-maintained registries
|
|
2881
|
+
are not a substitute for these kernel indexes.
|
|
2882
|
+
|
|
2883
|
+
`entry(id)` combines exact global lookup with the commit sequence required by
|
|
2884
|
+
historical document reads. `entry(conversationId, id)` returns that pair only
|
|
2885
|
+
when the entry is visible through the requested conversation's ancestry. `limit`
|
|
2886
|
+
is always the maximum page size.
|
|
1873
2887
|
`findDocument()` resolves one exact logical kind/scope/key address at current or
|
|
1874
2888
|
historical membership. A missing key means the singleton, not every family
|
|
1875
2889
|
member. `scanDocuments()` enumerates only the incarnations alive in one exact
|
|
@@ -1878,15 +2892,19 @@ ascending incarnation IDs. There is no ordinary open-time all-document scan.
|
|
|
1878
2892
|
Task queries support conversation, kind, live/terminal status, abort mark, and
|
|
1879
2893
|
background status.
|
|
1880
2894
|
|
|
1881
|
-
`document(id, at)`
|
|
1882
|
-
|
|
1883
|
-
`
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2895
|
+
`document(id, at)` materializes one specific incarnation and never follows a
|
|
2896
|
+
replacement at the same logical address. Callers resolve an address with
|
|
2897
|
+
`findDocument()` when they do not already hold an incarnation ID. It selects the
|
|
2898
|
+
newest applicable base, applies its ordered Chord delta tail, and returns the detached materialized value, stored
|
|
2899
|
+
definition version, and number of replayed deltas after that base. Base/delta records are backend-private. The lookup never
|
|
2900
|
+
scans unrelated documents. An unknown ID returns `undefined`. At `"current"`, a
|
|
2901
|
+
retired incarnation returns `undefined`. A numeric lookup of a rewindable
|
|
2902
|
+
conversation incarnation returns `undefined` outside its half-open lifetime and
|
|
2903
|
+
reconstructs the selected value inside it. A numeric lookup of a known current-
|
|
2904
|
+
only incarnation rejects rather than depending on reclaimed content. Metadata
|
|
2905
|
+
membership remains queryable historically. A missing required base, a version
|
|
2906
|
+
change inside a delta tail, or an operation that cannot be applied inside an
|
|
2907
|
+
addressable lifetime is storage corruption, not absence.
|
|
1890
2908
|
|
|
1891
2909
|
One normalized batch contains at most one create/change content command per
|
|
1892
2910
|
incarnation and may also retire that incarnation. Storage applies content before
|
|
@@ -1911,8 +2929,8 @@ after a committed base or retirement.
|
|
|
1911
2929
|
|
|
1912
2930
|
One SQL transaction is one Session commit. SQLite stores:
|
|
1913
2931
|
|
|
1914
|
-
- conversation, entry, task, and
|
|
1915
|
-
- document
|
|
2932
|
+
- conversation, entry, task, and submission records;
|
|
2933
|
+
- document records;
|
|
1916
2934
|
- indexed document bases/deltas by document and commit sequence.
|
|
1917
2935
|
|
|
1918
2936
|
Live task transitions replace one row. Terminal tasks remain as small records.
|
|
@@ -1926,14 +2944,15 @@ benchmarks.
|
|
|
1926
2944
|
|
|
1927
2945
|
### 11.3 JSONL
|
|
1928
2946
|
|
|
1929
|
-
JSONL
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2947
|
+
JSONL depends on the portable `FileSystem` capability, not the broader
|
|
2948
|
+
`ExecutionEnv`. It uses reclaimable sidecars without exposing them to the
|
|
2949
|
+
harness. Persistence alone does not provide the ownership boundary: any decoded
|
|
2950
|
+
indexes, materialized values, or caches retained in memory must be detached from
|
|
2951
|
+
commit arguments and must not be exposed directly by reads. A JSONL backend
|
|
2952
|
+
cannot simply add file appends around aliasing memory tables.
|
|
1934
2953
|
|
|
1935
2954
|
```text
|
|
1936
|
-
main.jsonl table writes,
|
|
2955
|
+
main.jsonl table writes, document records, and one marker per commit
|
|
1937
2956
|
doc-<id>.jsonl one document incarnation
|
|
1938
2957
|
task-<id>.jsonl live task replacements
|
|
1939
2958
|
```
|
|
@@ -1945,9 +2964,18 @@ Publication protocol:
|
|
|
1945
2964
|
3. Publish in memory only after the marker write succeeds.
|
|
1946
2965
|
|
|
1947
2966
|
Every commit uses this protocol; there is no standalone-sidecar fast path.
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2967
|
+
JSONL creation accepts an `fsync` option that defaults to `false`. Without
|
|
2968
|
+
`fsync`, it guarantees ordinary process-crash consistency, not survival of
|
|
2969
|
+
power, host, kernel, or filesystem failure. With `fsync: true`, the backend
|
|
2970
|
+
appends all affected sidecar records, flushes each affected sidecar, and only
|
|
2971
|
+
then appends the main marker. Ordinary publication does not explicitly flush
|
|
2972
|
+
`main.jsonl`; an acknowledged tail commit may therefore still disappear, but a
|
|
2973
|
+
marker that survives should not overtake its sidecar data. A main-only commit has
|
|
2974
|
+
no sidecars to flush. Before destructive reclamation with `fsync: true`, the
|
|
2975
|
+
backend flushes `main.jsonl` once so the authorizing marker cannot disappear
|
|
2976
|
+
while its replacement or removal survives. If that flush fails, the committed
|
|
2977
|
+
state remains published and reclamation is deferred. A non-empty temporary
|
|
2978
|
+
replacement is also flushed before rename.
|
|
1951
2979
|
|
|
1952
2980
|
Recovery:
|
|
1953
2981
|
|
|
@@ -1959,28 +2987,35 @@ Recovery:
|
|
|
1959
2987
|
record unnecessary.
|
|
1960
2988
|
- Any uncertain append failure poisons the open backend.
|
|
1961
2989
|
|
|
1962
|
-
Reclamation starts only after the authorizing base/retirement commits.
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2990
|
+
Reclamation starts only after the authorizing base/retirement commits. When no
|
|
2991
|
+
sidecar records remain, it removes the sidecar directly. Otherwise, it writes a
|
|
2992
|
+
temporary replacement, renames it, and invalidates cached file descriptors so
|
|
2993
|
+
future appends cannot target an unlinked inode. Flushing `main.jsonl` to
|
|
2994
|
+
authorize reclamation does not compact it. `main.jsonl` is not compacted in the
|
|
2995
|
+
initial implementation.
|
|
1966
2996
|
|
|
1967
2997
|
## 12. API footguns
|
|
1968
2998
|
|
|
1969
2999
|
These are contracts, not invitations to add defensive machinery:
|
|
1970
3000
|
|
|
1971
|
-
- **
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
3001
|
+
- **Detached draft work:** draft reads, draft writes, and all `Tx` operations
|
|
3002
|
+
reject after the Session callback settles. Fire-and-forget work that runs
|
|
3003
|
+
before callback settlement can still mutate the active transaction and is
|
|
3004
|
+
unsupported.
|
|
1975
3005
|
- **Read after write:** read every required table row before the first table
|
|
1976
3006
|
write. Document drafts remain usable afterward; table reads do not.
|
|
3007
|
+
- **Writing to a busy conversation:** only the Harness appends to a conversation
|
|
3008
|
+
with an active run. Raw entries appended by `Harness.commit()` or a custom
|
|
3009
|
+
task while a generation prepares its request can misplace its system prompt entries;
|
|
3010
|
+
use a write submission.
|
|
1977
3011
|
- **Long transactions:** an async commit callback holds the Session mutation
|
|
1978
3012
|
line. Never await models, tools, processes, network calls, humans, a nested
|
|
1979
3013
|
Session commit, or a Session waiter inside it. Use methods on the current `Tx`.
|
|
1980
|
-
- **
|
|
1981
|
-
|
|
1982
|
-
- **Family initialization:**
|
|
1983
|
-
|
|
3014
|
+
- **Explicit creation:** only typed `tx.doc()` creates an absent document. Snapshot,
|
|
3015
|
+
state, and watch lookup return `undefined` instead.
|
|
3016
|
+
- **Family initialization:** the first acquisition of an absent family address
|
|
3017
|
+
selects its seed. Existing instances and later calls ignore seeds; a seed is
|
|
3018
|
+
neither identity nor an update.
|
|
1984
3019
|
- **Checkpoint starvation:** if `checkpointWhen()` never returns true, replay
|
|
1985
3020
|
and current-only document storage can grow without bound while the document is
|
|
1986
3021
|
live.
|
|
@@ -1991,15 +3026,20 @@ These are contracts, not invitations to add defensive machinery:
|
|
|
1991
3026
|
change requires explicit copy and retirement. Passing incompatible definition
|
|
1992
3027
|
tokens that claim the same kind is unsupported caller misuse; Session does not
|
|
1993
3028
|
maintain a document-definition registry to detect it.
|
|
1994
|
-
- **
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
- **
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
- **
|
|
2002
|
-
|
|
3029
|
+
- **Trusted immutable revisions:** `snapshot()`, state values, watch values, and
|
|
3030
|
+
their descendants may share tracker-owned containers. Never mutate them; copy
|
|
3031
|
+
first when mutable ownership is required. Runtime freezing is not provided.
|
|
3032
|
+
- **Watch activation:** before `start()`, `watch.value` remains the immutable
|
|
3033
|
+
acquisition revision. Initialize the consumer from it first. After start, the
|
|
3034
|
+
property advances to each delivered immutable revision before its callback;
|
|
3035
|
+
every earlier reference remains stable.
|
|
3036
|
+
- **Buffered watches:** slow or unstarted watches retain up to 100 exact committed
|
|
3037
|
+
frames. Overflow replaces the undelivered suffix with one full-value root
|
|
3038
|
+
replacement, so intermediate committed states may be omitted. A consumer that
|
|
3039
|
+
must audit every transition must persist each fact in an immutable entry or
|
|
3040
|
+
journal and scan that history explicitly.
|
|
3041
|
+
- **Watch stop:** a listener may call and await `stop()`; it prevents future
|
|
3042
|
+
callbacks but neither aborts nor joins the callback already running.
|
|
2003
3043
|
- **Durable progress cadence:** clients see only committed progress. A crash may
|
|
2004
3044
|
lose the current uncommitted throttle window.
|
|
2005
3045
|
- **Large terminal results:** terminal task records remain queryable. Put large
|
|
@@ -2007,6 +3047,20 @@ These are contracts, not invitations to add defensive machinery:
|
|
|
2007
3047
|
outcome. Never reference a task-scoped document retired by that same outcome.
|
|
2008
3048
|
- **Raw transcript:** view entries are not model context. Rendering edits,
|
|
2009
3049
|
display-only entries, and model filtering require the appropriate reducer.
|
|
3050
|
+
- **Reserved `pi.` names:** task names, document kinds, and entry kinds starting
|
|
3051
|
+
with `pi.` belong to built-ins by convention. Nothing enforces it; reusing one
|
|
3052
|
+
collides with Harness behavior, such as `pi.system` entries being replayed as
|
|
3053
|
+
system messages.
|
|
3054
|
+
- **Early resource disposal:** disposing a `Registration` only stops new use.
|
|
3055
|
+
Freeing resources immediately can fail calls that are still running.
|
|
3056
|
+
- **Async batches:** `batch()` callbacks are synchronous. A callback that returns
|
|
3057
|
+
a promise publishes nothing and throws.
|
|
3058
|
+
- **Unstable prompt text:** a section renderer whose output changes without a
|
|
3059
|
+
real content change, for example by embedding the time, appends system deltas
|
|
3060
|
+
and defeats provider prompt caching.
|
|
3061
|
+
- **Durable stream options:** `streamOptions.headers` and `metadata` are stored
|
|
3062
|
+
in the conversation configuration history and copied into forks. Never put
|
|
3063
|
+
credentials there; `Models` resolves auth.
|
|
2010
3064
|
- **Fatal storage errors:** after an uncertain storage failure the Session is
|
|
2011
3065
|
poisoned. Do not catch the error and continue using it.
|
|
2012
3066
|
- **JSONL durability:** default JSONL ordering handles ordinary process crashes;
|
|
@@ -2020,7 +3074,6 @@ Pico5 initially has no:
|
|
|
2020
3074
|
- whole-Session DOM;
|
|
2021
3075
|
- visible-undurable publication;
|
|
2022
3076
|
- Session-kernel semantic event journal or independently maintained event state;
|
|
2023
|
-
- transaction membrane;
|
|
2024
3077
|
- session-scoped rewindable documents;
|
|
2025
3078
|
- automatic checkpoint heuristic;
|
|
2026
3079
|
- automatic third-party view mounting;
|
|
@@ -2028,4 +3081,4 @@ Pico5 initially has no:
|
|
|
2028
3081
|
- SQL translation of arbitrary Chord operations;
|
|
2029
3082
|
- JSONL global compaction or automatic corruption repair;
|
|
2030
3083
|
- compatibility layer for removed Pico prototypes;
|
|
2031
|
-
-
|
|
3084
|
+
- forced termination of non-cooperative extension code inside one process.
|