@earendil-works/pi-durable 1.3.0 → 1.5.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/CHANGELOG.md +227 -0
- package/README.md +287 -82
- package/dist/codemode/codemode.d.ts +4 -4
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +21 -16
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.d.ts.map +1 -1
- package/dist/codemode/models.js +6 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/documents.d.ts +1 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js.map +1 -1
- package/dist/env/index.d.ts +12 -10
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +3 -3
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js +18 -16
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +2 -2
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +12 -4
- package/dist/env/node.js.map +1 -1
- package/dist/errors.d.ts +18 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -3
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts +13 -2
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +48 -6
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +3 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +38 -6
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +23 -7
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +71 -25
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +7 -3
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js +4 -0
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +38 -5
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +497 -108
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +7 -2
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +147 -57
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +7 -3
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +7 -4
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +20 -4
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js +23 -5
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +122 -16
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +93 -12
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +54 -0
- package/dist/harness/partial-changes.d.ts.map +1 -0
- package/dist/harness/partial-changes.js +162 -0
- package/dist/harness/partial-changes.js.map +1 -0
- package/dist/harness/prompt.d.ts +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/registry-reader.d.ts +11 -0
- package/dist/harness/registry-reader.d.ts.map +1 -0
- package/dist/harness/registry-reader.js +25 -0
- package/dist/harness/registry-reader.js.map +1 -0
- package/dist/harness/scheduler.d.ts +86 -14
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +1459 -369
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +23 -11
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +104 -36
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +28 -8
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +85 -36
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +307 -0
- package/dist/harness/telemetry.d.ts.map +1 -0
- package/dist/harness/telemetry.js +136 -0
- package/dist/harness/telemetry.js.map +1 -0
- package/dist/harness/tool.d.ts +2 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +223 -61
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +72 -0
- package/dist/harness/transcript.d.ts.map +1 -0
- package/dist/harness/transcript.js +264 -0
- package/dist/harness/transcript.js.map +1 -0
- package/dist/harness/types.d.ts +302 -48
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/util.d.ts +7 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +13 -0
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +43 -22
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +130 -71
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +107 -44
- package/dist/search.js.map +1 -1
- package/dist/session/cached-storage.d.ts +71 -0
- package/dist/session/cached-storage.d.ts.map +1 -0
- package/dist/session/cached-storage.js +269 -0
- package/dist/session/cached-storage.js.map +1 -0
- package/dist/session/conversation-tails.d.ts +33 -0
- package/dist/session/conversation-tails.d.ts.map +1 -0
- package/dist/session/conversation-tails.js +101 -0
- package/dist/session/conversation-tails.js.map +1 -0
- package/dist/session/keys.d.ts +68 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +161 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/session.d.ts +181 -29
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1073 -183
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +86 -4
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +250 -38
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +74 -0
- package/dist/session/writer.d.ts.map +1 -0
- package/dist/session/writer.js +294 -0
- package/dist/session/writer.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +68 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +881 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +30 -2
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +88 -24
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +7 -2
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +29 -3
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/cloudflare.d.ts +2 -51
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
- package/dist/storage/sqlite/cloudflare.js +2 -133
- package/dist/storage/sqlite/cloudflare.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +1 -36
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/index.js.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +2 -5
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +28 -28
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts +2 -37
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +3 -174
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/schema.d.ts +1 -3
- package/dist/storage/sqlite/schema.d.ts.map +1 -1
- package/dist/storage/sqlite/schema.js +2 -9
- package/dist/storage/sqlite/schema.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +15 -3
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +110 -36
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +33 -0
- package/dist/storage/store/commit.d.ts.map +1 -0
- package/dist/storage/store/commit.js +286 -0
- package/dist/storage/store/commit.js.map +1 -0
- package/dist/storage/store/documents.d.ts +43 -0
- package/dist/storage/store/documents.d.ts.map +1 -0
- package/dist/storage/store/documents.js +102 -0
- package/dist/storage/store/documents.js.map +1 -0
- package/dist/storage/store/index.d.ts +2 -0
- package/dist/storage/store/index.d.ts.map +1 -0
- package/dist/storage/store/index.js +2 -0
- package/dist/storage/store/index.js.map +1 -0
- package/dist/storage/store/queries.d.ts +49 -0
- package/dist/storage/store/queries.d.ts.map +1 -0
- package/dist/storage/store/queries.js +93 -0
- package/dist/storage/store/queries.js.map +1 -0
- package/dist/storage/store/storage.d.ts +59 -0
- package/dist/storage/store/storage.d.ts.map +1 -0
- package/dist/storage/store/storage.js +438 -0
- package/dist/storage/store/storage.js.map +1 -0
- package/dist/storage/store/tables.d.ts +54 -0
- package/dist/storage/store/tables.d.ts.map +1 -0
- package/dist/storage/store/tables.js +108 -0
- package/dist/storage/store/tables.js.map +1 -0
- package/dist/testing/commit-writes.d.ts +5 -0
- package/dist/testing/commit-writes.d.ts.map +1 -0
- package/dist/testing/commit-writes.js +12 -0
- package/dist/testing/commit-writes.js.map +1 -0
- package/dist/testing/env-conformance.js +7 -7
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +18 -17
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +369 -67
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/timers.d.ts +21 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +72 -0
- package/dist/timers.js.map +1 -0
- package/dist/tool-search/tool-search.d.ts +6 -12
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +7 -7
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +12 -12
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +16 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +7 -7
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -9
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/index.d.ts +32 -32
- package/dist/tools/read.d.ts +6 -6
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +19 -8
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +4 -4
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +5 -5
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +331 -64
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +27 -10
- package/dist/harness/events.d.ts +0 -186
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js +0 -373
- package/dist/harness/events.js.map +0 -1
- package/dist/session/observation.d.ts +0 -42
- package/dist/session/observation.d.ts.map +0 -1
- package/dist/session/observation.js +0 -281
- package/dist/session/observation.js.map +0 -1
package/dist/harness/tool.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { copyJson } from "@earendil-works/chord";
|
|
2
2
|
import { awaitWithContext } from "@earendil-works/chord/context";
|
|
3
|
-
import { overlap } from "@earendil-works/chord/delta";
|
|
3
|
+
import { overlap, ownedSlice } from "@earendil-works/chord/delta";
|
|
4
4
|
import { boundOutput, OutputBuffer, PROGRESS_BYTES_PER_SECOND, Progress, } from "@earendil-works/chord/output";
|
|
5
5
|
import { validateToolArguments } from "@earendil-works/pi-ai/utils/validation";
|
|
6
|
-
import {
|
|
6
|
+
import { compile } from "@earendil-works/pi-schema";
|
|
7
|
+
import { formats } from "@earendil-works/pi-schema/formats";
|
|
7
8
|
import { defineDocFamily } from "../documents.js";
|
|
8
9
|
import { AssistantEntry, ToolResultEntry } from "../entries.js";
|
|
9
10
|
import { errorMessage } from "../errors.js";
|
|
10
11
|
import { defineTask } from "../tasks.js";
|
|
11
12
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, utf8ByteLength } from "../truncate.js";
|
|
13
|
+
import { toolExtension } from "./agent.js";
|
|
12
14
|
import { assignJson } from "./json.js";
|
|
13
|
-
import { clearProgress, finishSlot, LiveDoc, removeNestedSlots, toolSlot, } from "./live.js";
|
|
15
|
+
import { clearProgress, cutOffCount, cutOffTooOften, finishSlot, LiveDoc, PROGRESS_COMMIT, removeNestedSlots, salvageable, toolSlot, } from "./live.js";
|
|
14
16
|
import { recordUsage } from "./usage.js";
|
|
17
|
+
import { optionalExtension } from "./util.js";
|
|
15
18
|
/**
|
|
16
19
|
* Memo of a tool task that made a nested call, in any attempt, committed before the first admission of each attempt:
|
|
17
20
|
* only then can `pi.live` hold slots below it, which settling removes. A crash between the two only costs a scan.
|
|
@@ -28,6 +31,8 @@ export const NestedCallDoc = defineDocFamily({
|
|
|
28
31
|
scope: "task",
|
|
29
32
|
family: true,
|
|
30
33
|
initial: () => ({}),
|
|
34
|
+
// The document retires with its task.
|
|
35
|
+
checkpointWhen: () => false,
|
|
31
36
|
});
|
|
32
37
|
/**
|
|
33
38
|
* Built-in tool task: resolves the called tool among its phase agent's tools, validates, runs `beforeTool`, records intent,
|
|
@@ -37,25 +42,29 @@ export const NestedCallDoc = defineDocFamily({
|
|
|
37
42
|
*/
|
|
38
43
|
export const ToolTask = defineTask({
|
|
39
44
|
name: "pi.tool",
|
|
40
|
-
version:
|
|
45
|
+
version: 3,
|
|
41
46
|
initial: () => ({ phase: "call" }),
|
|
42
|
-
// Version 1 had only model-issued calls.
|
|
43
|
-
migrate: (input, checkpoint) => ({
|
|
44
|
-
input:
|
|
47
|
+
// Version 1 had only model-issued calls, without `kind`; version 3 added `unoffered`, absent in version 2 inputs.
|
|
48
|
+
migrate: (input, checkpoint, fromVersion) => ({
|
|
49
|
+
input: fromVersion === 1
|
|
50
|
+
? { kind: "model", ...input }
|
|
51
|
+
: input,
|
|
45
52
|
checkpoint: checkpoint,
|
|
46
53
|
}),
|
|
47
54
|
phases: {
|
|
48
55
|
call: async (task, runtime, context) => {
|
|
49
56
|
const call = await readCall(runtime, task.input, context);
|
|
50
|
-
|
|
57
|
+
if (cutOffTooOften(task, runtime))
|
|
58
|
+
return endCutOff(runtime, task, call, context);
|
|
59
|
+
const tool = isUnoffered(task.input) ? undefined : await resolveTool(runtime, task.input, call.name, context);
|
|
51
60
|
if (tool === undefined) {
|
|
52
61
|
const error = harnessError("tool_unavailable", `Tool ${call.name} is not available`);
|
|
53
|
-
return
|
|
62
|
+
return reject(runtime, task.input, call, undefined, error, context);
|
|
54
63
|
}
|
|
55
64
|
const prepared = prepare(tool, call.arguments);
|
|
56
65
|
const checked = "error" in prepared ? prepared : validate(tool, call, prepared.args);
|
|
57
66
|
if ("error" in checked)
|
|
58
|
-
return
|
|
67
|
+
return reject(runtime, task.input, call, tool, invalid(checked.error), context);
|
|
59
68
|
let args = checked.args;
|
|
60
69
|
let block;
|
|
61
70
|
await runtime.hooks.each("beforeTool", async (hook) => {
|
|
@@ -76,22 +85,26 @@ export const ToolTask = defineTask({
|
|
|
76
85
|
});
|
|
77
86
|
if (block !== undefined) {
|
|
78
87
|
const blocked = harnessError("blocked", `Tool call blocked: ${block}`);
|
|
79
|
-
return
|
|
88
|
+
return reject(runtime, task.input, call, tool, blocked, context);
|
|
80
89
|
}
|
|
81
90
|
const validated = validate(tool, call, args);
|
|
82
91
|
if ("error" in validated)
|
|
83
|
-
return
|
|
92
|
+
return reject(runtime, task.input, call, tool, invalid(validated.error), context);
|
|
84
93
|
// Strict JSON: repair may set optional properties to undefined, which the slot and checkpoint cannot hold.
|
|
85
94
|
const final = copyJson(validated.args, { omitUndefinedProperties: true });
|
|
95
|
+
// The slot's status and arguments are presentation, stored like progress (`PROGRESS_COMMIT`); the intent is
|
|
96
|
+
// durable before the tool runs. Parallel calls of one round then share no immediate write to `pi.live`.
|
|
86
97
|
await runtime.commit(async (tx) => {
|
|
87
98
|
const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
|
|
88
99
|
if (slot !== undefined)
|
|
89
100
|
slot.status = "running";
|
|
90
101
|
// A nested slot shows the arguments the call runs with, which repair, hooks, and coercion may have changed.
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
if (changed && slot !== undefined && "parentTaskId" in slot)
|
|
102
|
+
// Assigned whole: an equal value records no change, and a rerun corrects what an earlier attempt showed.
|
|
103
|
+
if (slot !== undefined && "parentTaskId" in slot)
|
|
94
104
|
slot.arguments = final;
|
|
105
|
+
return undefined;
|
|
106
|
+
}, context, PROGRESS_COMMIT);
|
|
107
|
+
await runtime.commit(() => {
|
|
95
108
|
const intent = { phase: "execute", arguments: final, replay: tool.replay ?? "unsafe" };
|
|
96
109
|
return { status: "running", checkpoint: intent };
|
|
97
110
|
}, context);
|
|
@@ -101,13 +114,21 @@ export const ToolTask = defineTask({
|
|
|
101
114
|
execute: async (task, runtime, context) => {
|
|
102
115
|
const { arguments: args, replay } = task.state.checkpoint;
|
|
103
116
|
const call = await readCall(runtime, task.input, context);
|
|
117
|
+
// Also before an interruption result: its `afterTool` hooks may be what cut the task off.
|
|
118
|
+
if (cutOffTooOften(task, runtime))
|
|
119
|
+
return endCutOff(runtime, task, call, context);
|
|
104
120
|
const tool = await resolveTool(runtime, task.input, call.name, context);
|
|
105
121
|
if (replay === "safe" && tool?.replay === "safe") {
|
|
106
|
-
// The rerun reports from scratch
|
|
122
|
+
// The rerun reports from scratch: clear what the interrupted attempt published, and show the slot running with
|
|
123
|
+
// the arguments it runs with again, as the deferred commit that showed them may have been lost with the crash.
|
|
107
124
|
await runtime.commit(async (tx) => {
|
|
108
125
|
const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
|
|
109
|
-
if (slot !== undefined)
|
|
126
|
+
if (slot !== undefined) {
|
|
110
127
|
clearProgress(slot);
|
|
128
|
+
slot.status = "running";
|
|
129
|
+
if ("parentTaskId" in slot)
|
|
130
|
+
slot.arguments = args;
|
|
131
|
+
}
|
|
111
132
|
return undefined;
|
|
112
133
|
}, context);
|
|
113
134
|
return run(runtime, task.input, call, tool, args, context);
|
|
@@ -115,30 +136,101 @@ export const ToolTask = defineTask({
|
|
|
115
136
|
const message = `Tool ${call.name} was interrupted and may have partially run`;
|
|
116
137
|
// `failed` records cancellation intent, so the call's owned conversations, left unsupervised, are aborted.
|
|
117
138
|
const ending = { status: "failed", message };
|
|
118
|
-
|
|
139
|
+
// Not cut off too often (checked above), so the hooks run.
|
|
140
|
+
await settleFromSlot(runtime, task.input, call, { tool }, ending, "interrupted", message, context);
|
|
119
141
|
},
|
|
120
142
|
},
|
|
121
143
|
abort: async (task, runtime, context) => {
|
|
122
144
|
const call = await readCall(runtime, task.input, context);
|
|
123
|
-
// Abandoned after a restart (`TaskOptions.abandonOnRestart`): say whether the call may have run.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
// Abandoned after a restart (`TaskOptions.abandonOnRestart`): say whether the call may have run. An unoffered call
|
|
146
|
+
// could not run in any case, and its result says so.
|
|
147
|
+
const [code, message] = isUnoffered(task.input)
|
|
148
|
+
? ["tool_unavailable", `Tool ${call.name} is not available`]
|
|
149
|
+
: task.abortReason !== "restart"
|
|
150
|
+
? ["aborted", `Tool ${call.name} was aborted`]
|
|
151
|
+
: task.state.checkpoint.phase === "execute"
|
|
152
|
+
? ["interrupted", `Tool ${call.name} was interrupted by a restart and may have partially run`]
|
|
153
|
+
: ["abandoned", `Tool ${call.name} was not started: its caller ended with a restart`];
|
|
154
|
+
// The scheduler runs an abort handler only while its restarts are below the limit, and faults the task without a
|
|
155
|
+
// result when the last attempt is cut off. That attempt skips the hooks, which may be what cuts it off.
|
|
156
|
+
const hooks = (task.restarts ?? 0) < runtime.settings.restartLimit - 1
|
|
157
|
+
? { tool: await toolForLimits(runtime, task.input, call.name, context) }
|
|
158
|
+
: undefined;
|
|
159
|
+
await settleFromSlot(runtime, task.input, call, hooks, { status: "aborted" }, code, message, context);
|
|
131
160
|
},
|
|
132
161
|
});
|
|
162
|
+
/**
|
|
163
|
+
* End a call cut off `settings.restartLimit` times in a row (spec §5.4) with an error result, built from the slot like
|
|
164
|
+
* an interrupted call's, instead of running it again. `failed`, like an interruption, so its owned work is aborted.
|
|
165
|
+
*/
|
|
166
|
+
async function endCutOff(runtime, task, call, context) {
|
|
167
|
+
const message = `Tool ${call.name} was interrupted ${cutOffCount(task)}; it is not run again`;
|
|
168
|
+
const ending = { status: "failed", message, reason: "restart_limit" };
|
|
169
|
+
// Past the limit the hooks may be what keeps cutting the task off (`salvageable`).
|
|
170
|
+
const hooks = salvageable(task, runtime)
|
|
171
|
+
? { tool: await toolForLimits(runtime, task.input, call.name, context) }
|
|
172
|
+
: undefined;
|
|
173
|
+
await settleFromSlot(runtime, task.input, call, hooks, ending, "restart_limit", message, context);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Settle a result the Harness decided in the `call` phase, without running the tool: the tool did not resolve, the
|
|
177
|
+
* arguments are invalid, or `beforeTool` blocked the call. `afterTool` applies as to an executed call's result.
|
|
178
|
+
*/
|
|
179
|
+
async function reject(runtime, input, call, tool, result, context) {
|
|
180
|
+
const settled = await afterHarnessResult(runtime, input, call, tool, result, undefined, context);
|
|
181
|
+
await settle(runtime, input, call, COMPLETED, settled, context);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Settle an error result built from the call's slot (`fromSlot`): an interruption, the restart limit, or an abort.
|
|
185
|
+
* The tool is not running, so the slot read before the commit is still current when it commits. With `hooks`, the
|
|
186
|
+
* result passes `afterTool` and is bounded with the limits of `hooks.tool` (the defaults when it is `undefined`);
|
|
187
|
+
* without, it is settled as built, when the hooks may be what cuts the task off.
|
|
188
|
+
*/
|
|
189
|
+
async function settleFromSlot(runtime, input, call, hooks, ending, code, message, context) {
|
|
190
|
+
const live = await runtime.snapshot(LiveDoc, runtime.conversationId, context);
|
|
191
|
+
const { result, truncation } = fromSlot(live === undefined ? undefined : toolSlot(live, runtime.taskId), code, message);
|
|
192
|
+
const settled = hooks !== undefined
|
|
193
|
+
? await afterHarnessResult(runtime, input, call, hooks.tool, result, truncation, context)
|
|
194
|
+
: truncation === undefined
|
|
195
|
+
? result
|
|
196
|
+
: { ...result, diagnostics: [...(result.diagnostics ?? []), truncation] };
|
|
197
|
+
await settle(runtime, input, call, ending, settled, context);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The tool whose output limits bound a hooks' result: the called tool, or none when it, or the agent, fails to resolve,
|
|
201
|
+
* and for an unoffered call, which never resolves its name.
|
|
202
|
+
*/
|
|
203
|
+
async function toolForLimits(runtime, input, name, context) {
|
|
204
|
+
if (isUnoffered(input))
|
|
205
|
+
return undefined;
|
|
206
|
+
return resolveTool(runtime, input, name, context).catch(() => undefined);
|
|
207
|
+
}
|
|
208
|
+
/** A model-issued call to a tool its request did not offer (spec §8.3). */
|
|
209
|
+
function isUnoffered(input) {
|
|
210
|
+
return input.kind === "model" && input.unoffered === true;
|
|
211
|
+
}
|
|
133
212
|
/** The called tool: among the tools the model is offered for a model-issued call, else among those tools may call. */
|
|
134
213
|
async function resolveTool(runtime, input, name, context) {
|
|
135
214
|
const agent = await runtime.agent(context);
|
|
136
215
|
return (input.kind === "model" ? agent.tools : agent.callable).find((tool) => tool.name === name);
|
|
137
216
|
}
|
|
217
|
+
/** The tool's output limits, the defaults filled in; all defaults when the tool did not resolve. */
|
|
218
|
+
function outputLimits(tool) {
|
|
219
|
+
return {
|
|
220
|
+
maxBytes: tool?.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
221
|
+
maxLines: tool?.outputLimits?.maxLines ?? DEFAULT_MAX_LINES,
|
|
222
|
+
retain: tool?.outputLimits?.retain ?? "head",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
138
225
|
/** The call: from the input for a nested call, which hooks see with its parent, else from the assistant entry. */
|
|
139
226
|
async function readCall(runtime, input, context) {
|
|
140
|
-
if (input.kind === "nested")
|
|
227
|
+
if (input.kind === "nested") {
|
|
228
|
+
context.telemetry.setAttributes({
|
|
229
|
+
"tool.name": input.call.name,
|
|
230
|
+
"tool.call_id": input.call.id,
|
|
231
|
+
});
|
|
141
232
|
return { ...input.call, parent: { taskId: input.parent, callId: input.parentCallId } };
|
|
233
|
+
}
|
|
142
234
|
const entry = await runtime.entry(AssistantEntry, input.assistant, context);
|
|
143
235
|
const message = entry?.model?.[0];
|
|
144
236
|
const call = message?.role === "assistant"
|
|
@@ -146,6 +238,10 @@ async function readCall(runtime, input, context) {
|
|
|
146
238
|
: undefined;
|
|
147
239
|
if (call === undefined)
|
|
148
240
|
throw new Error(`Entry ${input.assistant} has no tool call ${input.callId}`);
|
|
241
|
+
context.telemetry.setAttributes({
|
|
242
|
+
"tool.name": call.name,
|
|
243
|
+
"tool.call_id": call.id,
|
|
244
|
+
});
|
|
149
245
|
return call;
|
|
150
246
|
}
|
|
151
247
|
/** The call's arguments as repaired by the tool; a throwing repair makes them invalid. */
|
|
@@ -173,11 +269,7 @@ function invalid(message) {
|
|
|
173
269
|
}
|
|
174
270
|
/** Execute with the resolved implementation, then settle its result. */
|
|
175
271
|
async function run(runtime, input, call, tool, args, context) {
|
|
176
|
-
const limits =
|
|
177
|
-
maxBytes: tool.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
178
|
-
maxLines: tool.outputLimits?.maxLines ?? DEFAULT_MAX_LINES,
|
|
179
|
-
retain: tool.outputLimits?.retain ?? "head",
|
|
180
|
-
};
|
|
272
|
+
const limits = outputLimits(tool);
|
|
181
273
|
const reported = { output: new OutputBuffer(limits), limits, diagnostics: [], details: undefined };
|
|
182
274
|
const streams = input.kind === "model" || input.progress !== false;
|
|
183
275
|
const progress = publishProgress(runtime, reported, streams, context);
|
|
@@ -195,13 +287,21 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
195
287
|
if (ended)
|
|
196
288
|
throw new Error(`Tool call ${call.id} has settled`);
|
|
197
289
|
};
|
|
290
|
+
/** Built for this call, below, so a rerun after recovery gets the conversation's environment at that time. */
|
|
291
|
+
let env;
|
|
198
292
|
const api = {
|
|
199
293
|
taskId: runtime.taskId,
|
|
200
294
|
conversationId: runtime.conversationId,
|
|
201
295
|
callId: call.id,
|
|
202
296
|
registry: runtime.registry,
|
|
203
297
|
agent: runtime.agent,
|
|
204
|
-
models
|
|
298
|
+
get models() {
|
|
299
|
+
return runtime.models;
|
|
300
|
+
},
|
|
301
|
+
get env() {
|
|
302
|
+
return env;
|
|
303
|
+
},
|
|
304
|
+
now: () => runtime.now(),
|
|
205
305
|
output: (chunk, skipped) => {
|
|
206
306
|
assertLive();
|
|
207
307
|
if (reported.output.push(chunk, skipped))
|
|
@@ -234,12 +334,12 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
234
334
|
committed.catch(() => { });
|
|
235
335
|
return awaitWithContext(committed, detailsContext);
|
|
236
336
|
},
|
|
237
|
-
commit: async (change, commitContext) => {
|
|
337
|
+
commit: async (change, commitContext, options) => {
|
|
238
338
|
let result;
|
|
239
339
|
await runtime.commit(async (tx) => {
|
|
240
340
|
result = await change(tx);
|
|
241
341
|
return undefined;
|
|
242
|
-
}, commitContext);
|
|
342
|
+
}, commitContext, options);
|
|
243
343
|
return result;
|
|
244
344
|
},
|
|
245
345
|
memo: runtime.memo,
|
|
@@ -285,11 +385,10 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
285
385
|
// Execution time of this attempt; a rerun after recovery measures only itself.
|
|
286
386
|
let durationMs;
|
|
287
387
|
try {
|
|
288
|
-
|
|
289
|
-
const env = await runtime.env(context);
|
|
388
|
+
env = await runtime.env(context);
|
|
290
389
|
const startedAt = performance.now();
|
|
291
390
|
try {
|
|
292
|
-
result = await tool.execute(args,
|
|
391
|
+
result = await tool.execute(args, api, context);
|
|
293
392
|
}
|
|
294
393
|
finally {
|
|
295
394
|
durationMs = Math.round(performance.now() - startedAt);
|
|
@@ -315,7 +414,7 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
315
414
|
const pending = await progress.stop();
|
|
316
415
|
try {
|
|
317
416
|
const settled = await finalResult(runtime, input, call, tool, result, reported, context);
|
|
318
|
-
await settle(runtime, input, call, ending,
|
|
417
|
+
await settle(runtime, input, call, ending, settled, context, durationMs);
|
|
319
418
|
}
|
|
320
419
|
catch (error) {
|
|
321
420
|
for (const waiter of pending)
|
|
@@ -325,9 +424,28 @@ async function run(runtime, input, call, tool, args, context) {
|
|
|
325
424
|
for (const waiter of pending)
|
|
326
425
|
waiter.resolve();
|
|
327
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* The Harness clock and timers for progress pacing. After the invocation ends nothing more commits, and the clock
|
|
429
|
+
* answers with the last time it read.
|
|
430
|
+
*/
|
|
431
|
+
function progressClock(runtime) {
|
|
432
|
+
let last = 0;
|
|
433
|
+
return {
|
|
434
|
+
now: () => {
|
|
435
|
+
try {
|
|
436
|
+
last = runtime.now();
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// The invocation ended.
|
|
440
|
+
}
|
|
441
|
+
return last;
|
|
442
|
+
},
|
|
443
|
+
setTimer: (callback, delayMs) => runtime.setTimer(callback, delayMs),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
328
446
|
/**
|
|
329
447
|
* Throttled commits of what the tool reported into its slot, in `pi.live.tools` or `pi.live.nestedTools`, each writing
|
|
330
|
-
* only what changed since the last one; none when `enabled` is false.
|
|
448
|
+
* only what changed since the last one, deferred (`PROGRESS_COMMIT`); none when `enabled` is false.
|
|
331
449
|
*/
|
|
332
450
|
function publishProgress(runtime, reported, enabled, context) {
|
|
333
451
|
let written = { text: "", details: undefined, diagnostics: 0 };
|
|
@@ -375,14 +493,14 @@ function publishProgress(runtime, reported, enabled, context) {
|
|
|
375
493
|
slot.diagnostics.push(diagnostic);
|
|
376
494
|
}
|
|
377
495
|
return undefined;
|
|
378
|
-
}, context);
|
|
496
|
+
}, context, PROGRESS_COMMIT);
|
|
379
497
|
written = current;
|
|
380
498
|
return bytes;
|
|
381
499
|
}, (error) => {
|
|
382
500
|
// Rejections after an abort mark or close are expected; the committed state stays consistent.
|
|
383
501
|
if (!runtime.signal.aborted)
|
|
384
502
|
runtime.report(error);
|
|
385
|
-
}, runtime.settings.progress.outputIntervalMs);
|
|
503
|
+
}, runtime.settings.progress.outputIntervalMs, progressClock(runtime));
|
|
386
504
|
}
|
|
387
505
|
/**
|
|
388
506
|
* The settled result: the tool's result with the retained output and last details as fallbacks, its diagnostics after
|
|
@@ -420,7 +538,9 @@ async function finalResult(runtime, input, call, tool, result, reported, context
|
|
|
420
538
|
else {
|
|
421
539
|
// The model never sees structured output, so its call stands; the host learns of the broken tool.
|
|
422
540
|
final = rest;
|
|
423
|
-
|
|
541
|
+
// Only names the extension: a failed resolution leaves the report without the name.
|
|
542
|
+
const extension = await runtime.agent(context).then((agent) => toolExtension(agent, tool.name), () => undefined);
|
|
543
|
+
runtime.report(new Error(broken), { kind: "extension", ...optionalExtension(extension) });
|
|
424
544
|
}
|
|
425
545
|
}
|
|
426
546
|
// Programs get the tool's own value: without a schema, its output. The model reads only `output`, so model-issued
|
|
@@ -441,6 +561,33 @@ async function finalResult(runtime, input, call, tool, result, reported, context
|
|
|
441
561
|
}
|
|
442
562
|
return { ...final, output: bounded.content, diagnostics: [...(final.diagnostics ?? []), ...harness] };
|
|
443
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* A result the Harness wrote itself, without executing the tool, after the `afterTool` chain, normalized like an
|
|
566
|
+
* executed call's: the output bounded with the tool's limits (the defaults when it did not resolve), a truncation
|
|
567
|
+
* diagnostic when that dropped something, and the hooks' `structuredOutput` dropped, since the tool never produced
|
|
568
|
+
* one. A nested call that the hooks turned into a success gets the bounded output as its value, as a schema-less
|
|
569
|
+
* tool's success does, unless the tool declares a schema. `usage` is kept and recorded; `control` applies only when
|
|
570
|
+
* the task completes. `truncation` describes what the result's own output lost before (a slot's retained output); it
|
|
571
|
+
* is kept only while the hooks keep that output.
|
|
572
|
+
*/
|
|
573
|
+
async function afterHarnessResult(runtime, input, call, tool, result, truncation, context) {
|
|
574
|
+
let final = result;
|
|
575
|
+
await runtime.hooks.each("afterTool", async (hook) => {
|
|
576
|
+
final = (await hook(call, final, runtime, context)) ?? final;
|
|
577
|
+
});
|
|
578
|
+
const harness = [];
|
|
579
|
+
if (truncation !== undefined && final.output === result.output)
|
|
580
|
+
harness.push(truncation);
|
|
581
|
+
const limits = outputLimits(tool);
|
|
582
|
+
const bounded = boundContent(final.output ?? [], limits);
|
|
583
|
+
if (bounded.droppedBytes > 0)
|
|
584
|
+
harness.push(truncated(bounded, limits.retain));
|
|
585
|
+
const { structuredOutput: _dropped, ...rest } = final;
|
|
586
|
+
const value = input.kind === "nested" && final.isError !== true && tool?.structuredOutputSchema === undefined
|
|
587
|
+
? { structuredOutput: outputValue(bounded.content) }
|
|
588
|
+
: {};
|
|
589
|
+
return { ...rest, ...value, output: bounded.content, diagnostics: [...(rest.diagnostics ?? []), ...harness] };
|
|
590
|
+
}
|
|
444
591
|
const structuredValidators = new WeakMap();
|
|
445
592
|
/** Why a result's `structuredOutput` breaks the tool's contract, or `undefined` when it keeps it. */
|
|
446
593
|
function structuredOutputError(tool, result) {
|
|
@@ -456,23 +603,26 @@ function structuredOutputError(tool, result) {
|
|
|
456
603
|
}
|
|
457
604
|
let validator = structuredValidators.get(schema);
|
|
458
605
|
if (validator === undefined) {
|
|
459
|
-
validator =
|
|
606
|
+
validator = compile(schema, { formats, unresolvedRefs: "reject" });
|
|
460
607
|
structuredValidators.set(schema, validator);
|
|
461
608
|
}
|
|
462
|
-
if (validator.
|
|
609
|
+
if (validator.check(value))
|
|
463
610
|
return undefined;
|
|
464
|
-
const [first] = validator.
|
|
465
|
-
const path = first?.
|
|
611
|
+
const [first] = validator.errors(value);
|
|
612
|
+
const path = first?.path
|
|
613
|
+
.slice(1)
|
|
614
|
+
.split("/")
|
|
615
|
+
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
616
|
+
.join(".") || "root";
|
|
466
617
|
return `Tool ${tool.name} returned structuredOutput that does not match its schema: ${path}: ${first?.message ?? "invalid"}`;
|
|
467
618
|
}
|
|
468
619
|
/**
|
|
469
620
|
* Commit the tool's terminal state and mark its slot done. A model-issued call appends its result entry and ends with
|
|
470
621
|
* the entry ID; a nested call writes its result to the caller's `NestedCallDoc`, records its usage, and ends with a
|
|
471
622
|
* small receipt. Nested calls this call left running are aborted first, so they report into their slots before the
|
|
472
|
-
* slots below the call leave `pi.live`. `
|
|
473
|
-
* partial output.
|
|
623
|
+
* slots below the call leave `pi.live`. `control` applies only when the task completes.
|
|
474
624
|
*/
|
|
475
|
-
async function settle(runtime, input, call, ending,
|
|
625
|
+
async function settle(runtime, input, call, ending, result, context, durationMs) {
|
|
476
626
|
// A crash in between leaves the call in `execute`: a safe rerun reattaches to the calls, an interruption ends here.
|
|
477
627
|
// Every live nested call the task owns, also those an earlier attempt made and this one never called again.
|
|
478
628
|
const nestedIds = (await runtime.ownedTasks(context))
|
|
@@ -486,7 +636,6 @@ async function settle(runtime, input, call, ending, build, context, durationMs)
|
|
|
486
636
|
const conversationId = runtime.conversationId;
|
|
487
637
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
488
638
|
const slot = toolSlot(live, runtime.taskId);
|
|
489
|
-
const result = build(slot);
|
|
490
639
|
let settled;
|
|
491
640
|
if (input.kind === "nested") {
|
|
492
641
|
const nested = nestedResult(runtime.taskId, result, durationMs);
|
|
@@ -524,7 +673,10 @@ async function settle(runtime, input, call, ending, build, context, durationMs)
|
|
|
524
673
|
if (ending.status === "aborted")
|
|
525
674
|
return { status: "terminal", outcome: { status: "aborted", result: settled } };
|
|
526
675
|
if (ending.status === "failed") {
|
|
527
|
-
const error = {
|
|
676
|
+
const error = {
|
|
677
|
+
message: ending.message,
|
|
678
|
+
...(ending.reason === undefined ? {} : { detail: { reason: ending.reason } }),
|
|
679
|
+
};
|
|
528
680
|
return { status: "terminal", outcome: { status: "failed", error, result: settled } };
|
|
529
681
|
}
|
|
530
682
|
return { status: "terminal", outcome: { status: "completed", result: settled } };
|
|
@@ -561,6 +713,7 @@ async function startNestedCall(runtime, parentCallId, name, args, options, conte
|
|
|
561
713
|
arguments: copyJson(args, { omitUndefinedProperties: true }),
|
|
562
714
|
};
|
|
563
715
|
let id;
|
|
716
|
+
let replayed = false;
|
|
564
717
|
await runtime.commit(async (tx) => {
|
|
565
718
|
// Opened, or created empty for a key not made yet.
|
|
566
719
|
const state = await tx.doc(NestedCallDoc, runtime.taskId, key, null);
|
|
@@ -573,6 +726,7 @@ async function startNestedCall(runtime, parentCallId, name, args, options, conte
|
|
|
573
726
|
if (!same)
|
|
574
727
|
throw new Error(`Nested call ${call.id} was already made with another tool or other arguments`);
|
|
575
728
|
id = existing;
|
|
729
|
+
replayed = state.result !== undefined;
|
|
576
730
|
return undefined;
|
|
577
731
|
}
|
|
578
732
|
const input = {
|
|
@@ -603,6 +757,9 @@ async function startNestedCall(runtime, parentCallId, name, args, options, conte
|
|
|
603
757
|
id = created;
|
|
604
758
|
return undefined;
|
|
605
759
|
}, context);
|
|
760
|
+
if (replayed) {
|
|
761
|
+
context.telemetry.addEvent("pi.durable.replay", { "tool.name": name });
|
|
762
|
+
}
|
|
606
763
|
return id;
|
|
607
764
|
}
|
|
608
765
|
/**
|
|
@@ -628,19 +785,21 @@ function fallbackResult(taskId, name, outcome) {
|
|
|
628
785
|
return { taskId, isError: true, diagnostics: error.diagnostics ?? [] };
|
|
629
786
|
}
|
|
630
787
|
const COMPLETED = { status: "completed" };
|
|
631
|
-
/**
|
|
788
|
+
/**
|
|
789
|
+
* An error result from the slot's durable partial output, details, and diagnostics, ending with `code`; and the
|
|
790
|
+
* truncation diagnostic of that output, when its bounds dropped some, which the caller adds after `afterTool`.
|
|
791
|
+
*/
|
|
632
792
|
function fromSlot(slot, code, message) {
|
|
633
|
-
const diagnostics = [...(slot?.diagnostics ?? [])];
|
|
634
793
|
const droppedBytes = slot?.droppedBytes ?? 0;
|
|
635
|
-
|
|
636
|
-
diagnostics.push(truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }));
|
|
637
|
-
diagnostics.push(toolDiagnostic(code, message));
|
|
638
|
-
return {
|
|
794
|
+
const result = {
|
|
639
795
|
output: slot?.output === undefined || slot.output === "" ? [] : [{ type: "text", text: slot.output }],
|
|
640
796
|
isError: true,
|
|
641
797
|
...(slot?.details === undefined ? {} : { details: slot.details }),
|
|
642
|
-
diagnostics,
|
|
798
|
+
diagnostics: [...(slot?.diagnostics ?? []), toolDiagnostic(code, message)],
|
|
643
799
|
};
|
|
800
|
+
if (droppedBytes === 0)
|
|
801
|
+
return { result };
|
|
802
|
+
return { result, truncation: truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }) };
|
|
644
803
|
}
|
|
645
804
|
/** An error result the Harness writes itself: no content and one `error` diagnostic with `code`. */
|
|
646
805
|
export function harnessError(code, message) {
|
|
@@ -660,9 +819,12 @@ function truncated(dropped, retain) {
|
|
|
660
819
|
}
|
|
661
820
|
/** Error text a nested call's summary keeps. */
|
|
662
821
|
const MAX_SUMMARY_ERROR_CHARS = 500;
|
|
663
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* How a nested call ended, for its slot: error text, from its diagnostics or else its `output`, bounded. The bound is a
|
|
824
|
+
* copy, so the slot never keeps a large output alive through a slice of it.
|
|
825
|
+
*/
|
|
664
826
|
function summaryOf(result, output) {
|
|
665
|
-
const error = result.isError ? errorTextOf(result, output)
|
|
827
|
+
const error = result.isError ? ownedSlice(errorTextOf(result, output), 0, MAX_SUMMARY_ERROR_CHARS) : "";
|
|
666
828
|
return {
|
|
667
829
|
isError: result.isError,
|
|
668
830
|
...(result.durationMs === undefined ? {} : { durationMs: result.durationMs }),
|