@inflexa-ai/harness 0.44.0 → 0.45.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 +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +51 -0
- package/dist/app/approve-plan.js +84 -0
- package/dist/app/chat-turn.js +6 -3
- package/dist/app/data-profile-view.d.ts +1 -1
- package/dist/app/run-activity.js +4 -6
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +14 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.d.ts +7 -0
- package/dist/contracts/chat-frame.js +24 -6
- package/dist/contracts/chat-parts.d.ts +73 -2
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/part-registry.js +3 -0
- package/dist/contracts/schemas/chat-events.d.ts +38 -0
- package/dist/contracts/schemas/chat-events.js +8 -0
- package/dist/contracts/schemas/chat-parts.d.ts +239 -15
- package/dist/contracts/schemas/chat-parts.js +80 -12
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/contracts/subagent-activity.d.ts +12 -0
- package/dist/contracts/subagent-activity.js +76 -0
- package/dist/execution/artifact-metadata.js +1 -3
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +130 -28
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/execution/run-event-parts.d.ts +6 -5
- package/dist/execution/run-event-parts.js +6 -5
- package/dist/execution/run-event-stream.d.ts +18 -6
- package/dist/execution/run-event-stream.js +67 -17
- package/dist/execution/run-synthesis.d.ts +21 -0
- package/dist/execution/run-synthesis.js +77 -3
- package/dist/index.d.ts +28 -10
- package/dist/index.js +26 -2
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/metrics.d.ts +2 -2
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +16 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +8 -0
- package/dist/memory/card-builders.d.ts +11 -0
- package/dist/memory/card-builders.js +35 -21
- package/dist/memory/conversation-display-recorder.d.ts +2 -2
- package/dist/memory/conversation-display-recorder.js +38 -4
- package/dist/memory/conversation-display-storage.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +18 -3
- package/dist/memory/thread-history.d.ts +42 -8
- package/dist/memory/thread-history.js +164 -45
- package/dist/memory/thread-store.d.ts +13 -0
- package/dist/memory/thread-store.js +15 -1
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +3 -1
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +39 -20
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +93 -11
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/report-model/pin-snapshot.d.ts +1 -1
- package/dist/report-model/pin-snapshot.js +2 -50
- package/dist/runtime/assemble.d.ts +8 -1
- package/dist/runtime/assemble.js +11 -0
- package/dist/runtime/boot.d.ts +3 -0
- package/dist/runtime/boot.js +10 -1
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
- package/dist/sandbox/sandbox-step-translate.js +24 -0
- package/dist/schemas/artifact-manifest.d.ts +1 -1
- package/dist/schemas/artifact-manifest.js +11 -9
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +75 -21
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/analyses-metrics.d.ts +79 -7
- package/dist/state/analyses-metrics.js +141 -6
- package/dist/state/artifacts.d.ts +66 -3
- package/dist/state/artifacts.js +85 -7
- package/dist/state/data-profile.d.ts +4 -2
- package/dist/state/data-profile.js +7 -5
- package/dist/state/index.d.ts +9 -8
- package/dist/state/index.js +5 -5
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
- package/dist/state/migrations/index.js +8 -0
- package/dist/state/plans.d.ts +21 -0
- package/dist/state/plans.js +25 -0
- package/dist/state/runs.d.ts +79 -14
- package/dist/state/runs.js +150 -33
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tasks/data-profile-activity.d.ts +7 -1
- package/dist/tasks/data-profile-activity.js +18 -9
- package/dist/tasks/data-profile.d.ts +15 -1
- package/dist/tasks/data-profile.js +16 -11
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/approval/gateway.js +28 -5
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +80 -1
- package/dist/tools/execute-analysis.js +352 -110
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/inspect-data-profile.d.ts +1 -1
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/inspect-run.js +7 -7
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +62 -21
- package/dist/workflows/execute-analysis.js +307 -79
- package/dist/workflows/plan/graph.d.ts +16 -0
- package/dist/workflows/plan/graph.js +36 -0
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/dist/workflows/sandbox-step.js +14 -12
- package/package.json +5 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guarded-address classifier of the web research tools.
|
|
3
|
+
*
|
|
4
|
+
* One pure function over one address: `isGuardedAddress` answers whether the
|
|
5
|
+
* address falls in a range that `web_fetch` asks the user about before it
|
|
6
|
+
* connects. The ranges are a pinned subset of the special-purpose address
|
|
7
|
+
* ranges of the IANA registries — loopback, private, shared, link-local,
|
|
8
|
+
* multicast, reserved, documentation, and benchmarking space, IPv4 and IPv6.
|
|
9
|
+
* A public unicast address is never guarded.
|
|
10
|
+
*
|
|
11
|
+
* The caller owns the question "which address does the connection use": the
|
|
12
|
+
* classifier never resolves a name and it never reads the network.
|
|
13
|
+
*/
|
|
14
|
+
import { isIP } from "node:net";
|
|
15
|
+
/** Parse a dotted-quad IPv4 address into its 32-bit value. Anything else gives null. */
|
|
16
|
+
function ipv4ToNumber(ip) {
|
|
17
|
+
const parts = ip.split(".");
|
|
18
|
+
if (parts.length !== 4)
|
|
19
|
+
return null;
|
|
20
|
+
let value = 0;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (!/^\d{1,3}$/.test(part))
|
|
23
|
+
return null;
|
|
24
|
+
const octet = Number(part);
|
|
25
|
+
if (octet > 255)
|
|
26
|
+
return null;
|
|
27
|
+
value = (value << 8) | octet;
|
|
28
|
+
}
|
|
29
|
+
return value >>> 0;
|
|
30
|
+
}
|
|
31
|
+
/** Parse an IPv6 address into its 128-bit value. Anything else gives null. */
|
|
32
|
+
function ipv6ToBigInt(ip) {
|
|
33
|
+
let text = ip.toLowerCase();
|
|
34
|
+
// An embedded IPv4 tail stands for its two low-order groups.
|
|
35
|
+
if (text.includes(".")) {
|
|
36
|
+
const lastColon = text.lastIndexOf(":");
|
|
37
|
+
const tail = ipv4ToNumber(text.slice(lastColon + 1));
|
|
38
|
+
if (tail === null)
|
|
39
|
+
return null;
|
|
40
|
+
const high = ((tail >>> 16) & 0xffff).toString(16);
|
|
41
|
+
const low = (tail & 0xffff).toString(16);
|
|
42
|
+
text = `${text.slice(0, lastColon + 1)}${high}:${low}`;
|
|
43
|
+
}
|
|
44
|
+
const groupsOf = (half) => {
|
|
45
|
+
if (half === "")
|
|
46
|
+
return [];
|
|
47
|
+
const groups = [];
|
|
48
|
+
for (const group of half.split(":")) {
|
|
49
|
+
if (!/^[0-9a-f]{1,4}$/.test(group))
|
|
50
|
+
return null;
|
|
51
|
+
groups.push(BigInt(`0x${group}`));
|
|
52
|
+
}
|
|
53
|
+
return groups;
|
|
54
|
+
};
|
|
55
|
+
const halves = text.split("::");
|
|
56
|
+
if (halves.length > 2)
|
|
57
|
+
return null;
|
|
58
|
+
const left = groupsOf(halves[0] ?? "");
|
|
59
|
+
const right = halves.length === 2 ? groupsOf(halves[1] ?? "") : [];
|
|
60
|
+
if (left === null || right === null)
|
|
61
|
+
return null;
|
|
62
|
+
const fill = 8 - left.length - right.length;
|
|
63
|
+
if (halves.length === 2 ? fill < 0 : fill !== 0)
|
|
64
|
+
return null;
|
|
65
|
+
return [...left, ...new Array(Math.max(fill, 0)).fill(0n), ...right].reduce((acc, group) => (acc << 16n) | group, 0n);
|
|
66
|
+
}
|
|
67
|
+
const ipv4Span = (first, last) => {
|
|
68
|
+
const low = ipv4ToNumber(first);
|
|
69
|
+
const high = ipv4ToNumber(last);
|
|
70
|
+
if (low === null || high === null)
|
|
71
|
+
throw new Error(`Bad IPv4 range literal: ${first}..${last}`);
|
|
72
|
+
return { first: low, last: high };
|
|
73
|
+
};
|
|
74
|
+
const ipv6Span = (first, last) => {
|
|
75
|
+
const low = ipv6ToBigInt(first);
|
|
76
|
+
const high = ipv6ToBigInt(last);
|
|
77
|
+
if (low === null || high === null)
|
|
78
|
+
throw new Error(`Bad IPv6 range literal: ${first}..${last}`);
|
|
79
|
+
return { first: low, last: high };
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The pinned IPv4 table. `240.0.0.0/4` holds the reserved and the broadcast
|
|
83
|
+
* space together, and it ends at the top of the address family.
|
|
84
|
+
*/
|
|
85
|
+
const GUARDED_IPV4_RANGES = [
|
|
86
|
+
ipv4Span("0.0.0.0", "0.255.255.255"), // 0.0.0.0/8
|
|
87
|
+
ipv4Span("10.0.0.0", "10.255.255.255"), // 10.0.0.0/8
|
|
88
|
+
ipv4Span("100.64.0.0", "100.127.255.255"), // 100.64.0.0/10
|
|
89
|
+
ipv4Span("127.0.0.0", "127.255.255.255"), // 127.0.0.0/8
|
|
90
|
+
ipv4Span("169.254.0.0", "169.254.255.255"), // 169.254.0.0/16
|
|
91
|
+
ipv4Span("172.16.0.0", "172.31.255.255"), // 172.16.0.0/12
|
|
92
|
+
ipv4Span("192.0.0.0", "192.0.0.255"), // 192.0.0.0/24
|
|
93
|
+
ipv4Span("192.0.2.0", "192.0.2.255"), // 192.0.2.0/24
|
|
94
|
+
ipv4Span("192.88.99.0", "192.88.99.255"), // 192.88.99.0/24
|
|
95
|
+
ipv4Span("192.168.0.0", "192.168.255.255"), // 192.168.0.0/16
|
|
96
|
+
ipv4Span("198.18.0.0", "198.19.255.255"), // 198.18.0.0/15
|
|
97
|
+
ipv4Span("198.51.100.0", "198.51.100.255"), // 198.51.100.0/24
|
|
98
|
+
ipv4Span("203.0.113.0", "203.0.113.255"), // 203.0.113.0/24
|
|
99
|
+
ipv4Span("224.0.0.0", "239.255.255.255"), // 224.0.0.0/4
|
|
100
|
+
ipv4Span("240.0.0.0", "255.255.255.255"), // 240.0.0.0/4
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* The pinned IPv6 table, without `::ffff:0:0/96`: an IPv4-mapped address
|
|
104
|
+
* normalizes to its IPv4 half first, thus that range classifies through the
|
|
105
|
+
* IPv4 table above.
|
|
106
|
+
*/
|
|
107
|
+
const GUARDED_IPV6_RANGES = [
|
|
108
|
+
ipv6Span("::", "::"), // ::/128
|
|
109
|
+
ipv6Span("::1", "::1"), // ::1/128
|
|
110
|
+
ipv6Span("64:ff9b::", "64:ff9b::ffff:ffff"), // 64:ff9b::/96
|
|
111
|
+
ipv6Span("64:ff9b:1::", "64:ff9b:1:ffff:ffff:ffff:ffff:ffff"), // 64:ff9b:1::/48
|
|
112
|
+
ipv6Span("100::", "100::ffff:ffff:ffff:ffff"), // 100::/64
|
|
113
|
+
ipv6Span("2001::", "2001:0:ffff:ffff:ffff:ffff:ffff:ffff"), // 2001::/32
|
|
114
|
+
ipv6Span("2001:db8::", "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"), // 2001:db8::/32
|
|
115
|
+
ipv6Span("2002::", "2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), // 2002::/16
|
|
116
|
+
ipv6Span("fc00::", "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), // fc00::/7
|
|
117
|
+
ipv6Span("fe80::", "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), // fe80::/10
|
|
118
|
+
ipv6Span("ff00::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), // ff00::/8
|
|
119
|
+
];
|
|
120
|
+
function isGuardedIpv4(value) {
|
|
121
|
+
return GUARDED_IPV4_RANGES.some((range) => value >= range.first && value <= range.last);
|
|
122
|
+
}
|
|
123
|
+
function isGuardedIpv6(value) {
|
|
124
|
+
return GUARDED_IPV6_RANGES.some((range) => value >= range.first && value <= range.last);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Is this address one the web tools guard? An IP literal of either family
|
|
128
|
+
* classifies against the pinned table; the IPv4-mapped IPv6 form
|
|
129
|
+
* (`::ffff:10.0.0.1`) classifies as its IPv4 half. A value that is not an IP
|
|
130
|
+
* address gives false — the caller resolves names before it reaches here.
|
|
131
|
+
*/
|
|
132
|
+
export function isGuardedAddress(ip) {
|
|
133
|
+
const family = isIP(ip);
|
|
134
|
+
if (family === 4) {
|
|
135
|
+
const value = ipv4ToNumber(ip);
|
|
136
|
+
return value !== null && isGuardedIpv4(value);
|
|
137
|
+
}
|
|
138
|
+
if (family === 6) {
|
|
139
|
+
const value = ipv6ToBigInt(ip);
|
|
140
|
+
if (value === null)
|
|
141
|
+
return false;
|
|
142
|
+
// The IPv4-mapped form, ::ffff:0:0/96, classifies as its IPv4 half.
|
|
143
|
+
if (value >> 32n === 0xffffn)
|
|
144
|
+
return isGuardedIpv4(Number(value & 0xffffffffn));
|
|
145
|
+
return isGuardedIpv6(value);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
@@ -161,7 +161,7 @@ interface FailedOutput {
|
|
|
161
161
|
}
|
|
162
162
|
export type InspectDataProfileOutput = OverviewOutput | GroupsOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
|
|
163
163
|
export declare function createInspectDataProfileTool(pool: Pool): import("../define-tool.js").Tool<{
|
|
164
|
-
scope?: "
|
|
164
|
+
scope?: "groups" | "files" | "overview" | undefined;
|
|
165
165
|
page?: number | undefined;
|
|
166
166
|
pageSize?: number | undefined;
|
|
167
167
|
}, InspectDataProfileOutput>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* inspectRun — inspect workflow runs for the current analysis.
|
|
3
3
|
*
|
|
4
4
|
* Results are pull-only: workflows never append completion messages to a
|
|
5
|
-
* conversation. List mode
|
|
5
|
+
* conversation. List mode puts the running runs first and is paged. Targeted mode makes
|
|
6
6
|
* readiness explicit and can perform one bounded, abort-aware wait.
|
|
7
7
|
*/
|
|
8
8
|
import type { Pool } from "pg";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* inspectRun — inspect workflow runs for the current analysis.
|
|
3
3
|
*
|
|
4
4
|
* Results are pull-only: workflows never append completion messages to a
|
|
5
|
-
* conversation. List mode
|
|
5
|
+
* conversation. List mode puts the running runs first and is paged. Targeted mode makes
|
|
6
6
|
* readiness explicit and can perform one bounded, abort-aware wait.
|
|
7
7
|
*/
|
|
8
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
@@ -20,7 +20,7 @@ const systemClock = {
|
|
|
20
20
|
wait: (ms, signal) => sleep(ms, undefined, { signal }),
|
|
21
21
|
};
|
|
22
22
|
function isTerminal(status) {
|
|
23
|
-
return status === "completed" || status === "partial" || status === "failed" || status === "canceled";
|
|
23
|
+
return status === "completed" || status === "partial" || status === "failed" || status === "canceled" || status === "suspended_insufficient_funds";
|
|
24
24
|
}
|
|
25
25
|
function formatRun(r, verbose, exposePaths = isTerminal(r.status)) {
|
|
26
26
|
return {
|
|
@@ -98,10 +98,10 @@ export function createInspectRunTool(pool, clock = systemClock) {
|
|
|
98
98
|
return defineTool({
|
|
99
99
|
id: "inspect_run",
|
|
100
100
|
description: "Inspect workflow runs for this analysis. A run never posts its completion into the conversation; this tool is how you read it. " +
|
|
101
|
-
"Without a runId, returns
|
|
102
|
-
"With a runId, returns the run, its steps, and an inspectionState: not_found; in_progress; suspended (the run does not progress
|
|
101
|
+
"Without a runId, returns a paged list (running runs first, then each other run) with total and hasMore. " +
|
|
102
|
+
"With a runId, returns the run, its steps, and an inspectionState: not_found; in_progress; suspended (the host suspended the run, it does not progress, and a new run of the plan can start); " +
|
|
103
103
|
"or terminal (run.status completed, partial, failed, or canceled). " +
|
|
104
|
-
"Output paths appear only on a terminal run: run.synthesisPath when synthesisStatus is produced, and the summaryPath of each step that ran. " +
|
|
104
|
+
"Output paths appear only on a suspended or terminal run: run.synthesisPath when synthesisStatus is produced, and the summaryPath of each step that ran. " +
|
|
105
105
|
"For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
|
|
106
106
|
"A wait on the run that the caller itself runs inside returns at once with selfWaitPrevented. " +
|
|
107
107
|
"Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
|
|
@@ -179,8 +179,8 @@ export function createInspectRunTool(pool, clock = systemClock) {
|
|
|
179
179
|
if (run.status === "suspended_insufficient_funds") {
|
|
180
180
|
return ok({
|
|
181
181
|
inspectionState: "suspended",
|
|
182
|
-
message: "
|
|
183
|
-
run: formatRun(run, verbose,
|
|
182
|
+
message: "The host suspended this run. The run does not progress. A new run of the plan can start.",
|
|
183
|
+
run: formatRun(run, verbose, true),
|
|
184
184
|
steps,
|
|
185
185
|
...wait,
|
|
186
186
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SearXNG realization of the `WebSearchEngine` capability.
|
|
3
|
+
*
|
|
4
|
+
* One SearXNG instance is the only engine the harness ships (the embedder owns
|
|
5
|
+
* it and builds this realization at its composition root). The call rides
|
|
6
|
+
* `apiFetchValidated` with a zod schema over the JSON envelope, and every
|
|
7
|
+
* failure lands on the error side of the `Result` as the data-status variant the
|
|
8
|
+
* tool reports — an HTTP error, a spent retryable status, a schema mismatch,
|
|
9
|
+
* and a body that is not JSON are answers, not errors of the turn.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { type ApiError } from "../lib/api-utils.js";
|
|
13
|
+
import type { WebSearchEngine, WebSearchEngineError, WebSearchPage } from "./web-search.js";
|
|
14
|
+
export declare const SearxngSearchResponseSchema: z.ZodObject<{
|
|
15
|
+
results: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
16
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
17
|
+
url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
|
+
content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
|
+
}, z.core.$strip>>>;
|
|
20
|
+
answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
21
|
+
answer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
22
|
+
url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
23
|
+
}, z.core.$loose>>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
/** Map a parsed SearXNG envelope to the rows the tool reports. */
|
|
26
|
+
export declare function mapSearxngResponse(response: z.infer<typeof SearxngSearchResponseSchema>): WebSearchPage;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize the SearXNG base URL the embedder gives: `http`/`https` only, no
|
|
29
|
+
* user name and no password, and no query, fragment, or trailing slash. The
|
|
30
|
+
* value is a composition-root constant, thus a bad one throws here at wiring
|
|
31
|
+
* time and not inside a tool call.
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizeSearxngBaseUrl(raw: string): string;
|
|
34
|
+
/** Map one `ApiError` of the helper to the error data variant it names. */
|
|
35
|
+
export declare function describeSearchFailure(error: ApiError): WebSearchEngineError;
|
|
36
|
+
export interface SearxngEngineDeps {
|
|
37
|
+
/** The base URL of the SearXNG instance the embedder runs. */
|
|
38
|
+
readonly baseUrl: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function createSearxngEngine(deps: SearxngEngineDeps): WebSearchEngine;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SearXNG realization of the `WebSearchEngine` capability.
|
|
3
|
+
*
|
|
4
|
+
* One SearXNG instance is the only engine the harness ships (the embedder owns
|
|
5
|
+
* it and builds this realization at its composition root). The call rides
|
|
6
|
+
* `apiFetchValidated` with a zod schema over the JSON envelope, and every
|
|
7
|
+
* failure lands on the error side of the `Result` as the data-status variant the
|
|
8
|
+
* tool reports — an HTTP error, a spent retryable status, a schema mismatch,
|
|
9
|
+
* and a body that is not JSON are answers, not errors of the turn.
|
|
10
|
+
*/
|
|
11
|
+
import { err, ok } from "neverthrow";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { apiFetchValidated } from "../lib/api-utils.js";
|
|
14
|
+
// SearXNG sends an explicit null for a snippet no engine produced, and it omits
|
|
15
|
+
// a key no engine filled, thus each field is optional and nullable alike. The
|
|
16
|
+
// fields an engine adds (`engine`, `score`, `publishedDate`, …) pass and strip.
|
|
17
|
+
const SearxngResultSchema = z.object({
|
|
18
|
+
title: z.string().nullable().optional(),
|
|
19
|
+
url: z.string().nullable().optional(),
|
|
20
|
+
content: z.string().nullable().optional(),
|
|
21
|
+
});
|
|
22
|
+
// An answer is an object of a changing shape (`answer`, `url`, `template`,
|
|
23
|
+
// `engine`, …), thus the schema keeps it loose and the mapper reads the two
|
|
24
|
+
// fields the tool reports.
|
|
25
|
+
export const SearxngSearchResponseSchema = z.object({
|
|
26
|
+
results: z.array(SearxngResultSchema).optional(),
|
|
27
|
+
answers: z.array(z.looseObject({ answer: z.string().nullable().optional(), url: z.string().nullable().optional() })).optional(),
|
|
28
|
+
});
|
|
29
|
+
/** Map a parsed SearXNG envelope to the rows the tool reports. */
|
|
30
|
+
export function mapSearxngResponse(response) {
|
|
31
|
+
const answers = [];
|
|
32
|
+
for (const row of response.answers ?? []) {
|
|
33
|
+
const answer = typeof row.answer === "string" ? row.answer : undefined;
|
|
34
|
+
const url = typeof row.url === "string" ? row.url : undefined;
|
|
35
|
+
if (answer === undefined && url === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
answers.push({ answer, url });
|
|
38
|
+
}
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const row of response.results ?? []) {
|
|
41
|
+
if (typeof row.url !== "string" || typeof row.title !== "string")
|
|
42
|
+
continue;
|
|
43
|
+
results.push({ title: row.title, url: row.url, snippet: typeof row.content === "string" ? row.content : undefined });
|
|
44
|
+
}
|
|
45
|
+
return { answers, results };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalize the SearXNG base URL the embedder gives: `http`/`https` only, no
|
|
49
|
+
* user name and no password, and no query, fragment, or trailing slash. The
|
|
50
|
+
* value is a composition-root constant, thus a bad one throws here at wiring
|
|
51
|
+
* time and not inside a tool call.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeSearxngBaseUrl(raw) {
|
|
54
|
+
const parsed = new URL(raw);
|
|
55
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
56
|
+
throw new Error(`The SearXNG base URL must use http or https: ${raw}`);
|
|
57
|
+
}
|
|
58
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
59
|
+
throw new Error(`The SearXNG base URL must carry no user name or password: ${raw}`);
|
|
60
|
+
}
|
|
61
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
62
|
+
}
|
|
63
|
+
/** The first 300 characters of a body, as the HTTP-error text reports it. */
|
|
64
|
+
function bodyExcerpt(body) {
|
|
65
|
+
return body.length > 300 ? body.slice(0, 300) : body;
|
|
66
|
+
}
|
|
67
|
+
/** Map one `ApiError` of the helper to the error data variant it names. */
|
|
68
|
+
export function describeSearchFailure(error) {
|
|
69
|
+
switch (error.type) {
|
|
70
|
+
case "http_status":
|
|
71
|
+
return {
|
|
72
|
+
status: "http_error",
|
|
73
|
+
httpStatus: error.status,
|
|
74
|
+
text: `SearXNG answered HTTP ${error.status}${error.body ? `: ${bodyExcerpt(error.body)}` : ""}`,
|
|
75
|
+
};
|
|
76
|
+
case "invalid_response":
|
|
77
|
+
return { status: "schema_mismatch", text: `SearXNG response did not match the expected schema: ${error.issues}` };
|
|
78
|
+
case "timeout":
|
|
79
|
+
return {
|
|
80
|
+
status: "not_json",
|
|
81
|
+
text: `SearXNG did not answer with a valid JSON body: the request timed out after ${error.timeoutMs}ms`,
|
|
82
|
+
};
|
|
83
|
+
case "exhausted": {
|
|
84
|
+
// A spent retryable status arrives as `exhausted` with an `HTTP <status>`
|
|
85
|
+
// last error, because the helper reports it there and not as `http_status`.
|
|
86
|
+
const status = /^HTTP (\d+)$/.exec(error.lastError);
|
|
87
|
+
if (status) {
|
|
88
|
+
return {
|
|
89
|
+
status: "retryable_status",
|
|
90
|
+
httpStatus: Number(status[1]),
|
|
91
|
+
text: `SearXNG answered HTTP ${status[1]} and spent its retry attempt`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// A transport failure and a body that will not parse share this bucket,
|
|
95
|
+
// and the text stays fixed so it never echoes a runtime parse message.
|
|
96
|
+
return {
|
|
97
|
+
status: "not_json",
|
|
98
|
+
text: "SearXNG did not answer with a valid JSON body",
|
|
99
|
+
detail: error.lastError,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function createSearxngEngine(deps) {
|
|
105
|
+
const baseUrl = normalizeSearxngBaseUrl(deps.baseUrl);
|
|
106
|
+
return {
|
|
107
|
+
async search({ query, recencyFilter }) {
|
|
108
|
+
const params = new URLSearchParams({ q: query, format: "json" });
|
|
109
|
+
if (recencyFilter !== undefined)
|
|
110
|
+
params.set("time_range", recencyFilter);
|
|
111
|
+
const response = await apiFetchValidated(`${baseUrl}/search?${params}`, SearxngSearchResponseSchema, {
|
|
112
|
+
headers: { Accept: "application/json" },
|
|
113
|
+
maxRetries: 0,
|
|
114
|
+
});
|
|
115
|
+
if (response.isErr())
|
|
116
|
+
return err(describeSearchFailure(response.error));
|
|
117
|
+
return ok(mapSearxngResponse(response.value));
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `web_fetch` — the page-reading step of the conversation agent's web research.
|
|
3
|
+
*
|
|
4
|
+
* One URL in, extracted text out: HTML becomes Markdown (Readability over a
|
|
5
|
+
* `linkedom` DOM, then Turndown, with the body text as the fallback), a PDF
|
|
6
|
+
* becomes the text of its first 100 pages through `pdf-parse`, and a text or
|
|
7
|
+
* JSON body passes through. A bad scheme, a denied approval, an HTTP error, an
|
|
8
|
+
* oversized body, a redirect chain past the limit, and a body the tool cannot
|
|
9
|
+
* extract are all normal results on the ok channel — never a failure of the
|
|
10
|
+
* turn.
|
|
11
|
+
*
|
|
12
|
+
* The address guard reads the address the connection itself uses, because a
|
|
13
|
+
* separate name resolution before a `fetch` races the resolver (DNS rebinding):
|
|
14
|
+
* a literal host classifies before the request is sent — the connection layer
|
|
15
|
+
* runs no name lookup for a literal — and a name classifies inside the `lookup`
|
|
16
|
+
* hook of `node:http`/`node:https`, over every address of the resolution,
|
|
17
|
+
* because the connect layer falls back to a later address after an earlier one
|
|
18
|
+
* fails. A guarded hop is destroyed before the user is asked, and an approval
|
|
19
|
+
* re-sends the hop.
|
|
20
|
+
*/
|
|
21
|
+
import { type LookupAddress, type LookupAllOptions } from "node:dns";
|
|
22
|
+
import { type LookupFunction } from "node:net";
|
|
23
|
+
import { type ToolOutputStore } from "../../loop/tool-output.js";
|
|
24
|
+
/** The response head and body stream of one hop, narrowed to what the tool reads. */
|
|
25
|
+
export interface WebFetchHopResponse {
|
|
26
|
+
readonly statusCode?: number;
|
|
27
|
+
readonly headers: Record<string, string | string[] | undefined>;
|
|
28
|
+
on(event: "data", listener: (chunk: Buffer) => void): void;
|
|
29
|
+
on(event: "end", listener: () => void): void;
|
|
30
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
31
|
+
destroy(error?: Error): void;
|
|
32
|
+
/** A real response holds these; the decode path pauses the encoded side while the decoder drains. */
|
|
33
|
+
pause?(): void;
|
|
34
|
+
resume?(): void;
|
|
35
|
+
}
|
|
36
|
+
/** One hop request, narrowed to what the tool drives. */
|
|
37
|
+
export interface WebFetchHopRequestHandle {
|
|
38
|
+
on(event: "response", listener: (response: WebFetchHopResponse) => void): void;
|
|
39
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
40
|
+
on(event: "timeout", listener: () => void): void;
|
|
41
|
+
end(): void;
|
|
42
|
+
destroy(error?: Error): void;
|
|
43
|
+
}
|
|
44
|
+
export interface WebFetchHopRequestOptions {
|
|
45
|
+
readonly headers: Record<string, string>;
|
|
46
|
+
readonly lookup?: LookupFunction;
|
|
47
|
+
readonly timeoutMs: number;
|
|
48
|
+
}
|
|
49
|
+
/** One HTTP hop. The default is the real `node:http`/`node:https` request; a test can substitute a recorded hop. */
|
|
50
|
+
export type WebFetchHopRequest = (url: URL, options: WebFetchHopRequestOptions) => WebFetchHopRequestHandle;
|
|
51
|
+
/** A name resolution that answers every address at once, the `all: true` shape of `dns.lookup`. */
|
|
52
|
+
export type WebFetchLookup = (hostname: string, options: LookupAllOptions, callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void) => void;
|
|
53
|
+
export interface WebFetchKept {
|
|
54
|
+
readonly ref: string;
|
|
55
|
+
readonly keptLength: number;
|
|
56
|
+
}
|
|
57
|
+
export interface WebFetchPdfCut {
|
|
58
|
+
readonly totalPages: number;
|
|
59
|
+
readonly pagesRead: number;
|
|
60
|
+
}
|
|
61
|
+
export interface WebFetchOk {
|
|
62
|
+
readonly status: "ok";
|
|
63
|
+
readonly finalUrl: string;
|
|
64
|
+
readonly contentType: string;
|
|
65
|
+
readonly text: string;
|
|
66
|
+
readonly totalLength: number;
|
|
67
|
+
readonly cut: boolean;
|
|
68
|
+
readonly pdfCut?: WebFetchPdfCut;
|
|
69
|
+
readonly kept?: WebFetchKept;
|
|
70
|
+
readonly remainderNotKept?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export type WebFetchOutput = WebFetchOk | {
|
|
73
|
+
readonly status: "invalid_url";
|
|
74
|
+
readonly text: string;
|
|
75
|
+
} | {
|
|
76
|
+
readonly status: "unsupported_scheme";
|
|
77
|
+
readonly scheme: string;
|
|
78
|
+
readonly text: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly status: "refused";
|
|
81
|
+
readonly text: string;
|
|
82
|
+
readonly feedback?: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly status: "http_error";
|
|
85
|
+
readonly httpStatus: number;
|
|
86
|
+
readonly text: string;
|
|
87
|
+
} | {
|
|
88
|
+
readonly status: "redirect_limit";
|
|
89
|
+
readonly text: string;
|
|
90
|
+
} | {
|
|
91
|
+
readonly status: "body_too_large";
|
|
92
|
+
readonly limitBytes: number;
|
|
93
|
+
readonly contentType: string;
|
|
94
|
+
readonly text: string;
|
|
95
|
+
} | {
|
|
96
|
+
readonly status: "unextractable";
|
|
97
|
+
readonly contentType: string;
|
|
98
|
+
readonly text: string;
|
|
99
|
+
readonly detail?: string;
|
|
100
|
+
} | {
|
|
101
|
+
readonly status: "request_failed";
|
|
102
|
+
readonly text: string;
|
|
103
|
+
readonly detail?: string;
|
|
104
|
+
};
|
|
105
|
+
export interface WebFetchToolDeps {
|
|
106
|
+
readonly toolOutputStore?: ToolOutputStore;
|
|
107
|
+
/** Substitutes the name resolution, so a test answers a controlled address set. The default is `dns.lookup`. */
|
|
108
|
+
readonly lookup?: WebFetchLookup;
|
|
109
|
+
/** Substitutes one HTTP hop, so a test serves recorded bodies offline. The default is the real node hop. */
|
|
110
|
+
readonly hopRequest?: WebFetchHopRequest;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Estimate the deepest element nesting of an HTML document in one linear scan, without building a DOM.
|
|
114
|
+
* Deep nesting is the input shape that makes the linkedom/Readability pair burn CPU for minutes on a
|
|
115
|
+
* body under the byte limit, and the extraction is synchronous, so no deadline can interrupt it — the
|
|
116
|
+
* bound must gate the parse, not watch it. The estimate never parses: void elements and self-closing
|
|
117
|
+
* tags open no level, raw-text elements skip to their close, quoted attribute values hide their `<`,
|
|
118
|
+
* and an implied-close family pops its open peers, so a list or a table of any length stays flat and a
|
|
119
|
+
* stray close tag pops to its nearest matching open.
|
|
120
|
+
*/
|
|
121
|
+
export declare function htmlNestingDepth(html: string): number;
|
|
122
|
+
export declare function createWebFetchTool(deps?: WebFetchToolDeps): import("../define-tool.js").Tool<{
|
|
123
|
+
url: string;
|
|
124
|
+
}, WebFetchOutput>;
|