@qvac/harness 0.7.1 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +224 -0
  2. package/dist-lib/index.d.ts +55 -3
  3. package/dist-lib/index.js +721 -236
  4. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  6. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  44. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  46. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  48. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  50. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  52. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  54. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  56. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  58. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  60. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  62. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  64. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  66. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  67. package/dist-lib/lib/capabilities/completion.js +20 -1301
  68. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  69. package/dist-lib/lib/capabilities/download.js +7 -2
  70. package/dist-lib/lib/capabilities/invoke.js +5 -3
  71. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  72. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  73. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  74. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  75. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  76. package/dist-lib/lib/capabilities/types.js +1 -0
  77. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  78. package/dist-lib/lib/completion-lifecycle.js +126 -0
  79. package/dist-lib/lib/connect.d.ts +10 -3
  80. package/dist-lib/lib/connect.js +329 -23
  81. package/dist-lib/lib/contract-types.d.ts +19 -2
  82. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  83. package/dist-lib/lib/harness-lifecycle.js +129 -0
  84. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  85. package/dist-lib/lib/helpers/detach-json.js +146 -0
  86. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  87. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  88. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  89. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  90. package/dist-lib/lib/helpers/race-abort.js +2 -7
  91. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  92. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  93. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  94. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  95. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  96. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  97. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  98. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  99. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  101. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  102. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  103. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  105. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  106. package/dist-lib/lib/hooks/pipeline.js +24 -2
  107. package/dist-lib/lib/hooks/run.d.ts +2 -1
  108. package/dist-lib/lib/hooks/run.js +19 -7
  109. package/dist-lib/lib/hooks/types.d.ts +17 -6
  110. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  111. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  112. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  114. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  116. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  118. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  119. package/dist-lib/lib/machine/completion/context.js +44 -0
  120. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  121. package/dist-lib/lib/machine/completion/contract.js +1 -0
  122. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  124. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  125. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  126. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  127. package/dist-lib/lib/machine/completion/machine.js +511 -0
  128. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  129. package/dist-lib/lib/machine/completion/ports.js +224 -0
  130. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  131. package/dist-lib/lib/machine/completion/rules.js +115 -0
  132. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  133. package/dist-lib/lib/machine/completion/types.js +1 -0
  134. package/dist-lib/lib/machine/engine.d.ts +93 -0
  135. package/dist-lib/lib/machine/engine.js +483 -0
  136. package/dist-lib/lib/machine/errors.d.ts +44 -0
  137. package/dist-lib/lib/machine/errors.js +373 -0
  138. package/dist-lib/lib/machine/guards.d.ts +2 -0
  139. package/dist-lib/lib/machine/guards.js +6 -0
  140. package/dist-lib/lib/machine/index.d.ts +20 -0
  141. package/dist-lib/lib/machine/index.js +11 -0
  142. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  143. package/dist-lib/lib/machine/replay-output.js +50 -0
  144. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  145. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  146. package/dist-lib/lib/models/model-pool.d.ts +14 -6
  147. package/dist-lib/lib/models/model-pool.js +95 -30
  148. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  149. package/dist-lib/lib/run-log/history-codec.js +973 -0
  150. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  151. package/dist-lib/lib/run-log/patch.js +93 -0
  152. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  153. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  154. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  155. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  156. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  157. package/dist-lib/lib/run-log/protocol.js +1056 -0
  158. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  159. package/dist-lib/lib/run-log/replay.js +129 -0
  160. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  161. package/dist-lib/lib/run-log/run-journal.js +783 -0
  162. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  163. package/dist-lib/lib/run-log-inspect.js +627 -0
  164. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  165. package/dist-lib/lib/run-log-protocol.js +1 -0
  166. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  167. package/dist-lib/lib/run-log-redact.js +1107 -0
  168. package/dist-lib/lib/serve.js +307 -11
  169. package/dist-lib/lib/spawn.js +3 -2
  170. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  171. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +2 -1
  172. package/dist-lib/lib/tools/exec.d.ts +2 -1
  173. package/dist-lib/lib/tools/exec.js +57 -2
  174. package/dist-lib/lib/tools/generate-image.js +1 -1
  175. package/dist-lib/lib/tools/generate-music.js +1 -1
  176. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  177. package/dist-lib/lib/tools/http-request.js +2 -1
  178. package/dist-lib/lib/tools/mcp-call.js +8 -1
  179. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  180. package/dist-lib/lib/tools/operations/builders.js +1 -0
  181. package/dist-lib/lib/tools/operations/compile.js +11 -1
  182. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  183. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  184. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  185. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  186. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  187. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  188. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  189. package/dist-lib/lib/wire.d.ts +5 -0
  190. package/dist-lib/lib/wire.js +128 -5
  191. package/dist-lib/schema/harness.js +20 -5
  192. package/dist-lib/spec/hrpc/messages.js +95 -15
  193. package/dist-lib/spec/hyperschema/index.js +95 -15
  194. package/dist-lib/spec/hyperschema/schema.json +69 -1
  195. package/dist-lib/spec/hyperschema/types.d.ts +10 -3
  196. package/package.json +20 -2
  197. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  198. package/patches/bare-stow@0.1.5.patch +68 -0
  199. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  200. package/patches/localwatch@1.4.3.patch +13 -0
  201. package/patches/pyodide@314.0.2.patch +144 -0
