@inflexa-ai/harness 0.20.0 → 0.21.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.
- package/dist/agents/conversation-agent.d.ts +26 -14
- package/dist/agents/conversation-agent.js +12 -23
- package/dist/agents/report-session-agent.d.ts +31 -5
- package/dist/agents/report-session-agent.js +35 -9
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/message-assembly.d.ts +15 -1
- package/dist/app/message-assembly.js +22 -8
- package/dist/app/report-session-runtime.d.ts +21 -4
- package/dist/app/report-session-runtime.js +53 -6
- package/dist/app/spawn-report-session.d.ts +174 -17
- package/dist/app/spawn-report-session.js +233 -21
- package/dist/contracts/chat-events.d.ts +7 -2
- package/dist/contracts/message.d.ts +10 -0
- package/dist/contracts/report-blocks.d.ts +743 -19
- package/dist/contracts/report-blocks.js +186 -11
- package/dist/contracts/report-reference.d.ts +88 -0
- package/dist/contracts/report-reference.js +48 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/lib/chrome.d.ts +26 -6
- package/dist/lib/chrome.js +99 -32
- package/dist/lib/eyes.d.ts +57 -0
- package/dist/lib/eyes.js +45 -0
- package/dist/lib/page-capture.d.ts +14 -0
- package/dist/lib/page-capture.js +56 -2
- package/dist/loop/run-agent.js +58 -23
- package/dist/loop/tool-detail.d.ts +19 -2
- package/dist/loop/tool-detail.js +35 -2
- package/dist/memory/content-to-cortex.d.ts +3 -2
- package/dist/memory/content-to-cortex.js +21 -12
- package/dist/memory/conversation-display-replay.js +14 -7
- package/dist/memory/thread-history.d.ts +60 -3
- package/dist/memory/thread-history.js +55 -16
- package/dist/memory/thread-store.d.ts +11 -1
- package/dist/memory/thread-store.js +12 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +23 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +190 -9
- package/dist/report-model/authoring-grammar.d.ts +463 -0
- package/dist/report-model/authoring-grammar.js +77 -0
- package/dist/report-model/block-walk.d.ts +57 -14
- package/dist/report-model/block-walk.js +116 -11
- package/dist/report-model/draft-finish.d.ts +22 -4
- package/dist/report-model/draft-finish.js +39 -7
- package/dist/report-model/draft-operations.d.ts +3 -0
- package/dist/report-model/draft-operations.js +19 -5
- package/dist/report-model/draft-read.d.ts +8 -5
- package/dist/report-model/draft-read.js +21 -5
- package/dist/report-model/fixture-resolver.js +15 -4
- package/dist/report-model/pin-snapshot.d.ts +24 -6
- package/dist/report-model/pin-snapshot.js +204 -6
- package/dist/report-model/production-resolver.js +15 -4
- package/dist/report-model/reference-resolver.d.ts +50 -1
- package/dist/report-model/reference-resolver.js +94 -1
- package/dist/report-model/reference-stamp.d.ts +45 -0
- package/dist/report-model/reference-stamp.js +173 -0
- package/dist/report-model/structural-validation.d.ts +4 -1
- package/dist/report-model/structural-validation.js +48 -3
- package/dist/report-model/validate.d.ts +1 -1
- package/dist/report-model/validate.js +1 -1
- package/dist/report-render/asset-lookup.d.ts +19 -0
- package/dist/report-render/asset-lookup.js +37 -0
- package/dist/report-render/assets.d.ts +45 -1
- package/dist/report-render/assets.js +65 -6
- package/dist/report-render/chart-presets.d.ts +102 -0
- package/dist/report-render/chart-presets.js +205 -0
- package/dist/report-render/chart.d.ts +112 -10
- package/dist/report-render/chart.js +1164 -41
- package/dist/report-render/design.d.ts +99 -0
- package/dist/report-render/design.js +289 -107
- package/dist/report-render/number-format.d.ts +117 -0
- package/dist/report-render/number-format.js +479 -0
- package/dist/report-render/page.d.ts +121 -5
- package/dist/report-render/page.js +689 -7
- package/dist/report-render/references.d.ts +70 -16
- package/dist/report-render/references.js +54 -22
- package/dist/report-render/render.d.ts +23 -5
- package/dist/report-render/render.js +81 -22
- package/dist/report-render/table-data.d.ts +92 -0
- package/dist/report-render/table-data.js +129 -0
- package/dist/report-render/types.d.ts +15 -0
- package/dist/report-render/value-bridge.js +6 -2
- package/dist/report-render/views/chart-view.d.ts +12 -5
- package/dist/report-render/views/chart-view.js +14 -8
- package/dist/report-render/views/page-view.d.ts +36 -15
- package/dist/report-render/views/page-view.js +45 -23
- package/dist/report-render/views/prose.d.ts +12 -2
- package/dist/report-render/views/prose.js +29 -7
- package/dist/report-render/views/references-view.d.ts +22 -8
- package/dist/report-render/views/references-view.js +101 -13
- package/dist/report-render/views/values.d.ts +128 -13
- package/dist/report-render/views/values.js +199 -18
- package/dist/runtime/assemble.d.ts +39 -5
- package/dist/runtime/assemble.js +47 -6
- package/dist/sandbox/client.d.ts +10 -1
- package/dist/sandbox/create-sandbox.d.ts +9 -4
- package/dist/sandbox/create-sandbox.js +22 -16
- package/dist/sandbox/docker-client.js +10 -7
- package/dist/sandbox/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/state/init.js +38 -9
- package/dist/state/report-session-state.d.ts +42 -1
- package/dist/state/report-session-state.js +70 -2
- package/dist/state/report-versions.d.ts +22 -17
- package/dist/state/report-versions.js +48 -35
- package/dist/state/snapshot-parse.js +16 -1
- package/dist/tools/define-tool.d.ts +32 -0
- package/dist/tools/define-tool.js +4 -0
- package/dist/tools/detail-resolver.d.ts +7 -2
- package/dist/tools/detail-resolver.js +7 -2
- package/dist/tools/iterate-report.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
- package/dist/tools/report-authoring/authoring-tools.js +99 -18
- package/dist/tools/report-session/derive-table.d.ts +162 -0
- package/dist/tools/report-session/derive-table.js +459 -0
- package/dist/tools/report-session/examine-page.d.ts +48 -18
- package/dist/tools/report-session/examine-page.js +204 -24
- package/dist/tools/report-session/index.d.ts +2 -0
- package/dist/tools/report-session/index.js +2 -0
- package/dist/tools/report-session/list-artifacts.d.ts +97 -0
- package/dist/tools/report-session/list-artifacts.js +197 -0
- package/dist/tools/report-session/preview-report.d.ts +6 -0
- package/dist/tools/report-session/preview-report.js +180 -28
- package/dist/tools/report-session/record-version.d.ts +22 -10
- package/dist/tools/report-session/record-version.js +99 -14
- package/dist/tools/start-report-session.d.ts +115 -0
- package/dist/tools/start-report-session.js +179 -0
- package/dist/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
package/dist/lib/chrome.js
CHANGED
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Chrome sidecar reachable at `CHROME_BROWSER_URL`. Used by the iterate-report
|
|
4
|
-
* preview-snapshot tool for visual validation.
|
|
2
|
+
* The connection to a headless Chrome endpoint.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two callers name an endpoint. A composition names a standing sidecar at assembly, and a lease of the eyes
|
|
5
|
+
* seam names a browser for one look. Thus the module holds one connection for each endpoint, and it holds no
|
|
6
|
+
* connection for the process.
|
|
7
|
+
*
|
|
8
|
+
* A connection is lazy, and an endpoint connects again after a disconnect. Each endpoint also carries its own
|
|
9
|
+
* page gate. The gate bounds the pages that run at the same time against that one sidecar.
|
|
8
10
|
*/
|
|
9
11
|
import puppeteer from "puppeteer-core";
|
|
10
12
|
import { createNoopLogger } from "./console-logger.js";
|
|
13
|
+
/** Whether a value names an endpoint. An absent value and a blank value each name none. */
|
|
14
|
+
function namesEndpoint(browserUrl) {
|
|
15
|
+
return browserUrl !== undefined && browserUrl.trim().length > 0;
|
|
16
|
+
}
|
|
11
17
|
/**
|
|
12
18
|
* Whether the config names a browser to connect to. A composition with no endpoint has no eyes, and a
|
|
13
19
|
* caller reads that up front instead of failing once for each attempted connection.
|
|
20
|
+
*
|
|
21
|
+
* The predicate narrows the config. Thus a caller that reads the endpoint after the gate needs no type
|
|
22
|
+
* assertion for a field that the gate proved.
|
|
14
23
|
*/
|
|
15
24
|
export function hasBrowserUrl(cfg) {
|
|
16
|
-
return cfg.browserUrl
|
|
25
|
+
return namesEndpoint(cfg.browserUrl);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The endpoint of a connection, or the refusal of a config that names none.
|
|
29
|
+
*
|
|
30
|
+
* The refusal and {@link hasBrowserUrl} read the same rule. Thus the gate that a caller reads up front is the
|
|
31
|
+
* gate that the connection applies.
|
|
32
|
+
*/
|
|
33
|
+
function requireBrowserUrl(browserUrl) {
|
|
34
|
+
if (namesEndpoint(browserUrl))
|
|
35
|
+
return browserUrl;
|
|
36
|
+
throw new Error("CHROME_BROWSER_URL is not set — Cortex requires the chrome sidecar to be reachable");
|
|
37
|
+
}
|
|
38
|
+
const connectOverPuppeteer = (browserUrl) => puppeteer.connect({ browserURL: browserUrl });
|
|
39
|
+
let connector = connectOverPuppeteer;
|
|
40
|
+
/**
|
|
41
|
+
* Replace the connect operation, and give back the restore of the previous one.
|
|
42
|
+
*
|
|
43
|
+
* The cache keeps no state that a caller can read, because a connection is the only product of the module.
|
|
44
|
+
* Thus a real browser is the only other way to drive the cache, and a unit test of the keys needs this seam.
|
|
45
|
+
*
|
|
46
|
+
* Two facts make the seam safe. The module reads the binding at each connect, and no module under `src/`
|
|
47
|
+
* calls the setter. Thus a deployment always runs the connect of puppeteer. The caller restores the previous
|
|
48
|
+
* binding, thus a replacement never reaches a later file of the same test run.
|
|
49
|
+
*/
|
|
50
|
+
export function setBrowserConnector(next) {
|
|
51
|
+
const previous = connector;
|
|
52
|
+
connector = next;
|
|
53
|
+
return () => {
|
|
54
|
+
connector = previous;
|
|
55
|
+
};
|
|
17
56
|
}
|
|
18
|
-
let browser;
|
|
19
|
-
let connecting;
|
|
20
|
-
let semaphore;
|
|
21
57
|
function createSemaphore(max) {
|
|
22
58
|
let active = 0;
|
|
23
59
|
const queue = [];
|
|
@@ -42,50 +78,81 @@ function createSemaphore(max) {
|
|
|
42
78
|
},
|
|
43
79
|
};
|
|
44
80
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
/** The cap on the pages that run at the same time against one endpoint, when a config names none. */
|
|
82
|
+
const DEFAULT_MAX_PAGES = 4;
|
|
83
|
+
/**
|
|
84
|
+
* The entry of each endpoint that the process reached, keyed by the endpoint URL. A disconnect evicts an
|
|
85
|
+
* entry, thus a browser that appears for one look leaves nothing behind.
|
|
86
|
+
*
|
|
87
|
+
* The eviction is event-driven, and no sweep runs over this map. The socket close of a dead browser raises
|
|
88
|
+
* the disconnect event, thus a browser that dies in the normal way evicts its own entry. A browser that dies
|
|
89
|
+
* with no event leaves one entry that nothing removes. The cost is one dead entry for one endpoint URL that
|
|
90
|
+
* no later look names again.
|
|
91
|
+
*/
|
|
92
|
+
const endpoints = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* The entry of one endpoint, made on the first call for that endpoint.
|
|
95
|
+
*
|
|
96
|
+
* The first caller fixes the cap of the gate. A later caller with a different cap reads the gate that is
|
|
97
|
+
* already in place, because one endpoint answers to one gate.
|
|
98
|
+
*/
|
|
99
|
+
function entryFor(browserUrl, maxPages) {
|
|
100
|
+
const existing = endpoints.get(browserUrl);
|
|
101
|
+
if (existing)
|
|
102
|
+
return existing;
|
|
103
|
+
const max = maxPages && maxPages > 0 ? maxPages : DEFAULT_MAX_PAGES;
|
|
104
|
+
const fresh = { semaphore: createSemaphore(max) };
|
|
105
|
+
endpoints.set(browserUrl, fresh);
|
|
106
|
+
return fresh;
|
|
51
107
|
}
|
|
52
108
|
export async function getBrowser(browserUrl, injected) {
|
|
53
109
|
const logger = (injected ?? createNoopLogger()).named("chrome");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
throw new Error("CHROME_BROWSER_URL is not set — Cortex requires the chrome sidecar to be reachable");
|
|
61
|
-
}
|
|
110
|
+
const browserURL = requireBrowserUrl(browserUrl);
|
|
111
|
+
const entry = entryFor(browserURL);
|
|
112
|
+
if (entry.browser?.connected)
|
|
113
|
+
return entry.browser;
|
|
114
|
+
if (entry.connecting)
|
|
115
|
+
return entry.connecting;
|
|
62
116
|
logger.info("connecting to browser", { browserURL });
|
|
63
|
-
connecting =
|
|
64
|
-
.connect({ browserURL })
|
|
117
|
+
entry.connecting = connector(browserURL)
|
|
65
118
|
.then((b) => {
|
|
66
|
-
browser = b;
|
|
119
|
+
entry.browser = b;
|
|
67
120
|
b.on("disconnected", () => {
|
|
68
121
|
logger.info("browser disconnected; will reconnect on next request");
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
// A reconnect can hold the key again by the time that the dead browser reports. The two
|
|
123
|
+
// guards keep the eviction on the entry of this browser alone.
|
|
124
|
+
if (endpoints.get(browserURL) === entry && entry.browser === b)
|
|
125
|
+
endpoints.delete(browserURL);
|
|
71
126
|
});
|
|
72
127
|
logger.info("connected", { wsEndpoint: b.wsEndpoint() });
|
|
73
128
|
return b;
|
|
74
129
|
})
|
|
75
130
|
.catch((err) => {
|
|
76
131
|
logger.error("failed to connect to browser", logger.errorFields(err));
|
|
132
|
+
// A failed connect leaves nothing to reuse. The entry goes, thus a run of looks that each name a
|
|
133
|
+
// new endpoint and each fail cannot grow the map. The two guards match the disconnect listener,
|
|
134
|
+
// thus a reconnect that already took the key survives.
|
|
135
|
+
//
|
|
136
|
+
// A waiter that already holds a slot of the evicted gate keeps running, and a later caller makes
|
|
137
|
+
// a fresh gate. Thus the page cap of this endpoint can be exceeded for a moment. That is
|
|
138
|
+
// acceptable, because the connect just failed and no browser exists for either gate to bound.
|
|
139
|
+
if (endpoints.get(browserURL) === entry && entry.browser === undefined)
|
|
140
|
+
endpoints.delete(browserURL);
|
|
77
141
|
throw err;
|
|
78
142
|
})
|
|
79
143
|
.finally(() => {
|
|
80
|
-
connecting = undefined;
|
|
144
|
+
entry.connecting = undefined;
|
|
81
145
|
});
|
|
82
|
-
return connecting;
|
|
146
|
+
return entry.connecting;
|
|
83
147
|
}
|
|
84
148
|
export async function withPage(cfg, fn) {
|
|
85
|
-
|
|
149
|
+
// The gate belongs to one endpoint, thus the endpoint must be known before the gate. A config that names
|
|
150
|
+
// none refuses here, the same as the connection below refuses it.
|
|
151
|
+
const browserURL = requireBrowserUrl(cfg.browserUrl);
|
|
152
|
+
const release = await entryFor(browserURL, cfg.maxPages).semaphore.acquire();
|
|
86
153
|
let context;
|
|
87
154
|
try {
|
|
88
|
-
const b = await getBrowser(
|
|
155
|
+
const b = await getBrowser(browserURL, cfg.logger);
|
|
89
156
|
context = await b.createBrowserContext();
|
|
90
157
|
const page = await context.newPage();
|
|
91
158
|
return await fn(page, context);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eyes seam: where a browser comes from for one look at a report page.
|
|
3
|
+
*
|
|
4
|
+
* The seam answers that one question, and it answers nothing else. It gives an endpoint for one scope, and
|
|
5
|
+
* the harness keeps every page behavior: the `file://` navigation, the readiness wait, and the seen stamp.
|
|
6
|
+
* Thus a host with a standing sidecar and a host that starts a browser for one look share one page
|
|
7
|
+
* implementation.
|
|
8
|
+
*
|
|
9
|
+
* A realization must bound the life of what it provisions. A lease that no release ends must still end at
|
|
10
|
+
* that bound. A process can die between the acquire and the release, thus no release of a caller prevents
|
|
11
|
+
* every leak. The release of a caller is hygiene, and it is not the guarantee. This bound is the load-bearing
|
|
12
|
+
* contract of the seam, and each new realization obeys it.
|
|
13
|
+
*
|
|
14
|
+
* A realization must also bound how many browsers it provisions at one time. The page gate of the chrome
|
|
15
|
+
* module bounds one endpoint alone. Thus a realization that starts a browser at a new endpoint for each look
|
|
16
|
+
* meets no bound in the harness. Only the realization knows its own resource, and the count bound sits there.
|
|
17
|
+
*
|
|
18
|
+
* The seam speaks the throw protocol, the same as the capture seam beside it. A caller that needs a typed
|
|
19
|
+
* outcome guards the acquire and the release.
|
|
20
|
+
*/
|
|
21
|
+
import { type ChromeConfig } from "./chrome.js";
|
|
22
|
+
/**
|
|
23
|
+
* What one acquire covers.
|
|
24
|
+
*
|
|
25
|
+
* A realization that starts a container mounts the workspace root at the identical host path, because the
|
|
26
|
+
* page navigation reads a `file://` URL of the host tree. The caller resolves the root already, thus the
|
|
27
|
+
* scope hands it over and a realization holds no second resolver.
|
|
28
|
+
*/
|
|
29
|
+
export interface EyesScope {
|
|
30
|
+
readonly analysisId: string;
|
|
31
|
+
readonly workspaceRoot: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One acquired browser, and the end of the lease.
|
|
35
|
+
*
|
|
36
|
+
* `browserUrl` is the endpoint that a capture connects to. `release` ends the lease of the caller. The shape
|
|
37
|
+
* binds no duration, thus one look and a longer interactive hold read the same.
|
|
38
|
+
*/
|
|
39
|
+
export interface EyesLease {
|
|
40
|
+
readonly browserUrl: string;
|
|
41
|
+
release(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The eyes seam. One call gives one lease over one scope.
|
|
45
|
+
*
|
|
46
|
+
* The embedder binds a realization at its composition root. The harness reads this type alone, and it never
|
|
47
|
+
* asks which realization answers.
|
|
48
|
+
*/
|
|
49
|
+
export type AcquireEyes = (scope: EyesScope) => Promise<EyesLease>;
|
|
50
|
+
/**
|
|
51
|
+
* Make the static realization over a standing sidecar.
|
|
52
|
+
*
|
|
53
|
+
* Each look reads the configured endpoint, and the release does nothing, because the realization provisions
|
|
54
|
+
* nothing. The sidecar stands outside this process. Thus the deployment that runs the sidecar owns its life,
|
|
55
|
+
* and the no-leak bound of the seam holds with no work here.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createStaticEyes(chrome: ChromeConfig): AcquireEyes;
|
package/dist/lib/eyes.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eyes seam: where a browser comes from for one look at a report page.
|
|
3
|
+
*
|
|
4
|
+
* The seam answers that one question, and it answers nothing else. It gives an endpoint for one scope, and
|
|
5
|
+
* the harness keeps every page behavior: the `file://` navigation, the readiness wait, and the seen stamp.
|
|
6
|
+
* Thus a host with a standing sidecar and a host that starts a browser for one look share one page
|
|
7
|
+
* implementation.
|
|
8
|
+
*
|
|
9
|
+
* A realization must bound the life of what it provisions. A lease that no release ends must still end at
|
|
10
|
+
* that bound. A process can die between the acquire and the release, thus no release of a caller prevents
|
|
11
|
+
* every leak. The release of a caller is hygiene, and it is not the guarantee. This bound is the load-bearing
|
|
12
|
+
* contract of the seam, and each new realization obeys it.
|
|
13
|
+
*
|
|
14
|
+
* A realization must also bound how many browsers it provisions at one time. The page gate of the chrome
|
|
15
|
+
* module bounds one endpoint alone. Thus a realization that starts a browser at a new endpoint for each look
|
|
16
|
+
* meets no bound in the harness. Only the realization knows its own resource, and the count bound sits there.
|
|
17
|
+
*
|
|
18
|
+
* The seam speaks the throw protocol, the same as the capture seam beside it. A caller that needs a typed
|
|
19
|
+
* outcome guards the acquire and the release.
|
|
20
|
+
*/
|
|
21
|
+
import { hasBrowserUrl } from "./chrome.js";
|
|
22
|
+
/**
|
|
23
|
+
* Make the static realization over a standing sidecar.
|
|
24
|
+
*
|
|
25
|
+
* Each look reads the configured endpoint, and the release does nothing, because the realization provisions
|
|
26
|
+
* nothing. The sidecar stands outside this process. Thus the deployment that runs the sidecar owns its life,
|
|
27
|
+
* and the no-leak bound of the seam holds with no work here.
|
|
28
|
+
*/
|
|
29
|
+
export function createStaticEyes(chrome) {
|
|
30
|
+
// A realization with no endpoint can only fail at the first look, and a look sits deep inside a model
|
|
31
|
+
// turn. The refusal at construction moves the fault to the boot of the composition, where a programmer
|
|
32
|
+
// reads it. `getBrowser` in `chrome.ts` refuses the same condition with a throw, thus one absent endpoint
|
|
33
|
+
// gets one answer at both sites. The assembly constructs this realization behind `hasBrowserUrl` alone,
|
|
34
|
+
// thus a composition that names no browser never reaches the refusal.
|
|
35
|
+
if (!hasBrowserUrl(chrome)) {
|
|
36
|
+
throw new Error("static eyes need a browser endpoint, and the chrome config names none");
|
|
37
|
+
}
|
|
38
|
+
const browserUrl = chrome.browserUrl;
|
|
39
|
+
return async (_scope) => ({
|
|
40
|
+
browserUrl,
|
|
41
|
+
release: async () => {
|
|
42
|
+
// The standing sidecar outlives every look, thus nothing ends here.
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* imports them, thus a rename or a retime reaches the waiter at compile time. A second copy of the wait
|
|
11
11
|
* script would instead degrade every capture to the readiness timeout, and no type would break.
|
|
12
12
|
*
|
|
13
|
+
* The capture emulates the reduced-motion preference before the navigation. The design source collapses each
|
|
14
|
+
* transition under that preference, and it starts each reveal in its visible state. Thus the picture shows
|
|
15
|
+
* the final state, and no element appears mid-fade.
|
|
16
|
+
*
|
|
13
17
|
* The capture speaks the throw protocol, because the chrome connection does. A caller that needs a typed
|
|
14
18
|
* outcome guards the call.
|
|
15
19
|
*/
|
|
@@ -22,6 +26,12 @@ export interface FailedRequest {
|
|
|
22
26
|
/** The picture and the faults of one page capture. */
|
|
23
27
|
export interface PageCapture {
|
|
24
28
|
screenshotBase64: string;
|
|
29
|
+
/**
|
|
30
|
+
* What the picture holds. `full` is the whole document, and it is the shape of a capture that passed at
|
|
31
|
+
* the first attempt. `viewport` appears when the full-page screenshot threw and the retry at the window
|
|
32
|
+
* passed, thus the picture shows the top window alone and a section below the fold is absent from it.
|
|
33
|
+
*/
|
|
34
|
+
coverage: "full" | "viewport";
|
|
25
35
|
consoleErrors: string[];
|
|
26
36
|
failedRequests: FailedRequest[];
|
|
27
37
|
}
|
|
@@ -41,6 +51,10 @@ export interface CaptureOptions {
|
|
|
41
51
|
/**
|
|
42
52
|
* Capture one page: the screenshot, the console errors, and the failed requests.
|
|
43
53
|
*
|
|
54
|
+
* The picture holds the whole document, and not the window alone. Thus a caller can judge a section that a
|
|
55
|
+
* reader reaches by a scroll. A refused full-page bitmap degrades to the window, and the coverage of the
|
|
56
|
+
* result names which of the two pictures arrived.
|
|
57
|
+
*
|
|
44
58
|
* The readiness wait is best-effort. A page that never signals still captures at the readiness budget, thus
|
|
45
59
|
* a broken page gives a picture that shows what broke.
|
|
46
60
|
*/
|
package/dist/lib/page-capture.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* imports them, thus a rename or a retime reaches the waiter at compile time. A second copy of the wait
|
|
11
11
|
* script would instead degrade every capture to the readiness timeout, and no type would break.
|
|
12
12
|
*
|
|
13
|
+
* The capture emulates the reduced-motion preference before the navigation. The design source collapses each
|
|
14
|
+
* transition under that preference, and it starts each reveal in its visible state. Thus the picture shows
|
|
15
|
+
* the final state, and no element appears mid-fade.
|
|
16
|
+
*
|
|
13
17
|
* The capture speaks the throw protocol, because the chrome connection does. A caller that needs a typed
|
|
14
18
|
* outcome guards the call.
|
|
15
19
|
*/
|
|
@@ -17,6 +21,14 @@ import { withPage } from "./chrome.js";
|
|
|
17
21
|
import { PAGE_NAV_TIMEOUT_MS, PAGE_READY_TIMEOUT_MS, THEME_READY_EVENT, THEME_READY_SENTINEL } from "../report-render/page.js";
|
|
18
22
|
/** The budget of the optional selector wait. The wait is best-effort, thus a miss captures the page anyway. */
|
|
19
23
|
const SELECTOR_TIMEOUT_MS = 5_000;
|
|
24
|
+
/**
|
|
25
|
+
* The window of one capture, in CSS pixels.
|
|
26
|
+
*
|
|
27
|
+
* The width clears each breakpoint of the design source. Thus a multi-column band lays out at the width that a
|
|
28
|
+
* reader gets, and no band collapses to one column because the window was narrow.
|
|
29
|
+
*/
|
|
30
|
+
const VIEWPORT_WIDTH = 1440;
|
|
31
|
+
const VIEWPORT_HEIGHT = 900;
|
|
20
32
|
/**
|
|
21
33
|
* The body of the readiness wait, in the browser context. The sentinel arm resolves a page that dispatched
|
|
22
34
|
* the event before this wait registered, thus a plain listener never blocks forever. The timer arm bounds a
|
|
@@ -41,9 +53,42 @@ function waitForThemeReady(sentinel, event, timeout) {
|
|
|
41
53
|
}, { once: true });
|
|
42
54
|
});
|
|
43
55
|
}
|
|
56
|
+
/** The connection gives base64 text or raw bytes, and a caller of the capture reads base64 text alone. */
|
|
57
|
+
function toBase64(shot) {
|
|
58
|
+
return typeof shot === "string" ? shot : Buffer.from(shot).toString("base64");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Take the picture of one settled page, at the whole document or at the window alone.
|
|
62
|
+
*
|
|
63
|
+
* The compositor can refuse a full-page bitmap of a tall page while the bitmap of the window is fine. A
|
|
64
|
+
* degraded picture beats a dead look, thus a refused full page retries one time at the window. A second throw
|
|
65
|
+
* names a broken browser and not a tall page, thus it propagates to the caller.
|
|
66
|
+
*/
|
|
67
|
+
async function captureShot(page) {
|
|
68
|
+
try {
|
|
69
|
+
return { screenshotBase64: toBase64(await page.screenshot({ encoding: "base64", fullPage: true })), coverage: "full" };
|
|
70
|
+
}
|
|
71
|
+
catch (refusedFullPage) {
|
|
72
|
+
try {
|
|
73
|
+
return { screenshotBase64: toBase64(await page.screenshot({ encoding: "base64" })), coverage: "viewport" };
|
|
74
|
+
}
|
|
75
|
+
catch (refusedViewport) {
|
|
76
|
+
// The two refusals together are the account of a dead look, and the caller reports the second one
|
|
77
|
+
// alone. Thus the first one rides as the cause, and no reader of that report loses half of it.
|
|
78
|
+
if (refusedViewport instanceof Error && refusedViewport.cause === undefined) {
|
|
79
|
+
refusedViewport.cause = refusedFullPage;
|
|
80
|
+
}
|
|
81
|
+
throw refusedViewport;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
44
85
|
/**
|
|
45
86
|
* Capture one page: the screenshot, the console errors, and the failed requests.
|
|
46
87
|
*
|
|
88
|
+
* The picture holds the whole document, and not the window alone. Thus a caller can judge a section that a
|
|
89
|
+
* reader reaches by a scroll. A refused full-page bitmap degrades to the window, and the coverage of the
|
|
90
|
+
* result names which of the two pictures arrived.
|
|
91
|
+
*
|
|
47
92
|
* The readiness wait is best-effort. A page that never signals still captures at the readiness budget, thus
|
|
48
93
|
* a broken page gives a picture that shows what broke.
|
|
49
94
|
*/
|
|
@@ -59,6 +104,12 @@ export function capturePage(chrome, url, options = {}) {
|
|
|
59
104
|
page.on("requestfailed", (req) => {
|
|
60
105
|
failedRequests.push({ url: req.url(), reason: req.failure()?.errorText ?? "unknown" });
|
|
61
106
|
});
|
|
107
|
+
// The size is set before the navigation, because a layout resolves at load time. The connection gives
|
|
108
|
+
// a small default window, and that window collapses each multi-column band of the design.
|
|
109
|
+
await page.setViewport({ width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT });
|
|
110
|
+
// The preference is active before the navigation, because the page reveals its sections as it loads.
|
|
111
|
+
// A preference that arrives after the load reaches a page that already runs its transitions.
|
|
112
|
+
await page.emulateMediaFeatures([{ name: "prefers-reduced-motion", value: "reduce" }]);
|
|
62
113
|
await page.goto(url, { waitUntil: "networkidle2", timeout: PAGE_NAV_TIMEOUT_MS });
|
|
63
114
|
// The two names and the budget ride as arguments, because the browser context has no module scope.
|
|
64
115
|
await page.evaluate(waitForThemeReady, THEME_READY_SENTINEL, THEME_READY_EVENT, PAGE_READY_TIMEOUT_MS).catch(() => {
|
|
@@ -73,9 +124,12 @@ export function capturePage(chrome, url, options = {}) {
|
|
|
73
124
|
const settle = options.waitMs;
|
|
74
125
|
await new Promise((resolve) => setTimeout(resolve, settle));
|
|
75
126
|
}
|
|
76
|
-
|
|
127
|
+
// The picture must show the whole document at the layout that a reader gets. Thus a defect below the
|
|
128
|
+
// fold is visible, and a question about content that never appeared has an answer.
|
|
129
|
+
const shot = await captureShot(page);
|
|
77
130
|
return {
|
|
78
|
-
screenshotBase64:
|
|
131
|
+
screenshotBase64: shot.screenshotBase64,
|
|
132
|
+
coverage: shot.coverage,
|
|
79
133
|
consoleErrors,
|
|
80
134
|
failedRequests,
|
|
81
135
|
};
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resultStep } from "./run-step.js";
|
|
|
11
11
|
import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
|
|
12
12
|
import { isToolError, readToolResultImage } from "../tools/define-tool.js";
|
|
13
13
|
import { addChatUsage, hasReportedUsage, recordAgentRun } from "./metrics.js";
|
|
14
|
-
import { computeDetail } from "./tool-detail.js";
|
|
14
|
+
import { computeDetail, computeResultDetail } from "./tool-detail.js";
|
|
15
15
|
import { toolOutcomeForOutputType } from "./tool-outcome.js";
|
|
16
16
|
const TRUNCATED_PROSE_STEER = "Your previous reply was cut off at the output-token limit; continue concisely, or finish via your terminal tool.";
|
|
17
17
|
const TRUNCATED_TOOL_USE_ERROR = "Your previous tool call was cut off at the output-token limit and was not executed. Retry with a smaller payload, writing in incremental pieces.";
|
|
@@ -145,7 +145,8 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
145
145
|
*
|
|
146
146
|
* Computed once per round and carried onto both the `tool-started` and the
|
|
147
147
|
* `tool-finished` event, so the pair a host renders as one chip cannot show
|
|
148
|
-
* two different descriptions of the same call.
|
|
148
|
+
* two different descriptions of the same call. A tool that describes its own
|
|
149
|
+
* result replaces the second half of that pair with the outcome it produced.
|
|
149
150
|
*/
|
|
150
151
|
const roundDetails = (calls) => calls.map((tu) => {
|
|
151
152
|
const tool = toolsById.get(tu.toolName);
|
|
@@ -160,11 +161,11 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
160
161
|
* it is not a thrown failure, so nothing reports it and the loop simply feeds it
|
|
161
162
|
* back — and a run that ends badly is usually a run whose tool calls were failing.
|
|
162
163
|
*
|
|
163
|
-
* `details` and `durations` are positionally aligned with `calls`.
|
|
164
|
-
* measures a duration around the call itself, because this sink emits
|
|
165
|
-
* event only after the whole round settles.
|
|
164
|
+
* `details`, `resultDetails` and `durations` are positionally aligned with `calls`.
|
|
165
|
+
* `dispatchTools` measures a duration around the call itself, because this sink emits
|
|
166
|
+
* every finish event only after the whole round settles.
|
|
166
167
|
*/
|
|
167
|
-
const settleRound = async (calls, results, details, durations) => {
|
|
168
|
+
const settleRound = async (calls, results, details, resultDetails, durations) => {
|
|
168
169
|
const errored = [];
|
|
169
170
|
for (let idx = 0; idx < calls.length; idx++) {
|
|
170
171
|
const tu = calls[idx];
|
|
@@ -183,7 +184,11 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
183
184
|
toolUseId: tu.toolCallId,
|
|
184
185
|
name: tu.toolName,
|
|
185
186
|
outcome,
|
|
186
|
-
|
|
187
|
+
// The result detail wins where there is one, and the call detail
|
|
188
|
+
// stands otherwise. Only the ok branch of a dispatch computes a
|
|
189
|
+
// result detail, thus an error and a denial reach here with none
|
|
190
|
+
// and keep the line the `tool-started` carried.
|
|
191
|
+
...detailField(resultDetails[idx] ?? details[idx]),
|
|
187
192
|
...durationField(durations[idx]),
|
|
188
193
|
});
|
|
189
194
|
}
|
|
@@ -242,8 +247,8 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
242
247
|
for (const [idx, tu] of earlier.entries()) {
|
|
243
248
|
await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(earlierDetails[idx]) });
|
|
244
249
|
}
|
|
245
|
-
const { results, durations } = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
|
|
246
|
-
const errored = await settleRound(earlier, results, earlierDetails, durations);
|
|
250
|
+
const { results, durations, resultDetails } = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
|
|
251
|
+
const errored = await settleRound(earlier, results, earlierDetails, resultDetails, durations);
|
|
247
252
|
results.push(errorResult(trailing, TRUNCATED_TOOL_USE_ERROR));
|
|
248
253
|
// The trailing call was never dispatched, but it reaches the model as an error
|
|
249
254
|
// result like any other — so it counts, or `toolErrors` reports a cleaner run
|
|
@@ -273,8 +278,8 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
273
278
|
for (const [idx, tu] of toolCalls.entries()) {
|
|
274
279
|
await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(details[idx]) });
|
|
275
280
|
}
|
|
276
|
-
const { results, durations } = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
|
|
277
|
-
const errored = await settleRound(toolCalls, results, details, durations);
|
|
281
|
+
const { results, durations, resultDetails } = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
|
|
282
|
+
const errored = await settleRound(toolCalls, results, details, resultDetails, durations);
|
|
278
283
|
if (errored.length > 0)
|
|
279
284
|
log.debug("tool results returned errors", { iteration: i, tools: errored });
|
|
280
285
|
messages.push({ role: "tool", content: results });
|
|
@@ -438,7 +443,7 @@ function appendDeferredImages(messages, results, deferredImages) {
|
|
|
438
443
|
/**
|
|
439
444
|
* Dispatch one round of tool calls, and measure the time of each call.
|
|
440
445
|
*
|
|
441
|
-
* `results` and `
|
|
446
|
+
* `results`, `durations` and `resultDetails` are positionally aligned with `toolUses`.
|
|
442
447
|
*
|
|
443
448
|
* Each measurement brackets the same unit that the loop awaits for that call. For a
|
|
444
449
|
* step-mode call that unit is `runStep`, thus the figure includes the durable-step
|
|
@@ -451,6 +456,10 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
|
|
|
451
456
|
// says so, thus it agrees with what `settleRound` accepts. Every index is in
|
|
452
457
|
// fact assigned, because the mode partition below covers each call.
|
|
453
458
|
const durations = new Array(toolUses.length);
|
|
459
|
+
// A hole stays a hole for a call that described no result: an error, a
|
|
460
|
+
// denial, and a tool with no result hook each leave the index unassigned,
|
|
461
|
+
// and `settleRound` then reads the call detail.
|
|
462
|
+
const resultDetails = new Array(toolUses.length);
|
|
454
463
|
const stepTools = [];
|
|
455
464
|
const workflowTools = [];
|
|
456
465
|
const inlineTools = [];
|
|
@@ -465,31 +474,53 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
|
|
|
465
474
|
}
|
|
466
475
|
await Promise.all(stepTools.map(({ tu, idx }) => {
|
|
467
476
|
const startedAt = performance.now();
|
|
468
|
-
return runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding)).then((
|
|
477
|
+
return runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding)).then((settled) => {
|
|
469
478
|
durations[idx] = elapsedMs(startedAt);
|
|
470
|
-
|
|
479
|
+
const dispatched = readDispatchedStep(settled);
|
|
480
|
+
results[idx] = dispatched.result;
|
|
481
|
+
if (dispatched.detail !== undefined)
|
|
482
|
+
resultDetails[idx] = dispatched.detail;
|
|
471
483
|
});
|
|
472
484
|
}));
|
|
473
485
|
for (const { tu, idx } of workflowTools) {
|
|
474
486
|
const startedAt = performance.now();
|
|
475
|
-
|
|
487
|
+
const dispatched = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
|
|
488
|
+
results[idx] = dispatched.result;
|
|
489
|
+
if (dispatched.detail !== undefined)
|
|
490
|
+
resultDetails[idx] = dispatched.detail;
|
|
476
491
|
durations[idx] = elapsedMs(startedAt);
|
|
477
492
|
}
|
|
478
493
|
for (const { tu, idx } of inlineTools) {
|
|
479
494
|
const startedAt = performance.now();
|
|
480
|
-
|
|
495
|
+
const dispatched = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
|
|
496
|
+
results[idx] = dispatched.result;
|
|
497
|
+
if (dispatched.detail !== undefined)
|
|
498
|
+
resultDetails[idx] = dispatched.detail;
|
|
481
499
|
durations[idx] = elapsedMs(startedAt);
|
|
482
500
|
}
|
|
483
|
-
return { results, durations };
|
|
501
|
+
return { results, durations, resultDetails };
|
|
484
502
|
}
|
|
485
503
|
/** Whole milliseconds since `startedAt`, on the monotonic clock that took that mark. */
|
|
486
504
|
function elapsedMs(startedAt) {
|
|
487
505
|
return Math.round(performance.now() - startedAt);
|
|
488
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Read one settled step-mode dispatch, tolerating the bare result beside the pair.
|
|
509
|
+
*
|
|
510
|
+
* A step-mode call goes through `runStep`, which caches what the body returned and hands
|
|
511
|
+
* that cached value back on a replay. The durable step store therefore holds values that
|
|
512
|
+
* a DIFFERENT build of this file wrote: a workflow in flight when the shape of the return
|
|
513
|
+
* widened replays the bare `ToolResultPart` its own run cached, and it never re-enters the
|
|
514
|
+
* body that would produce the pair. The replay boundary is the whole reason for the test:
|
|
515
|
+
* a bare value carries no detail, thus the call keeps the line its `tool-started` showed.
|
|
516
|
+
*/
|
|
517
|
+
function readDispatchedStep(settled) {
|
|
518
|
+
return "result" in settled ? settled : { result: settled };
|
|
519
|
+
}
|
|
489
520
|
async function dispatchTool(tu, toolsById, ctx, isFatalLoopError, encoding) {
|
|
490
521
|
const tool = toolsById.get(tu.toolName);
|
|
491
522
|
if (tool === undefined) {
|
|
492
|
-
return errorResult(tu, `unknown tool: ${tu.toolName}`);
|
|
523
|
+
return { result: errorResult(tu, `unknown tool: ${tu.toolName}`) };
|
|
493
524
|
}
|
|
494
525
|
const parsed = tool.inputSchema.safeParse(tu.input);
|
|
495
526
|
if (parsed.success)
|
|
@@ -502,21 +533,25 @@ async function dispatchTool(tu, toolsById, ctx, isFatalLoopError, encoding) {
|
|
|
502
533
|
const repaired = repairedInput === undefined ? undefined : tool.inputSchema.safeParse(repairedInput);
|
|
503
534
|
if (repaired?.success === true)
|
|
504
535
|
return execute(tu, tool, repaired.data, ctx, isFatalLoopError, encoding);
|
|
505
|
-
return errorResult(tu, `input validation failed: ${formatZodIssues(parsed.error, tu.input)}`);
|
|
536
|
+
return { result: errorResult(tu, `input validation failed: ${formatZodIssues(parsed.error, tu.input)}`) };
|
|
506
537
|
}
|
|
507
538
|
async function execute(tu, tool, input, ctx, isFatalLoopError, encoding) {
|
|
508
539
|
try {
|
|
509
540
|
const output = await tool.execute(input, ctx);
|
|
510
541
|
if (output.isErr())
|
|
511
|
-
return errorResult(tu, toolErrorContent(output.error));
|
|
512
|
-
|
|
542
|
+
return { result: errorResult(tu, toolErrorContent(output.error)) };
|
|
543
|
+
// The one place a result description runs: the ok value in hand, the
|
|
544
|
+
// input already validated, and the guard inside the compute.
|
|
545
|
+
const detail = computeResultDetail(tool, input, output.value, encoding.log);
|
|
546
|
+
const result = successResult(tu, output.value, encoding);
|
|
547
|
+
return detail === undefined ? { result } : { result, detail };
|
|
513
548
|
}
|
|
514
549
|
catch (err) {
|
|
515
550
|
if (isFatalLoopError(err))
|
|
516
551
|
throw err;
|
|
517
552
|
if (isAskRejected(err))
|
|
518
|
-
return deniedResult(tu, err.feedback);
|
|
519
|
-
return errorResult(tu, toolErrorContent(err));
|
|
553
|
+
return { result: deniedResult(tu, err.feedback) };
|
|
554
|
+
return { result: errorResult(tu, toolErrorContent(err)) };
|
|
520
555
|
}
|
|
521
556
|
}
|
|
522
557
|
/**
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* The call detail: one line naming what a tool call is doing.
|
|
3
3
|
*
|
|
4
4
|
* A tool states it through its own `describeCall` hook, colocated with the Zod
|
|
5
|
-
* `inputSchema` the compiler checks it against
|
|
6
|
-
*
|
|
5
|
+
* `inputSchema` the compiler checks it against, and through its optional
|
|
6
|
+
* `describeResult` hook, which names what the call produced. Everything else
|
|
7
|
+
* about the detail happens here, once, for every tool: validate, guard,
|
|
8
|
+
* normalize.
|
|
7
9
|
*
|
|
8
10
|
* Two rules make this the only place that matters:
|
|
9
11
|
*
|
|
@@ -92,3 +94,18 @@ export declare function normalizeDetail(raw: unknown): ToolCallDetail | undefine
|
|
|
92
94
|
* second time from the repaired value costs more than the case is worth.
|
|
93
95
|
*/
|
|
94
96
|
export declare function computeDetail(tool: Tool, rawInput: unknown, log: Logger): ToolCallDetail | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Compute a tool call's result detail, best-effort.
|
|
99
|
+
*
|
|
100
|
+
* `parsedInput` is the value the dispatch already validated, and `result` is the
|
|
101
|
+
* ok value the tool returned one line above. Thus neither is re-parsed here: a
|
|
102
|
+
* second parse of the input would repeat work the dispatch did, and the result
|
|
103
|
+
* satisfies no schema at all — the tool's own `Output` type is what the hook
|
|
104
|
+
* declares, and the tool is the author of both.
|
|
105
|
+
*
|
|
106
|
+
* The guard is the whole difference from a bare call. A hook that throws, returns
|
|
107
|
+
* a non-string, or returns nothing usable yields no result detail, and the
|
|
108
|
+
* finished event falls back on the call detail. A description must never be able
|
|
109
|
+
* to fail a call that already succeeded.
|
|
110
|
+
*/
|
|
111
|
+
export declare function computeResultDetail(tool: Tool, parsedInput: unknown, result: unknown, log: Logger): ToolCallDetail | undefined;
|