@earendil-works/pi-agent-core 0.85.0 → 0.86.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.
Files changed (198) hide show
  1. package/README.md +16 -6
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +64 -22
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/agent.d.ts +5 -3
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +15 -6
  8. package/dist/agent.js.map +1 -1
  9. package/dist/harness/config.d.ts +1 -1
  10. package/dist/harness/config.d.ts.map +1 -1
  11. package/dist/harness/config.js +10 -2
  12. package/dist/harness/config.js.map +1 -1
  13. package/dist/harness/env/nodejs.d.ts +2 -1
  14. package/dist/harness/env/nodejs.d.ts.map +1 -1
  15. package/dist/harness/env/nodejs.js +98 -27
  16. package/dist/harness/env/nodejs.js.map +1 -1
  17. package/dist/harness/execution/tools.d.ts +1 -1
  18. package/dist/harness/execution/tools.d.ts.map +1 -1
  19. package/dist/harness/execution/tools.js +0 -2
  20. package/dist/harness/execution/tools.js.map +1 -1
  21. package/dist/harness/messages.d.ts.map +1 -1
  22. package/dist/harness/messages.js +1 -0
  23. package/dist/harness/messages.js.map +1 -1
  24. package/dist/harness/pico3/bash.d.ts +10 -0
  25. package/dist/harness/pico3/bash.d.ts.map +1 -0
  26. package/dist/harness/pico3/bash.js +25 -0
  27. package/dist/harness/pico3/bash.js.map +1 -0
  28. package/dist/harness/pico3/bounded.d.ts +21 -0
  29. package/dist/harness/pico3/bounded.d.ts.map +1 -0
  30. package/dist/harness/pico3/bounded.js +98 -0
  31. package/dist/harness/pico3/bounded.js.map +1 -0
  32. package/dist/harness/pico3/chord.d.ts +41 -0
  33. package/dist/harness/pico3/chord.d.ts.map +1 -0
  34. package/dist/harness/pico3/chord.js +130 -0
  35. package/dist/harness/pico3/chord.js.map +1 -0
  36. package/dist/harness/pico3/context.d.ts +16 -0
  37. package/dist/harness/pico3/context.d.ts.map +1 -0
  38. package/dist/harness/pico3/context.js +88 -0
  39. package/dist/harness/pico3/context.js.map +1 -0
  40. package/dist/harness/pico3/harness.d.ts +197 -0
  41. package/dist/harness/pico3/harness.d.ts.map +1 -0
  42. package/dist/harness/pico3/harness.js +654 -0
  43. package/dist/harness/pico3/harness.js.map +1 -0
  44. package/dist/harness/pico3/hooks.d.ts +10 -0
  45. package/dist/harness/pico3/hooks.d.ts.map +1 -0
  46. package/dist/harness/pico3/hooks.js +33 -0
  47. package/dist/harness/pico3/hooks.js.map +1 -0
  48. package/dist/harness/pico3/index.d.ts +19 -0
  49. package/dist/harness/pico3/index.d.ts.map +1 -0
  50. package/dist/harness/pico3/index.js +15 -0
  51. package/dist/harness/pico3/index.js.map +1 -0
  52. package/dist/harness/pico3/jsonl.d.ts +58 -0
  53. package/dist/harness/pico3/jsonl.d.ts.map +1 -0
  54. package/dist/harness/pico3/jsonl.js +325 -0
  55. package/dist/harness/pico3/jsonl.js.map +1 -0
  56. package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
  57. package/dist/harness/pico3/kinds/collapse.d.ts.map +1 -0
  58. package/dist/harness/pico3/kinds/collapse.js +191 -0
  59. package/dist/harness/pico3/kinds/collapse.js.map +1 -0
  60. package/dist/harness/pico3/kinds/entries.d.ts +78 -0
  61. package/dist/harness/pico3/kinds/entries.d.ts.map +1 -0
  62. package/dist/harness/pico3/kinds/entries.js +13 -0
  63. package/dist/harness/pico3/kinds/entries.js.map +1 -0
  64. package/dist/harness/pico3/kinds/frames.d.ts +9 -0
  65. package/dist/harness/pico3/kinds/frames.d.ts.map +1 -0
  66. package/dist/harness/pico3/kinds/frames.js +75 -0
  67. package/dist/harness/pico3/kinds/frames.js.map +1 -0
  68. package/dist/harness/pico3/kinds/generation.d.ts +94 -0
  69. package/dist/harness/pico3/kinds/generation.d.ts.map +1 -0
  70. package/dist/harness/pico3/kinds/generation.js +504 -0
  71. package/dist/harness/pico3/kinds/generation.js.map +1 -0
  72. package/dist/harness/pico3/kinds/job.d.ts +44 -0
  73. package/dist/harness/pico3/kinds/job.d.ts.map +1 -0
  74. package/dist/harness/pico3/kinds/job.js +143 -0
  75. package/dist/harness/pico3/kinds/job.js.map +1 -0
  76. package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
  77. package/dist/harness/pico3/kinds/plugin.d.ts.map +1 -0
  78. package/dist/harness/pico3/kinds/plugin.js +35 -0
  79. package/dist/harness/pico3/kinds/plugin.js.map +1 -0
  80. package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
  81. package/dist/harness/pico3/kinds/post-tools.d.ts.map +1 -0
  82. package/dist/harness/pico3/kinds/post-tools.js +145 -0
  83. package/dist/harness/pico3/kinds/post-tools.js.map +1 -0
  84. package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
  85. package/dist/harness/pico3/kinds/task-api.d.ts.map +1 -0
  86. package/dist/harness/pico3/kinds/task-api.js +45 -0
  87. package/dist/harness/pico3/kinds/task-api.js.map +1 -0
  88. package/dist/harness/pico3/kinds/tool.d.ts +43 -0
  89. package/dist/harness/pico3/kinds/tool.d.ts.map +1 -0
  90. package/dist/harness/pico3/kinds/tool.js +373 -0
  91. package/dist/harness/pico3/kinds/tool.js.map +1 -0
  92. package/dist/harness/pico3/membrane.d.ts +23 -0
  93. package/dist/harness/pico3/membrane.d.ts.map +1 -0
  94. package/dist/harness/pico3/membrane.js +141 -0
  95. package/dist/harness/pico3/membrane.js.map +1 -0
  96. package/dist/harness/pico3/memory.d.ts +53 -0
  97. package/dist/harness/pico3/memory.d.ts.map +1 -0
  98. package/dist/harness/pico3/memory.js +264 -0
  99. package/dist/harness/pico3/memory.js.map +1 -0
  100. package/dist/harness/pico3/scheduler.d.ts +46 -0
  101. package/dist/harness/pico3/scheduler.d.ts.map +1 -0
  102. package/dist/harness/pico3/scheduler.js +438 -0
  103. package/dist/harness/pico3/scheduler.js.map +1 -0
  104. package/dist/harness/pico3/session.d.ts +263 -0
  105. package/dist/harness/pico3/session.d.ts.map +1 -0
  106. package/dist/harness/pico3/session.js +1347 -0
  107. package/dist/harness/pico3/session.js.map +1 -0
  108. package/dist/harness/pico3/system.d.ts +126 -0
  109. package/dist/harness/pico3/system.d.ts.map +1 -0
  110. package/dist/harness/pico3/system.js +244 -0
  111. package/dist/harness/pico3/system.js.map +1 -0
  112. package/dist/harness/pico3/types.d.ts +973 -0
  113. package/dist/harness/pico3/types.d.ts.map +1 -0
  114. package/dist/harness/pico3/types.js +96 -0
  115. package/dist/harness/pico3/types.js.map +1 -0
  116. package/dist/harness/pico3/view.d.ts +34 -0
  117. package/dist/harness/pico3/view.d.ts.map +1 -0
  118. package/dist/harness/pico3/view.js +417 -0
  119. package/dist/harness/pico3/view.js.map +1 -0
  120. package/dist/harness/runtime/drive/boundary.d.ts.map +1 -1
  121. package/dist/harness/runtime/drive/boundary.js +6 -3
  122. package/dist/harness/runtime/drive/boundary.js.map +1 -1
  123. package/dist/harness/runtime/drive/response.d.ts.map +1 -1
  124. package/dist/harness/runtime/drive/response.js +4 -4
  125. package/dist/harness/runtime/drive/response.js.map +1 -1
  126. package/dist/harness/runtime/drive/retry.d.ts +2 -2
  127. package/dist/harness/runtime/drive/retry.d.ts.map +1 -1
  128. package/dist/harness/runtime/drive/retry.js +3 -6
  129. package/dist/harness/runtime/drive/retry.js.map +1 -1
  130. package/dist/harness/runtime/drive/structural.d.ts.map +1 -1
  131. package/dist/harness/runtime/drive/structural.js +5 -5
  132. package/dist/harness/runtime/drive/structural.js.map +1 -1
  133. package/dist/harness/runtime/drive/tool-placement.d.ts.map +1 -1
  134. package/dist/harness/runtime/drive/tool-placement.js +2 -27
  135. package/dist/harness/runtime/drive/tool-placement.js.map +1 -1
  136. package/dist/harness/runtime/drive/tools.d.ts.map +1 -1
  137. package/dist/harness/runtime/drive/tools.js.map +1 -1
  138. package/dist/harness/session/fork-policy.d.ts +20 -12
  139. package/dist/harness/session/fork-policy.d.ts.map +1 -1
  140. package/dist/harness/session/fork-policy.js +45 -12
  141. package/dist/harness/session/fork-policy.js.map +1 -1
  142. package/dist/harness/session/fork.d.ts +0 -2
  143. package/dist/harness/session/fork.d.ts.map +1 -1
  144. package/dist/harness/session/fork.js +20 -68
  145. package/dist/harness/session/fork.js.map +1 -1
  146. package/dist/harness/session/in-memory-storage-state.d.ts +5 -5
  147. package/dist/harness/session/in-memory-storage-state.d.ts.map +1 -1
  148. package/dist/harness/session/in-memory-storage-state.js +99 -39
  149. package/dist/harness/session/in-memory-storage-state.js.map +1 -1
  150. package/dist/harness/session/index.d.ts +1 -1
  151. package/dist/harness/session/index.d.ts.map +1 -1
  152. package/dist/harness/session/index.js +1 -1
  153. package/dist/harness/session/index.js.map +1 -1
  154. package/dist/harness/session/jsonl/fork.d.ts +38 -0
  155. package/dist/harness/session/jsonl/fork.d.ts.map +1 -0
  156. package/dist/harness/session/jsonl/fork.js +264 -0
  157. package/dist/harness/session/jsonl/fork.js.map +1 -0
  158. package/dist/harness/session/jsonl/io.d.ts +14 -0
  159. package/dist/harness/session/jsonl/io.d.ts.map +1 -0
  160. package/dist/harness/session/jsonl/io.js +86 -0
  161. package/dist/harness/session/jsonl/io.js.map +1 -0
  162. package/dist/harness/session/jsonl/legacy-v3.d.ts +33 -8
  163. package/dist/harness/session/jsonl/legacy-v3.d.ts.map +1 -1
  164. package/dist/harness/session/jsonl/legacy-v3.js +285 -206
  165. package/dist/harness/session/jsonl/legacy-v3.js.map +1 -1
  166. package/dist/harness/session/jsonl/repo.d.ts +2 -1
  167. package/dist/harness/session/jsonl/repo.d.ts.map +1 -1
  168. package/dist/harness/session/jsonl/repo.js +36 -20
  169. package/dist/harness/session/jsonl/repo.js.map +1 -1
  170. package/dist/harness/session/jsonl/storage.d.ts +4 -5
  171. package/dist/harness/session/jsonl/storage.d.ts.map +1 -1
  172. package/dist/harness/session/jsonl/storage.js +39 -113
  173. package/dist/harness/session/jsonl/storage.js.map +1 -1
  174. package/dist/harness/session/memory.d.ts +3 -5
  175. package/dist/harness/session/memory.d.ts.map +1 -1
  176. package/dist/harness/session/memory.js +9 -14
  177. package/dist/harness/session/memory.js.map +1 -1
  178. package/dist/harness/session/testing/conformance/session-repo.d.ts +2 -0
  179. package/dist/harness/session/testing/conformance/session-repo.d.ts.map +1 -1
  180. package/dist/harness/session/testing/conformance/session-repo.js +220 -1
  181. package/dist/harness/session/testing/conformance/session-repo.js.map +1 -1
  182. package/dist/harness/session/testing/index.d.ts +1 -1
  183. package/dist/harness/session/testing/index.d.ts.map +1 -1
  184. package/dist/harness/session/testing/index.js +1 -1
  185. package/dist/harness/session/testing/index.js.map +1 -1
  186. package/dist/harness/session/types.d.ts +1 -0
  187. package/dist/harness/session/types.d.ts.map +1 -1
  188. package/dist/harness/session/types.js.map +1 -1
  189. package/dist/harness/types.d.ts +14 -0
  190. package/dist/harness/types.d.ts.map +1 -1
  191. package/dist/harness/types.js.map +1 -1
  192. package/dist/proxy.d.ts +2 -2
  193. package/dist/proxy.d.ts.map +1 -1
  194. package/dist/proxy.js.map +1 -1
  195. package/dist/types.d.ts +30 -14
  196. package/dist/types.d.ts.map +1 -1
  197. package/dist/types.js.map +1 -1
  198. package/package.json +10 -6