@@ -9,7 +9,12 @@ import type { DeleteModelSummary } from './models/model-delete.ts';
9
9
  import type { CopyBaseResult, DeleteOverlayResult, ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud.ts';
10
10
  import type { SkillSettingEntry } from './tools/skills/skill-settings.ts';
11
11
  export declare const HARNESS_DIED = "HARNESS_DIED";
12
- export declare function connectHarness(transport: Transport): RemoteHarness;
12
+ export interface ConnectOptions {
13
+ pauseGraceMs?: number;
14
+ runLogReleaseTimeoutMs?: number;
15
+ }
16
+ export type { HostRunLog, HostRunLogLease, RunRecord } from './contract-types.ts';
17
+ export declare function connectHarness(transport: Transport, options?: ConnectOptions): RemoteHarness;
13
18
  export declare function readEmbeddingDeviceResponse(response: unknown): EmbedDevice;
14
19
  export declare class RemoteHarness extends EventEmitter implements HarnessContract {
15
20
  on: (event: 'model' | 'status', listener: (...args: never[]) => void) => this;
@@ -18,11 +23,13 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
18
23
  private _session;
19
24
  private _opening;
20
25
  private _closed;
26
+ private readonly _pauseGraceMs;
27
+ private readonly _runLogReleaseTimeoutMs;
21
28
  private _customModels;
22
- constructor(transport: Transport);
29
+ constructor(transport: Transport, options?: ConnectOptions);
23
30
  private _ensure;
24
31
  private _open;
25
- completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
32
+ completion(context: CompletionContext | import('./contract-types.ts').ResumeCompletionContext): AsyncIterable<HarnessEvent>;
26
33
  invoke(context: InvokeContext): AsyncIterable<HarnessEvent>;
27
34
  private _run;
28
35
  download(context: DownloadContext): AsyncIterable<HarnessEvent>;
@@ -1,3 +1,5 @@
1
+ import b4a from 'b4a';
2
+ import { randomBytes } from 'crypto';
1
3
  import EventEmitter from 'events';
2
4
  import fs from 'fs';
3
5
  import { z } from 'zod';
@@ -6,6 +8,7 @@ import { harnessClient } from '../spec/calls.js';
6
8
  import HRPC from '../spec/hrpc/index.js';
7
9
  import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
8
10
  import { readModelSourcePreview } from './capabilities/preview-model-source.js';
11
+ import { validateEngineBoot } from './run-log-protocol.js';
9
12
  import { hlog } from './log.js';
10
13
  import { assertEmbedDevice } from './models/embed-config.js';
11
14
  import { addonTaskType } from './models/model-task.js';
@@ -17,8 +20,9 @@ import { SKILL_STATE } from './tools/skills/skill-state.js';
17
20
  import { asToolset } from './tools/toolsets.js';
18
21
  import { fromDownloadWire, fromWire, unpackVectors } from './wire.js';
19
22
  export const HARNESS_DIED = 'HARNESS_DIED';
20
- export function connectHarness(transport) {
21
- return new RemoteHarness(transport);
23
+ const PAUSE_GRACE_MS = 10_000;
24
+ export function connectHarness(transport, options = {}) {
25
+ return new RemoteHarness(transport, options);
22
26
  }
23
27
  export function readEmbeddingDeviceResponse(response) {
24
28
  const device = isRecord(response) ? response.device : undefined;
@@ -33,10 +37,14 @@ export class RemoteHarness extends EventEmitter {
33
37
  _session = null;
34
38
  _opening = null;
35
39
  _closed = false;
40
+ _pauseGraceMs;
41
+ _runLogReleaseTimeoutMs;
36
42
  _customModels = [];
37
- constructor(transport) {
43
+ constructor(transport, options = {}) {
38
44
  super();
39
45
  this._transport = transport;
46
+ this._pauseGraceMs = options.pauseGraceMs ?? PAUSE_GRACE_MS;
47
+ this._runLogReleaseTimeoutMs = options.runLogReleaseTimeoutMs ?? 5000;
40
48
  }
41
49
  _ensure() {
42
50
  if (this._closed) {
@@ -97,16 +105,32 @@ export class RemoteHarness extends EventEmitter {
97
105
  return session;
98
106
  }
99
107
  async *completion(context) {
108
+ const runId = context.runId ?? b4a.toHex(randomBytes(16));
109
+ if (context.signal?.aborted) {
110
+ yield { type: 'aborted', runId };
111
+ return;
112
+ }
113
+ const explicitEmptyIdentity = explicitEmptyIdentityFields(context);
114
+ const payload = {
115
+ resume: context.resume === true,
116
+ recoveryEvents: true,
117
+ resolutions: context.recovery
118
+ };
119
+ if (explicitEmptyIdentity.length) {
120
+ payload.explicitEmptyIdentity = explicitEmptyIdentity;
121
+ }
100
122
  yield* this._run({
101
123
  type: 'start',
124
+ runId,
125
+ payload,
102
126
  model: context.model,
103
- agent: knobs(context.agent),
127
+ agent: context.agent ? knobs(context.agent) : undefined,
104
128
  cacheKey: context.cacheKey,
105
129
  thinkingDefault: context.thinkingDefault,
106
130
  generationParams: context.generationParams,
107
131
  responseFormat: context.responseFormat,
108
132
  captureInferenceCalls: context.captureInferenceCalls,
109
- messages: context.messages.map(({ role, content, attachments }) => ({
133
+ messages: context.messages?.map(({ role, content, attachments }) => ({
110
134
  role,
111
135
  content,
112
136
  attachments: attachments?.map(({ path: file }) => ({ path: file }))
@@ -115,7 +139,7 @@ export class RemoteHarness extends EventEmitter {
115
139
  grantedDirs: context.grantedDirs,
116
140
  grantedDrives: context.grantedDrives,
117
141
  grantedWriteDirs: context.grantedWriteDirs
118
- }, context);
142
+ }, { ...context, runId });
119
143
  }
120
144
  async *invoke(context) {
121
145
  const agent = context.agent ?? {};
@@ -136,8 +160,62 @@ export class RemoteHarness extends EventEmitter {
136
160
  });
137
161
  }
138
162
  async *_run(open, context) {
139
- const { client } = await this._ensure();
163
+ if (context.signal?.aborted) {
164
+ yield { type: 'aborted', runId: context.runId ?? open.runId ?? undefined };
165
+ return;
166
+ }
167
+ let session;
168
+ try {
169
+ session = await awaitWithAbort(this._ensure(), context.signal);
170
+ }
171
+ catch (err) {
172
+ if (context.signal?.aborted) {
173
+ yield { type: 'aborted', runId: context.runId ?? open.runId ?? undefined };
174
+ return;
175
+ }
176
+ throw err;
177
+ }
178
+ const { client } = session;
179
+ let pausable = false;
180
+ const peer = this.capability().then((c) => {
181
+ pausable = c.hostRunLog === true && c.hostRunLogVersion === 1;
182
+ return c;
183
+ }, () => null);
184
+ const canResume = async () => {
185
+ const c = await peer;
186
+ return context.runLog
187
+ ? c?.hostRunLog === true && c.hostRunLogVersion === 1
188
+ : c?.completionRecovery === true;
189
+ };
190
+ let capability;
191
+ try {
192
+ capability = await awaitWithAbort(peer, context.signal);
193
+ }
194
+ catch (err) {
195
+ if (context.signal?.aborted) {
196
+ yield { type: 'aborted', runId: context.runId ?? open.runId ?? undefined };
197
+ return;
198
+ }
199
+ throw err;
200
+ }
201
+ if (context.runLog && (capability?.hostRunLog !== true || capability.hostRunLogVersion !== 1)) {
202
+ throw new Error('This peer does not support a host-owned run log');
203
+ }
204
+ if (context.resume && !(await canResume())) {
205
+ throw new Error('This peer does not support persistent completion recovery');
206
+ }
207
+ let activeRunId = context.runId ?? open.runId ?? undefined;
208
+ if (context.signal?.aborted) {
209
+ yield { type: 'aborted', runId: activeRunId };
210
+ return;
211
+ }
212
+ const leasedRunId = context.runLog ? activeRunId : undefined;
213
+ if (context.runLog && !leasedRunId) {
214
+ throw new Error('host-owned run log requires a run id');
215
+ }
216
+ let hostLease;
140
217
  let duplex = null;
218
+ let startFrame = open;
141
219
  let emittedAborted = false;
142
220
  const staged = [];
143
221
  let closed = false;
@@ -147,36 +225,67 @@ export class RemoteHarness extends EventEmitter {
147
225
  void fs.promises.unlink(file).catch(noop);
148
226
  }
149
227
  };
150
- const onAbort = () => duplex?.destroy();
228
+ let pauseTimer;
229
+ let started = false;
230
+ const onAbort = () => {
231
+ if (!pausable || !duplex || !started) {
232
+ return duplex?.destroy();
233
+ }
234
+ duplex.write({ type: 'pause' });
235
+ pauseTimer = setTimeout(() => duplex?.destroy(), this._pauseGraceMs);
236
+ };
151
237
  try {
238
+ if (context.runLog) {
239
+ const leaseResult = await acquireHostRunLogLease(context.runLog, leasedRunId, context.signal);
240
+ if (leaseResult === null) {
241
+ yield { type: 'aborted', runId: activeRunId };
242
+ return;
243
+ }
244
+ hostLease = leaseResult;
245
+ }
246
+ if (hostLease) {
247
+ validateHostRunLogLease(hostLease, leasedRunId);
248
+ const payload = isRecord(open.payload) ? { ...open.payload } : {};
249
+ payload.resume =
250
+ context.resume === true ||
251
+ (hostLease.boot !== undefined && hostLease.boot.metadata !== null);
252
+ payload.runLog = {
253
+ boot: hostLease.boot ?? null,
254
+ digestKey: b4a.toHex(hostLease.digestKey),
255
+ hostAppendTimeoutMs: context.hostAppendTimeoutMs
256
+ };
257
+ startFrame = { ...open, payload };
258
+ }
259
+ if (context.signal?.aborted) {
260
+ yield { type: 'aborted', runId: activeRunId };
261
+ return;
262
+ }
263
+ context.signal?.addEventListener?.('abort', onAbort, { once: true });
152
264
  duplex = client.completion();
153
- duplex.write(open);
154
265
  if (context.signal?.aborted) {
155
266
  duplex.destroy();
156
- yield { type: 'aborted' };
267
+ yield { type: 'aborted', runId: activeRunId };
157
268
  return;
158
269
  }
159
- context.signal?.addEventListener?.('abort', onAbort, { once: true });
270
+ duplex.write(startFrame);
271
+ started = true;
160
272
  hlog('client wire open', { model: context.model, capability: open.capability });
161
273
  for await (const event of frames(duplex)) {
162
274
  received++;
163
275
  if (event.type !== 'content' && event.type !== 'thinking') {
164
276
  hlog('client frame', { type: event.type, n: received });
165
277
  }
278
+ if (event.type === 'run-started' || event.type === 'run-resumed') {
279
+ activeRunId = event.runId ?? activeRunId;
280
+ }
166
281
  if (event.type === 'done') {
167
282
  break;
168
283
  }
169
284
  if (event.type === 'approval-ask') {
170
285
  const callId = event.callId;
286
+ const approval = approvalCall(event);
171
287
  void Promise.resolve()
172
- .then(() => context.approve?.({
173
- name: event.name ?? '',
174
- arguments: isRecord(event.arguments) ? event.arguments : {},
175
- resource: event.resource ?? null,
176
- effect: event.effect === 'read' ? 'read' : 'write',
177
- intent: event.intent || null,
178
- display: isToolCallDisplay(event.display) ? event.display : null
179
- }))
288
+ .then(() => context.approve?.(approval))
180
289
  .then((approved) => {
181
290
  if (!closed) {
182
291
  duplex.write({ type: 'decision', callId, approved: approved === true });
@@ -248,6 +357,30 @@ export class RemoteHarness extends EventEmitter {
248
357
  .catch((err) => hlog('drive reply failed', { callId, message: errorMessage(err) }));
249
358
  continue;
250
359
  }
360
+ if (event.type === 'run-log-append') {
361
+ const callId = event.callId;
362
+ void bridgeRunLog({
363
+ event,
364
+ context,
365
+ runId: leasedRunId,
366
+ generation: hostLease?.generation
367
+ })
368
+ .then((frame) => {
369
+ if (!closed) {
370
+ duplex.write({ ...frame, callId });
371
+ }
372
+ }, (err) => {
373
+ if (!closed) {
374
+ duplex.write({
375
+ type: 'run-log-result',
376
+ callId,
377
+ message: errorMessage(err)
378
+ });
379
+ }
380
+ })
381
+ .catch((err) => hlog('run-log reply failed', { callId, message: errorMessage(err) }));
382
+ continue;
383
+ }
251
384
  if (event.type === 'rag-ask') {
252
385
  const callId = event.callId;
253
386
  void bridgeRag(event, context)
@@ -283,7 +416,7 @@ export class RemoteHarness extends EventEmitter {
283
416
  continue;
284
417
  }
285
418
  const wire = fromWire(event);
286
- if (wire.type === 'aborted') {
419
+ if (wire.type === 'aborted' || wire.type === 'paused') {
287
420
  emittedAborted = true;
288
421
  }
289
422
  yield wire;
@@ -293,21 +426,42 @@ export class RemoteHarness extends EventEmitter {
293
426
  catch (err) {
294
427
  if (context.signal?.aborted) {
295
428
  if (!emittedAborted) {
296
- yield { type: 'aborted' };
429
+ yield { type: 'aborted', runId: activeRunId };
297
430
  }
298
431
  return;
299
432
  }
433
+ if (!started) {
434
+ throw err;
435
+ }
300
436
  throw died(`harness died mid-run: ${errorMessage(err)}`);
301
437
  }
302
438
  finally {
303
439
  hlog('client run end', { received, aborted: Boolean(context.signal?.aborted) });
304
440
  context.signal?.removeEventListener?.('abort', onAbort);
441
+ clearTimeout(pauseTimer);
305
442
  duplex?.destroy();
306
443
  closed = true;
307
444
  reclaim();
445
+ if (hostLease && leasedRunId && context.runLog) {
446
+ let timer;
447
+ try {
448
+ await Promise.race([
449
+ context.runLog.release(leasedRunId, hostLease.generation),
450
+ new Promise((_resolve, reject) => {
451
+ timer = setTimeout(() => reject(new Error('host run-log release timed out')), this._runLogReleaseTimeoutMs);
452
+ })
453
+ ]);
454
+ }
455
+ catch (err) {
456
+ hlog('host run-log release failed', { runId: leasedRunId, message: errorMessage(err) });
457
+ }
458
+ finally {
459
+ clearTimeout(timer);
460
+ }
461
+ }
308
462
  }
309
463
  if (context.signal?.aborted && !emittedAborted) {
310
- yield { type: 'aborted' };
464
+ yield { type: 'aborted', runId: activeRunId };
311
465
  }
312
466
  }
313
467
  async *download(context) {
@@ -616,7 +770,7 @@ export class RemoteHarness extends EventEmitter {
616
770
  const { client } = await this._ensure();
617
771
  const response = await client.capability({});
618
772
  const memoryBudget = response.memoryBudget;
619
- return {
773
+ const capability = {
620
774
  capabilityProfile: response.capabilityProfile,
621
775
  recommendedModelName: response.recommendedModelName,
622
776
  memoryBudget: memoryBudget
@@ -629,6 +783,16 @@ export class RemoteHarness extends EventEmitter {
629
783
  : undefined,
630
784
  mobile: response.mobile === true
631
785
  };
786
+ if (response.completionRecovery === true) {
787
+ capability.completionRecovery = true;
788
+ }
789
+ if (response.hostRunLog === true) {
790
+ capability.hostRunLog = true;
791
+ }
792
+ if (response.hostRunLogVersion && response.hostRunLogVersion > 0) {
793
+ capability.hostRunLogVersion = response.hostRunLogVersion;
794
+ }
795
+ return capability;
632
796
  }
633
797
  close() {
634
798
  this._closed = true;
@@ -719,6 +883,28 @@ function knobs(agent) {
719
883
  sandboxMode: agent.sandboxMode ?? undefined
720
884
  };
721
885
  }
886
+ function explicitEmptyIdentityFields(context) {
887
+ const fields = [];
888
+ if (context.model === '') {
889
+ fields.push('model');
890
+ }
891
+ if (context.cacheKey === '') {
892
+ fields.push('cacheKey');
893
+ }
894
+ if (context.thinkingDefault === '') {
895
+ fields.push('thinkingDefault');
896
+ }
897
+ if (context.agent?.id === '') {
898
+ fields.push('agent.id');
899
+ }
900
+ if (context.agent?.name === '') {
901
+ fields.push('agent.name');
902
+ }
903
+ if (context.agent?.bio === '') {
904
+ fields.push('agent.bio');
905
+ }
906
+ return fields;
907
+ }
722
908
  async function bridgeAttach(event, attach) {
723
909
  try {
724
910
  if (!attach) {
@@ -839,6 +1025,126 @@ function answerQuestion(event, { askUser, reply }) {
839
1025
  .then((answer) => reply({ type: 'question-result', callId, answer: answer ?? '' }), (err) => reply({ type: 'question-result', callId, message: errorMessage(err) }))
840
1026
  .catch((err) => hlog('question reply failed', { callId, message: errorMessage(err) }));
841
1027
  }
1028
+ async function bridgeRunLog({ event, context, runId, generation }) {
1029
+ if (!context.runLog || !generation) {
1030
+ return { type: 'run-log-result', message: 'the host does not own this run log' };
1031
+ }
1032
+ const step = event.payload;
1033
+ if (!isEngineStep(step)) {
1034
+ return { type: 'run-log-result', message: 'the worker sent an invalid run-log step' };
1035
+ }
1036
+ try {
1037
+ await context.runLog.append(runId ?? '', generation, step);
1038
+ return { type: 'run-log-result' };
1039
+ }
1040
+ catch (err) {
1041
+ return { type: 'run-log-result', message: errorMessage(err) };
1042
+ }
1043
+ }
1044
+ function isEngineStep(value) {
1045
+ return (isRecord(value) &&
1046
+ typeof value.seq === 'number' &&
1047
+ Number.isSafeInteger(value.seq) &&
1048
+ value.seq >= 0 &&
1049
+ typeof value.expectedLength === 'number' &&
1050
+ Number.isSafeInteger(value.expectedLength) &&
1051
+ value.expectedLength >= 0 &&
1052
+ Array.isArray(value.records));
1053
+ }
1054
+ function isDigestKey(value) {
1055
+ return value instanceof Uint8Array && value.byteLength === 32;
1056
+ }
1057
+ function releaseLateHostLease(pending, runLog, runId) {
1058
+ void pending
1059
+ .then(async (lease) => {
1060
+ if (!lease || typeof lease.generation !== 'string' || !lease.generation) {
1061
+ throw new Error('host returned an invalid generation for an aborted run-log begin');
1062
+ }
1063
+ await runLog.release(runId, lease.generation);
1064
+ })
1065
+ .catch((err) => hlog('aborted run-log begin cleanup failed', {
1066
+ runId,
1067
+ message: errorMessage(err)
1068
+ }));
1069
+ }
1070
+ function validateHostRunLogLease(lease, runId) {
1071
+ if (typeof lease.generation !== 'string' || !lease.generation || !isDigestKey(lease.digestKey)) {
1072
+ throw new Error('host returned an invalid run-log lease');
1073
+ }
1074
+ if (lease.boot) {
1075
+ validateEngineBoot(lease.boot);
1076
+ }
1077
+ const metadata = lease.boot?.metadata;
1078
+ if (metadata && metadata.runId !== runId) {
1079
+ throw new Error('host returned a run-log boot for a different run');
1080
+ }
1081
+ }
1082
+ function approvalCall(event) {
1083
+ const identity = isRecord(event.payload) ? event.payload : {};
1084
+ const approval = {
1085
+ name: event.name ?? '',
1086
+ arguments: isRecord(event.arguments) ? event.arguments : {},
1087
+ resource: event.resource ?? null,
1088
+ effect: event.effect === 'read' ? 'read' : 'write',
1089
+ intent: event.intent || null,
1090
+ display: isToolCallDisplay(event.display) ? event.display : null
1091
+ };
1092
+ if (typeof identity.callId === 'string') {
1093
+ approval.callId = identity.callId;
1094
+ }
1095
+ if (typeof identity.round === 'number') {
1096
+ approval.round = identity.round;
1097
+ }
1098
+ return approval;
1099
+ }
1100
+ async function acquireHostRunLogLease(runLog, runId, signal) {
1101
+ if (!runId) {
1102
+ throw new Error('host-owned run log requires a run id');
1103
+ }
1104
+ const pending = runLog.begin(runId);
1105
+ try {
1106
+ return await awaitWithAbort(pending, signal);
1107
+ }
1108
+ catch (err) {
1109
+ if (!signal?.aborted) {
1110
+ throw err;
1111
+ }
1112
+ releaseLateHostLease(pending, runLog, runId);
1113
+ return null;
1114
+ }
1115
+ }
1116
+ function awaitWithAbort(promise, signal) {
1117
+ if (!signal)
1118
+ return promise;
1119
+ if (signal.aborted) {
1120
+ return Promise.reject(new Error('operation aborted'));
1121
+ }
1122
+ return new Promise((resolve, reject) => {
1123
+ let settled = false;
1124
+ const cleanup = () => signal.removeEventListener?.('abort', onAbort);
1125
+ const onAbort = () => {
1126
+ if (settled)
1127
+ return;
1128
+ settled = true;
1129
+ cleanup();
1130
+ reject(new Error('operation aborted'));
1131
+ };
1132
+ signal.addEventListener?.('abort', onAbort, { once: true });
1133
+ promise.then((value) => {
1134
+ if (settled)
1135
+ return;
1136
+ settled = true;
1137
+ cleanup();
1138
+ resolve(value);
1139
+ }, (error) => {
1140
+ if (settled)
1141
+ return;
1142
+ settled = true;
1143
+ cleanup();
1144
+ reject(error);
1145
+ });
1146
+ });
1147
+ }
842
1148
  async function bridgeRag(event, context) {
843
1149
  if (!context.ragSearch) {
844
1150
  return {
@@ -13,7 +13,9 @@ import type { ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud
13
13
  import type { DriveOpFn, GrantedDrive } from './tools/fs/remote.ts';
14
14
  import type { ResolvedSkillCatalogEntry } from './tools/skills/catalog.ts';
15
15
  import type { InstallStep, SkillSetup } from './tools/skills/manifest.ts';
16
- export type { InstallStep, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, ResourceDescriptor, SkillSetup, ToolCatalogEntry };
16
+ import type { EngineBoot, EngineStep, HostRunLog, HostRunLogLease, RecoveryResolution, RunRecord } from './run-log-protocol.ts';
17
+ export type { InstallStep, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, ResourceDescriptor, RunRecord, SkillSetup, ToolCatalogEntry };
18
+ export type { HostRunLog, HostRunLogLease };
17
19
  export type ApprovalCall = Parameters<ApproveFn>[0];
18
20
  export interface ChatMessage {
19
21
  role: 'system' | 'user' | 'assistant' | 'tool';
@@ -43,6 +45,9 @@ export interface AgentKnobs {
43
45
  sandboxMode?: boolean | null;
44
46
  }
45
47
  export interface CompletionContext {
48
+ runId?: string;
49
+ resume?: boolean;
50
+ recovery?: RecoveryResolution[];
46
51
  model: string;
47
52
  agent: AgentKnobs;
48
53
  messages: ChatMessage[];
@@ -62,6 +67,11 @@ export interface CompletionContext {
62
67
  ragSearch?: RagSearchFn;
63
68
  progress?: ProgressFn;
64
69
  askUser?: AskUserFn;
70
+ runLog?: HostRunLog;
71
+ boot?: EngineBoot;
72
+ onStep?: (step: EngineStep) => Promise<void>;
73
+ hostAppendTimeoutMs?: number;
74
+ requestDigestKey?: Uint8Array;
65
75
  signal?: {
66
76
  aborted?: boolean;
67
77
  addEventListener?: (type: 'abort', fn: () => void, opts?: {
@@ -70,6 +80,10 @@ export interface CompletionContext {
70
80
  removeEventListener?: (type: 'abort', fn: () => void) => void;
71
81
  };
72
82
  }
83
+ export type ResumeCompletionContext = Omit<Partial<CompletionContext>, 'runId' | 'resume'> & {
84
+ runId: string;
85
+ resume: true;
86
+ };
73
87
  export interface InvokeContext {
74
88
  capability: string;
75
89
  args: Record<string, unknown>;
@@ -172,6 +186,9 @@ export interface HarnessInfo {
172
186
  supportsKvCacheDeletion?: boolean;
173
187
  }
174
188
  export interface CapabilityInfo {
189
+ completionRecovery?: boolean;
190
+ hostRunLog?: boolean;
191
+ hostRunLogVersion?: number;
175
192
  capabilityProfile: HarnessCapabilityProfile;
176
193
  recommendedModelName: string;
177
194
  memoryBudget?: MemoryBudget;
@@ -196,7 +213,7 @@ export interface InfraModel {
196
213
  addon: string;
197
214
  }
198
215
  export interface HarnessContract {
199
- completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
216
+ completion(context: CompletionContext | ResumeCompletionContext): AsyncIterable<HarnessEvent>;
200
217
  invoke?(context: InvokeContext): AsyncIterable<HarnessEvent>;
201
218
  download?(context: DownloadContext): AsyncIterable<HarnessEvent>;
202
219
  describe?(): Promise<HarnessInfo | null> | HarnessInfo;
@@ -0,0 +1,66 @@
1
+ import { type SnapshotFrom } from 'xstate';
2
+ type Request = 'SUSPEND' | 'RESUME' | 'WAKEUP';
3
+ export interface LifecycleEvent {
4
+ type: 'PRESUSPEND' | 'CLOSE' | Request;
5
+ }
6
+ export interface LifecycleWork {
7
+ suspend(): Promise<void>;
8
+ resume(): Promise<void>;
9
+ }
10
+ interface LifecycleContext {
11
+ pending: Request | null;
12
+ failureCount: number;
13
+ failureMessage: string | null;
14
+ }
15
+ export declare const harnessLifecycle: import("xstate").StateMachine<LifecycleContext, LifecycleEvent, {
16
+ [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<void, import("xstate").NonReducibleUnknown, import("xstate").EventObject>> | undefined;
17
+ }, {
18
+ src: "resume";
19
+ logic: import("xstate").PromiseActorLogic<void, import("xstate").NonReducibleUnknown, import("xstate").EventObject>;
20
+ id: string | undefined;
21
+ } | {
22
+ src: "suspend";
23
+ logic: import("xstate").PromiseActorLogic<void, import("xstate").NonReducibleUnknown, import("xstate").EventObject>;
24
+ id: string | undefined;
25
+ }, {
26
+ type: "clear";
27
+ params: import("xstate").NonReducibleUnknown;
28
+ } | {
29
+ type: "hold";
30
+ params: import("xstate").NonReducibleUnknown;
31
+ } | {
32
+ type: "recordError";
33
+ params: import("xstate").NonReducibleUnknown;
34
+ } | {
35
+ type: "report";
36
+ params: unknown;
37
+ }, {
38
+ type: "resumeHeld";
39
+ params: unknown;
40
+ } | {
41
+ type: "suspendHeld";
42
+ params: unknown;
43
+ } | {
44
+ type: "wakeupHeld";
45
+ params: unknown;
46
+ }, never, "presuspended" | "resuming" | "running" | "suspended" | "suspending" | "waking", string, import("xstate").NonReducibleUnknown, import("xstate").NonReducibleUnknown, import("xstate").EventObject, import("xstate").MetaObject, {
47
+ id: "harness";
48
+ states: {
49
+ readonly running: {};
50
+ readonly presuspended: {};
51
+ readonly suspending: {};
52
+ readonly suspended: {};
53
+ readonly resuming: {};
54
+ readonly waking: {};
55
+ };
56
+ }, import("xstate").MetaObject>;
57
+ export interface HarnessLifecycle {
58
+ readonly suspended: boolean;
59
+ readonly state: SnapshotFrom<typeof harnessLifecycle>['value'];
60
+ send(type: LifecycleEvent['type']): Promise<void>;
61
+ settled(): Promise<SnapshotFrom<typeof harnessLifecycle>>;
62
+ cancelPending(): void;
63
+ stop(): void;
64
+ }
65
+ export declare function createHarnessLifecycle(work: LifecycleWork): HarnessLifecycle;
66
+ export {};