@@ -0,0 +1,654 @@
1
+ import { withAbortSignal } from "@earendil-works/chord/context";
2
+ import { createHookRunners } from "./hooks.js";
3
+ import { chooseThrough, collapse } from "./kinds/collapse.js";
4
+ import { entries as builtinEntries } from "./kinds/entries.js";
5
+ import { generation } from "./kinds/generation.js";
6
+ import { job } from "./kinds/job.js";
7
+ import { plugin } from "./kinds/plugin.js";
8
+ import { postTools } from "./kinds/post-tools.js";
9
+ import { tool } from "./kinds/tool.js";
10
+ import { Scheduler } from "./scheduler.js";
11
+ import { isCoreKind, Session } from "./session.js";
12
+ import { systemSections } from "./system.js";
13
+ import { Forbidden, } from "./types.js";
14
+ import { ViewManager } from "./view.js";
15
+ const KERNEL = { type: "kernel" };
16
+ export class Harness {
17
+ session;
18
+ scheduler;
19
+ views;
20
+ kinds;
21
+ namespaces = new Map();
22
+ tools = new Map();
23
+ sectionMap = new Map();
24
+ entryKinds = new Map();
25
+ revisions = { sections: 0, tools: 0 };
26
+ plugins;
27
+ hookRegistrations = [];
28
+ conversationListeners = new Set();
29
+ onReport;
30
+ now;
31
+ ctx;
32
+ options;
33
+ resumed = false;
34
+ suspended = false;
35
+ static async open(storage, options, ctx) {
36
+ const h = new Harness(storage, options, ctx);
37
+ await h.init();
38
+ return h;
39
+ }
40
+ constructor(storage, options, ctx) {
41
+ this.ctx = ctx;
42
+ this.options = options;
43
+ this.now = options.now ?? Date.now;
44
+ this.onReport = (error) => {
45
+ try {
46
+ options.onReport?.(error);
47
+ }
48
+ catch { }
49
+ };
50
+ // Fixed core, installed internally. User kinds may not replace or shadow a built-in.
51
+ const builtins = [generation, tool, postTools, collapse, job, plugin];
52
+ this.kinds = new Map(builtins.map((k) => [k.name, k]));
53
+ for (const k of options.taskKinds ?? []) {
54
+ if (k.name.startsWith("pi."))
55
+ throw new Error(`task kind "${k.name}": names beginning with "pi." are reserved`);
56
+ if (this.kinds.has(k.name))
57
+ throw new Error(`task kind "${k.name}" registered twice`);
58
+ this.kinds.set(k.name, k);
59
+ }
60
+ for (const t of options.tools ?? [])
61
+ this.registerTool(t, "open");
62
+ for (const s of Object.values(systemSections))
63
+ this.sectionMap.set(s.key, s);
64
+ for (const s of options.sections ?? [])
65
+ this.registerSection(s, "open");
66
+ for (const e of Object.values(builtinEntries))
67
+ this.entryKinds.set(e.kind, e);
68
+ this.plugins = new Map(Object.entries(options.plugins ?? {}));
69
+ this.session = new Session(storage, this.kinds, this.namespaces, this.now); // throws if the Storage already has an owner; validates config disjointness
70
+ this.session.onReport = this.onReport;
71
+ this.views = new ViewManager(this.session, this.onReport);
72
+ this.session.lineListeners.add((result) => this.views.update(result));
73
+ this.session.listeners.add((result) => {
74
+ this.views.deliver();
75
+ for (const conversation of result.changes.conversations)
76
+ this.notifyConversation(conversation);
77
+ });
78
+ const self = this;
79
+ const hooksFor = createHookRunners(() => this.hookRegistrations, (c) => this.session.index.ancestors(c), this.onReport);
80
+ this.scheduler = new Scheduler({
81
+ session: this.session,
82
+ kinds: this.kinds,
83
+ onReport: this.onReport,
84
+ ctx,
85
+ runtime(task, invoker, _ictx) {
86
+ return {
87
+ taskId: task.id,
88
+ conversationId: task.conversationId,
89
+ kind: invoker.kind,
90
+ commit: (fn, c) => {
91
+ const owns = self.session.liveTasks.get(task.id)?.owns ?? [];
92
+ const docs = owns.flatMap((id) => [
93
+ { doc: "rewindable", conversationId: id },
94
+ { doc: "sticky", conversationId: id },
95
+ ]);
96
+ return self.session
97
+ .commit(invoker, (tx, lineCtx) => fn(tx, self.session.liveTasks.get(task.id), lineCtx), c, {
98
+ docs,
99
+ })
100
+ .then((r) => r.value);
101
+ },
102
+ hooks: hooksFor(invoker.kind, { taskId: task.id, conversationId: task.conversationId }),
103
+ models: options.models,
104
+ tools: self.tools,
105
+ registries: {
106
+ sections: {
107
+ map: self.sectionMap,
108
+ get revision() {
109
+ return self.revisions.sections;
110
+ },
111
+ },
112
+ tools: {
113
+ map: self.tools,
114
+ get revision() {
115
+ return self.revisions.tools;
116
+ },
117
+ },
118
+ },
119
+ kinds: self.kinds,
120
+ processHost: options.processHost,
121
+ plugins: self.plugins,
122
+ now: self.now,
123
+ sleep: (untilMs, c) => new Promise((resolve, reject) => {
124
+ const ms = Math.max(0, untilMs - Date.now());
125
+ const t = setTimeout(() => {
126
+ c.abortSignal?.removeEventListener("abort", onAbort);
127
+ resolve();
128
+ }, ms);
129
+ const onAbort = () => {
130
+ clearTimeout(t);
131
+ reject(c.abortSignal?.reason ?? new Error("aborted"));
132
+ };
133
+ c.abortSignal?.addEventListener("abort", onAbort, { once: true });
134
+ }),
135
+ waitForInput: (id, c) => self.scheduler.waitForInput(id, c),
136
+ waitForTask: (id, c) => self.scheduler.waitForTask(id, c),
137
+ abortTask: async (id, c) => {
138
+ await self.session.onLine(async (lineCtx) => {
139
+ self.assertInvocation(invoker);
140
+ const target = self.session.liveTasks.get(id) ?? (await self.session.storage.task(id, lineCtx));
141
+ if (target === undefined)
142
+ throw new Error(`task ${id} not found`);
143
+ self.assertTaskConversationScope(task.id, target.conversationId);
144
+ }, c);
145
+ return self.scheduler.abortTask(id, c);
146
+ },
147
+ abortConversation: async (id, c) => {
148
+ await self.session.onLine(() => {
149
+ self.assertInvocation(invoker);
150
+ self.assertOwnedConversation(task.id, id);
151
+ }, c);
152
+ const h = await self.conversation(id, c);
153
+ if (h)
154
+ await h.abort(c);
155
+ },
156
+ createOwnedConversation: (spec, c) => self.session
157
+ .commit(KERNEL, (tx) => {
158
+ self.assertInvocation(invoker);
159
+ return tx.createOwnedConversation(task.id, task.conversationId, spec);
160
+ }, c)
161
+ .then((result) => result.value),
162
+ sendOwned: (id, input, c) => self.session
163
+ .commit(KERNEL, (tx) => {
164
+ self.assertInvocation(invoker);
165
+ self.assertOwnedConversation(task.id, id);
166
+ return tx.send(id, input);
167
+ }, c, {
168
+ docs: [
169
+ { doc: "rewindable", conversationId: id },
170
+ { doc: "sticky", conversationId: id },
171
+ ],
172
+ })
173
+ .then((r) => r.value),
174
+ context: (c, at, cx) => self.session.commit(invoker, (tx) => tx.context(c, at), cx).then((r) => r.value),
175
+ newestEntry: (c, opts, cx) => self.session.commit(invoker, (tx) => tx.newestEntry(c, opts), cx).then((r) => r.value),
176
+ rewindable: (c, cx) => self.session
177
+ .commit(invoker, (tx) => tx.snapshot({ doc: "rewindable", conversationId: c }), cx, {
178
+ docs: [{ doc: "rewindable", conversationId: c }],
179
+ })
180
+ .then((r) => r.value),
181
+ sticky: (c, cx) => self.session
182
+ .commit(invoker, (tx) => tx.snapshot({ doc: "sticky", conversationId: c }), cx, {
183
+ docs: [{ doc: "sticky", conversationId: c }],
184
+ })
185
+ .then((r) => r.value),
186
+ rewindableAsOf: (c, at, cx) => self.session.commit(invoker, (tx) => tx.rewindableAsOf(c, at), cx).then((r) => r.value),
187
+ };
188
+ },
189
+ });
190
+ }
191
+ notifyConversation(conversation) {
192
+ if (this.conversationListeners.size === 0)
193
+ return;
194
+ const handle = this.handle(conversation);
195
+ for (const listener of [...this.conversationListeners]) {
196
+ try {
197
+ listener(handle);
198
+ }
199
+ catch (error) {
200
+ this.onReport(error);
201
+ }
202
+ }
203
+ }
204
+ assertInvocation(invoker) {
205
+ if (!invoker.token.alive)
206
+ throw new Forbidden("operation from a finished invocation");
207
+ const live = this.session.liveTasks.get(invoker.id);
208
+ if (live === undefined)
209
+ throw new Forbidden("operation from a task that is not live");
210
+ if (invoker.mode === "run" && live.abort === true)
211
+ throw new Forbidden("operation from a marked run invocation");
212
+ }
213
+ assertTaskConversationScope(taskId, conversationId) {
214
+ const task = this.session.liveTasks.get(taskId);
215
+ if (task?.conversationId === conversationId)
216
+ return;
217
+ if (task?.owns.some((root) => this.session.index.subtree(root).has(conversationId)))
218
+ return;
219
+ throw new Forbidden(`conversation ${conversationId} is outside task ${taskId}'s subtree`);
220
+ }
221
+ assertOwnedConversation(taskId, conversationId) {
222
+ const task = this.session.liveTasks.get(taskId);
223
+ if (task === undefined || !task.owns.some((root) => this.session.index.subtree(root).has(conversationId))) {
224
+ throw new Forbidden(`conversation ${conversationId} is not owned by task ${taskId}`);
225
+ }
226
+ }
227
+ async init() {
228
+ const { session } = this;
229
+ const convs = await session.read((s, ctx) => s.conversations(ctx), this.ctx);
230
+ for (const c of convs)
231
+ session.conversationRecords.set(c.id, c);
232
+ const live = await session.read((s, ctx) => s.scanTasks({ status: ["pending", "running"] }, ctx), this.ctx);
233
+ for (const task of live)
234
+ session.liveTasks.set(task.id, task);
235
+ // Owner tasks that are terminal but still own existing conversations: needed for ancestry.
236
+ const owners = new Set(convs.map((c) => c.owner).filter((o) => o !== undefined));
237
+ for (const id of owners)
238
+ if (!session.liveTasks.has(id)) {
239
+ const t = await session.read((s, ctx) => s.task(id, ctx), this.ctx);
240
+ if (t)
241
+ session.ownerTaskCache.set(id, t);
242
+ }
243
+ if (convs.length === 0) {
244
+ await session.commit(KERNEL, (tx) => tx.createConversation({ rewindable: this.options.root?.rewindable, sticky: this.options.root?.sticky }), this.ctx);
245
+ }
246
+ }
247
+ resume() {
248
+ if (this.suspended)
249
+ throw new Error("cannot resume a suspended harness; reopen storage with a new harness");
250
+ if (this.resumed)
251
+ return;
252
+ this.resumed = true;
253
+ void this.reconcileOrphans()
254
+ .then(() => {
255
+ if (!this.suspended)
256
+ this.scheduler.resume();
257
+ })
258
+ .catch((error) => this.onReport(error));
259
+ }
260
+ async reconcileOrphans() {
261
+ const orphaned = [...this.session.liveTasks.values()].filter((task) => !this.kinds.has(task.kind));
262
+ if (orphaned.length === 0)
263
+ return;
264
+ const docs = [...new Set(orphaned.map((task) => task.conversationId))].map((conversationId) => ({ doc: "sticky", conversationId }));
265
+ await this.session.commit(KERNEL, (tx) => {
266
+ for (const task of orphaned)
267
+ tx.setTask({ ...task, status: "terminal", outcome: { status: "orphaned" } });
268
+ }, this.ctx, { docs });
269
+ }
270
+ quiescent() {
271
+ return this.scheduler.quiescent();
272
+ }
273
+ hold() {
274
+ if (!this.scheduler.quiescent())
275
+ throw new Error("cannot hold a non-quiescent harness; suspend it instead");
276
+ return this.scheduler.hold();
277
+ }
278
+ /** Cancel and join in-process invocations, clear transient waits, then close without terminalizing tasks. */
279
+ async suspend(ctx) {
280
+ if (this.suspended)
281
+ return;
282
+ this.suspended = true;
283
+ await this.scheduler.joinAll();
284
+ const tools = [...this.session.liveTasks.values()].filter((task) => task.kind === "pi.tool");
285
+ if (tools.length > 0) {
286
+ const docs = [...new Set(tools.map((task) => task.conversationId))].map((conversationId) => ({ doc: "sticky", conversationId }));
287
+ await this.session.commit(KERNEL, (tx) => {
288
+ for (const task of tools) {
289
+ const index = task.input.index;
290
+ const slot = tx.sticky(task.conversationId).turn.tools[index];
291
+ if (slot?.waitingOn !== undefined)
292
+ delete slot.waitingOn;
293
+ }
294
+ }, ctx, { docs });
295
+ }
296
+ this.views.close();
297
+ await this.session.close(ctx);
298
+ }
299
+ // --- registries --------------------------------------------------------------
300
+ registerTaskKind(kind) {
301
+ if (kind.name.startsWith("pi."))
302
+ throw new Error(`task kind "${kind.name}": names beginning with "pi." are reserved`);
303
+ if (this.kinds.has(kind.name))
304
+ throw new Error(`task kind "${kind.name}" already registered`);
305
+ this.session.defaults.register(kind);
306
+ this.kinds.set(kind.name, kind);
307
+ this.scheduler.kick();
308
+ return () => {
309
+ if (this.kinds.get(kind.name) !== kind)
310
+ return;
311
+ this.kinds.delete(kind.name);
312
+ this.session.defaults.unregister(kind);
313
+ };
314
+ }
315
+ namespace(id, defaults, opts = {}) {
316
+ if (!/^[a-z][a-z0-9_.-]*$/i.test(id) || id.startsWith("pi."))
317
+ throw new Error(`invalid namespace "${id}"`);
318
+ if (this.namespaces.has(id))
319
+ throw new Error(`namespace "${id}" already registered`);
320
+ const stored = JSON.parse(JSON.stringify(defaults));
321
+ const routes = new Map();
322
+ for (const doc of ["rewindable", "sticky", "session"]) {
323
+ for (const key of Object.keys(stored[doc] ?? {})) {
324
+ if (routes.has(key))
325
+ throw new Error(`namespace "${id}" key "${key}" is declared in more than one document`);
326
+ routes.set(key, doc);
327
+ }
328
+ }
329
+ let token;
330
+ token = {
331
+ id,
332
+ unregister: () => {
333
+ if (this.namespaces.get(id)?.token !== token)
334
+ return;
335
+ this.namespaces.delete(id);
336
+ for (let index = this.hookRegistrations.length - 1; index >= 0; index--) {
337
+ if (this.hookRegistrations[index].namespace === token)
338
+ this.hookRegistrations.splice(index, 1);
339
+ }
340
+ },
341
+ };
342
+ const registration = {
343
+ token,
344
+ defaults: {
345
+ rewindable: stored.rewindable ?? {},
346
+ sticky: stored.sticky ?? {},
347
+ session: stored.session ?? {},
348
+ },
349
+ routes,
350
+ project: opts.view === undefined ? undefined : (slice) => opts.view(slice),
351
+ };
352
+ this.namespaces.set(id, registration);
353
+ return token;
354
+ }
355
+ /** Register a tool. A duplicate name rejects. Unregister removes only this exact declaration; idempotent. */
356
+ registerTool(tool, at = "runtime") {
357
+ if (this.tools.has(tool.name))
358
+ throw new Error(`tool "${tool.name}" already registered`);
359
+ this.tools.set(tool.name, tool);
360
+ this.revisions.tools++;
361
+ void at;
362
+ return () => {
363
+ if (this.tools.get(tool.name) === tool) {
364
+ this.tools.delete(tool.name);
365
+ this.revisions.tools++;
366
+ }
367
+ };
368
+ }
369
+ registerSection(section, at = "runtime") {
370
+ if (this.sectionMap.has(section.key))
371
+ throw new Error(`section "${section.key}" already registered`);
372
+ this.sectionMap.set(section.key, section);
373
+ this.revisions.sections++;
374
+ void at;
375
+ return () => {
376
+ if (this.sectionMap.get(section.key) === section) {
377
+ this.sectionMap.delete(section.key);
378
+ this.revisions.sections++;
379
+ }
380
+ };
381
+ }
382
+ registerEntryKind(kind) {
383
+ if (kind.kind.startsWith("pi."))
384
+ throw new Error(`entry kind "${kind.kind}": names beginning with "pi." are reserved`);
385
+ if (this.entryKinds.has(kind.kind))
386
+ throw new Error(`entry kind "${kind.kind}" already registered`);
387
+ this.entryKinds.set(kind.kind, kind);
388
+ return () => {
389
+ if (this.entryKinds.get(kind.kind) === kind)
390
+ this.entryKinds.delete(kind.kind);
391
+ };
392
+ }
393
+ /** Register namespace-bound handlers for one kind's hook points, harness-wide. Both tokens must be current. */
394
+ hooks(namespace, kind, handlers, _opts) {
395
+ return this.addHooks({ namespace: this.checkNamespace(namespace), kind: this.checkKind(kind), handlers });
396
+ }
397
+ checkKind(kind) {
398
+ if (this.kinds.get(kind.name) !== kind)
399
+ throw new Error(`kind "${kind.name}" is not the registered token`);
400
+ return kind;
401
+ }
402
+ checkNamespace(namespace) {
403
+ if (this.namespaces.get(namespace.id)?.token !== namespace)
404
+ throw new Forbidden(`namespace "${namespace.id}" is stale`);
405
+ return namespace;
406
+ }
407
+ addHooks(reg) {
408
+ this.hookRegistrations.push(reg);
409
+ let done = false;
410
+ return () => {
411
+ if (done)
412
+ return;
413
+ done = true;
414
+ const index = this.hookRegistrations.indexOf(reg);
415
+ if (index >= 0)
416
+ this.hookRegistrations.splice(index, 1);
417
+ };
418
+ }
419
+ // --- conversations -------------------------------------------------------------
420
+ async root(ctx) {
421
+ return this.conversation(1, ctx).then((c) => c);
422
+ }
423
+ onConversation(listener) {
424
+ this.conversationListeners.add(listener);
425
+ for (const conversation of this.session.conversationRecords.values()) {
426
+ try {
427
+ listener(this.handle(conversation));
428
+ }
429
+ catch (error) {
430
+ this.onReport(error);
431
+ }
432
+ }
433
+ return () => this.conversationListeners.delete(listener);
434
+ }
435
+ async conversation(id, ctx) {
436
+ const c = await this.session.read((s, lineCtx) => s.conversation(id, lineCtx), ctx);
437
+ return c === undefined ? undefined : this.handle(c);
438
+ }
439
+ async createConversation(spec, ctx) {
440
+ const { input, ...rest } = spec;
441
+ const id = await this.session
442
+ .commit(KERNEL, async (tx) => {
443
+ const id = tx.createConversation(rest);
444
+ if (input !== undefined)
445
+ await tx.send(id, { content: input });
446
+ return id;
447
+ }, ctx)
448
+ .then((r) => r.value);
449
+ return (await this.conversation(id, ctx));
450
+ }
451
+ entries(scan, ctx) {
452
+ return this.session.read((s, lineCtx) => s.scanEntries(scan, lineCtx), ctx);
453
+ }
454
+ getTask(id, ctx) {
455
+ return this.session.read((s, lineCtx) => s.task(id, lineCtx), ctx);
456
+ }
457
+ async abortInput(id, ctx, conversationId) {
458
+ if (conversationId !== undefined) {
459
+ const input = await this.session.read((storage, lineCtx) => storage.input(id, lineCtx), ctx);
460
+ if (input !== undefined && input.conversationId !== conversationId)
461
+ throw new Forbidden(`input ${id} is outside conversation ${conversationId}`);
462
+ }
463
+ return this.inputHandle(id).abort(ctx);
464
+ }
465
+ abortTask(id, ctx) {
466
+ return this.scheduler.abortTask(id, ctx);
467
+ }
468
+ /** Durably mark a task for abort without signalling its invocation; the scheduler aborts it when it next drains. */
469
+ markTask(id, ctx) {
470
+ return this.session
471
+ .commit(KERNEL, async (tx) => {
472
+ const t = await tx.task(id);
473
+ if (t === undefined)
474
+ throw new Error(`task ${id} not found`);
475
+ if (t.status === "terminal")
476
+ return "terminal";
477
+ tx.markTask(id);
478
+ return "marked";
479
+ }, ctx)
480
+ .then((r) => r.value);
481
+ }
482
+ waitForIdle(ctx) {
483
+ return this.scheduler.waitForIdle(undefined, ctx);
484
+ }
485
+ waitForTask(id, ctx) {
486
+ return this.scheduler.waitForTask(id, ctx);
487
+ }
488
+ /** Signals every invocation, waits for them, closes storage. Writes nothing. */
489
+ async close(ctx) {
490
+ await this.suspend(ctx);
491
+ }
492
+ handle(c) {
493
+ const self = this;
494
+ const docs = [
495
+ { doc: "rewindable", conversationId: c.id },
496
+ { doc: "sticky", conversationId: c.id },
497
+ ];
498
+ const hostInvoker = { type: "host", conversationId: c.id };
499
+ const kernelInvoker = { type: "kernel", conversationId: c.id };
500
+ const host = (fn, ctx) => self.session.commit(hostInvoker, (tx, lineCtx) => fn(tx, lineCtx), ctx, { docs }).then((r) => r.value);
501
+ const kernel = (fn, ctx) => self.session.commit(kernelInvoker, fn, ctx, { docs }).then((r) => r.value);
502
+ const config = {
503
+ get: (ctx) => host((tx) => {
504
+ const cfg = tx.config(c.id);
505
+ const out = {};
506
+ for (const key of self.session.defaults.route.keys())
507
+ out[key] = cfg.get(key);
508
+ return out;
509
+ }, ctx),
510
+ set: (patch, ctx) => host((tx) => {
511
+ const cfg = tx.config(c.id);
512
+ for (const [key, value] of Object.entries(patch)) {
513
+ if (value === undefined)
514
+ throw new Error(`config.set(${key}): use config.reset()`);
515
+ cfg.set(key, value);
516
+ }
517
+ }, ctx),
518
+ reset: (keys, ctx) => host((tx) => {
519
+ const cfg = tx.config(c.id);
520
+ for (const key of keys)
521
+ cfg.reset(String(key));
522
+ }, ctx),
523
+ };
524
+ return {
525
+ id: c.id,
526
+ config,
527
+ async send(input, ctx) {
528
+ const id = await kernel((tx) => tx.send(c.id, input), ctx);
529
+ return self.inputHandle(id);
530
+ },
531
+ write: (entry, ctx) => host((tx) => tx.write(c.id, entry), ctx),
532
+ commit: host,
533
+ rewindable: (ctx) => host((tx) => tx.snapshot({ doc: "rewindable", conversationId: c.id }), ctx),
534
+ sticky: (ctx) => host((tx) => tx.snapshot({ doc: "sticky", conversationId: c.id }), ctx),
535
+ context: (ctx) => host((tx) => tx.context(c.id), ctx),
536
+ async fork(at, spec, ctx) {
537
+ const id = await self.session.fork(c.id, at, spec, ctx);
538
+ return (await self.conversation(id, ctx));
539
+ },
540
+ collapse: (instructions, ctx) => kernel(async (tx) => {
541
+ const { entries } = await tx.context(c.id);
542
+ const state = tx.rewindable(c.id);
543
+ const through = chooseThrough(entries, state.keepRecent);
544
+ if (through === undefined)
545
+ throw new Error("nothing to collapse");
546
+ return tx.createTask({
547
+ kind: "pi.collapse",
548
+ conversationId: c.id,
549
+ background: true,
550
+ input: { reason: "manual", through, ...(instructions === undefined ? {} : { instructions }) },
551
+ });
552
+ }, ctx),
553
+ reset: async (handoff, ctx) => {
554
+ await kernel((tx) => tx.write(c.id, handoff === undefined
555
+ ? { kind: "pi.reset", head: "self" }
556
+ : {
557
+ kind: "pi.handoff",
558
+ head: "self",
559
+ model: [{ role: "user", content: handoff, timestamp: self.now() }],
560
+ }), ctx);
561
+ },
562
+ async abort(ctx) {
563
+ const owned = [];
564
+ const marked = await kernel(async (tx) => {
565
+ const s = tx.sticky(c.id);
566
+ const withdrawn = s.inbox.filter((q) => q.mode !== "write").map((q) => q.id);
567
+ for (let i = s.inbox.length - 1; i >= 0; i--)
568
+ if (s.inbox[i].mode !== "write")
569
+ s.inbox.splice(i, 1);
570
+ await tx.resolveInputs(withdrawn, { status: "unanswered", reason: "aborted" });
571
+ for (const input of withdrawn)
572
+ tx.emit({ type: "input.aborted", input });
573
+ const ids = [];
574
+ for (const t of [...self.session.liveTasks.values()]) {
575
+ if (t.conversationId !== c.id || t.background)
576
+ continue;
577
+ for (const o of t.owns)
578
+ owned.push(o);
579
+ if (t.abort !== true)
580
+ tx.markTask(t.id);
581
+ ids.push(t.id);
582
+ }
583
+ return ids;
584
+ }, ctx);
585
+ for (const id of marked)
586
+ await self.scheduler.abortTask(id, ctx);
587
+ await self.scheduler.waitForIdle(c.id, ctx);
588
+ await Promise.all(owned.map((o) => self.scheduler.waitForIdle(o, ctx)));
589
+ },
590
+ waitForIdle: (ctx) => self.scheduler.waitForIdle(c.id, ctx),
591
+ hooks: (namespace, kind, handlers, opts) => self.addHooks({
592
+ namespace: self.checkNamespace(namespace),
593
+ kind: self.checkKind(kind),
594
+ handlers,
595
+ conversationId: c.id,
596
+ subtree: opts?.subtree,
597
+ }),
598
+ watch: (ctx) => self.watch(c, ctx),
599
+ };
600
+ }
601
+ /** Capture and subscribe in one line operation. */
602
+ async watch(conversation, ctx) {
603
+ return this.session
604
+ .commit({ type: "host", conversationId: conversation.id }, async (tx) => {
605
+ const entries = await captureActiveTranscript((scan) => tx.scanEntries(scan), conversation.id);
606
+ return this.views.watch(conversation, entries);
607
+ }, ctx, {
608
+ docs: [
609
+ { doc: "rewindable", conversationId: conversation.id },
610
+ { doc: "sticky", conversationId: conversation.id },
611
+ ],
612
+ })
613
+ .then((result) => result.value);
614
+ }
615
+ inputHandle(id) {
616
+ return {
617
+ id,
618
+ result: (ctx) => this.session.read((s, lineCtx) => s.input(id, lineCtx), ctx),
619
+ wait: (ctx) => this.scheduler.waitForInput(id, ctx),
620
+ abort: (ctx) => this.session.commit(KERNEL, (tx) => tx.withdrawInput(id), ctx, { docs: [] }).then((r) => r.value),
621
+ };
622
+ }
623
+ }
624
+ /**
625
+ * §9: H = newest fork-visible entry with a head. Active transcript = H plus every fork-visible entry
626
+ * with id ≥ H.head, chronological, nothing dropped inside the range; the whole transcript if no head.
627
+ */
628
+ export async function captureActiveTranscript(scan, conversationId) {
629
+ const [h] = await scan({ conversationId, withHead: true, limit: 1 });
630
+ const from = h?.head;
631
+ const out = [];
632
+ let before;
633
+ for (;;) {
634
+ const page = await scan({ conversationId, ...(before === undefined ? {} : { before }), limit: 256 });
635
+ let done = page.length < 256;
636
+ for (const e of page) {
637
+ if (from !== undefined && e.id < from) {
638
+ done = true;
639
+ break;
640
+ }
641
+ out.push(e);
642
+ }
643
+ if (done)
644
+ break;
645
+ before = page[page.length - 1].id;
646
+ }
647
+ return out.reverse();
648
+ }
649
+ export { withAbortSignal, isCoreKind };
650
+ export { applyEnvelope, WATCH_CAPACITY } from "./view.js";
651
+ /** The built-in kinds, as typed witnesses for `api.task`, `waitForTask`, and `hooks(kind, …)`. */
652
+ export const kinds = { generation, tool, postTools, collapse, job, plugin };
653
+ export const entries = builtinEntries;
654
+ //# sourceMappingURL=harness.js.map