wotann 0.5.39 → 0.5.40
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/autopilot/completion-oracle.d.ts +71 -0
- package/dist/autopilot/completion-oracle.d.ts.map +1 -1
- package/dist/autopilot/completion-oracle.js +69 -2
- package/dist/autopilot/completion-oracle.js.map +1 -1
- package/dist/context/compaction-floor.d.ts +100 -0
- package/dist/context/compaction-floor.d.ts.map +1 -0
- package/dist/context/compaction-floor.js +94 -0
- package/dist/context/compaction-floor.js.map +1 -0
- package/dist/daemon/kairos-rpc.d.ts +1 -0
- package/dist/daemon/kairos-rpc.d.ts.map +1 -1
- package/dist/daemon/kairos-rpc.js +24 -0
- package/dist/daemon/kairos-rpc.js.map +1 -1
- package/dist/daemon/rpc-handlers/ports-rpc.d.ts +100 -0
- package/dist/daemon/rpc-handlers/ports-rpc.d.ts.map +1 -0
- package/dist/daemon/rpc-handlers/ports-rpc.js +278 -0
- package/dist/daemon/rpc-handlers/ports-rpc.js.map +1 -0
- package/dist/hooks/agentmemory-event-taxonomy.d.ts +144 -0
- package/dist/hooks/agentmemory-event-taxonomy.d.ts.map +1 -0
- package/dist/hooks/agentmemory-event-taxonomy.js +85 -0
- package/dist/hooks/agentmemory-event-taxonomy.js.map +1 -0
- package/dist/hooks/built-in.d.ts.map +1 -1
- package/dist/hooks/built-in.js +7 -3
- package/dist/hooks/built-in.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/memory/injection-scanner.d.ts +78 -0
- package/dist/memory/injection-scanner.d.ts.map +1 -0
- package/dist/memory/injection-scanner.js +204 -0
- package/dist/memory/injection-scanner.js.map +1 -0
- package/dist/middleware/ttsr.d.ts +79 -3
- package/dist/middleware/ttsr.d.ts.map +1 -1
- package/dist/middleware/ttsr.js +136 -16
- package/dist/middleware/ttsr.js.map +1 -1
- package/dist/orchestration/ports-bridge.d.ts +325 -0
- package/dist/orchestration/ports-bridge.d.ts.map +1 -0
- package/dist/orchestration/ports-bridge.js +712 -0
- package/dist/orchestration/ports-bridge.js.map +1 -0
- package/dist/orchestration/textgrad-refinement.d.ts +123 -0
- package/dist/orchestration/textgrad-refinement.d.ts.map +1 -0
- package/dist/orchestration/textgrad-refinement.js +111 -0
- package/dist/orchestration/textgrad-refinement.js.map +1 -0
- package/dist/prompt/engine.d.ts +15 -0
- package/dist/prompt/engine.d.ts.map +1 -1
- package/dist/prompt/engine.js +20 -7
- package/dist/prompt/engine.js.map +1 -1
- package/dist/prompt/modules/capabilities.d.ts.map +1 -1
- package/dist/prompt/modules/capabilities.js +1 -0
- package/dist/prompt/modules/capabilities.js.map +1 -1
- package/dist/prompt/modules/caveman.d.ts.map +1 -1
- package/dist/prompt/modules/caveman.js +1 -0
- package/dist/prompt/modules/caveman.js.map +1 -1
- package/dist/prompt/modules/conventions.d.ts.map +1 -1
- package/dist/prompt/modules/conventions.js +1 -0
- package/dist/prompt/modules/conventions.js.map +1 -1
- package/dist/prompt/modules/identity.d.ts.map +1 -1
- package/dist/prompt/modules/identity.js +1 -0
- package/dist/prompt/modules/identity.js.map +1 -1
- package/dist/prompt/modules/index.d.ts +29 -0
- package/dist/prompt/modules/index.d.ts.map +1 -1
- package/dist/prompt/modules/index.js +51 -9
- package/dist/prompt/modules/index.js.map +1 -1
- package/dist/prompt/modules/llms-txt.d.ts.map +1 -1
- package/dist/prompt/modules/llms-txt.js +2 -4
- package/dist/prompt/modules/llms-txt.js.map +1 -1
- package/dist/prompt/modules/safety.d.ts.map +1 -1
- package/dist/prompt/modules/safety.js +1 -0
- package/dist/prompt/modules/safety.js.map +1 -1
- package/dist/prompt/modules/security.d.ts.map +1 -1
- package/dist/prompt/modules/security.js +1 -0
- package/dist/prompt/modules/security.js.map +1 -1
- package/dist/prompt/modules/skills.d.ts.map +1 -1
- package/dist/prompt/modules/skills.js +1 -0
- package/dist/prompt/modules/skills.js.map +1 -1
- package/dist/prompt/modules/tools.d.ts.map +1 -1
- package/dist/prompt/modules/tools.js +1 -0
- package/dist/prompt/modules/tools.js.map +1 -1
- package/dist/prompt/modules/user.d.ts.map +1 -1
- package/dist/prompt/modules/user.js +1 -0
- package/dist/prompt/modules/user.js.map +1 -1
- package/dist/providers/credential-pool.d.ts +45 -1
- package/dist/providers/credential-pool.d.ts.map +1 -1
- package/dist/providers/credential-pool.js +94 -1
- package/dist/providers/credential-pool.js.map +1 -1
- package/dist/providers/sticky-rotation-wire.d.ts +133 -0
- package/dist/providers/sticky-rotation-wire.d.ts.map +1 -0
- package/dist/providers/sticky-rotation-wire.js +185 -0
- package/dist/providers/sticky-rotation-wire.js.map +1 -0
- package/dist/runtime-hooks/ttsr-rule.d.ts +82 -0
- package/dist/runtime-hooks/ttsr-rule.d.ts.map +1 -0
- package/dist/runtime-hooks/ttsr-rule.js +207 -0
- package/dist/runtime-hooks/ttsr-rule.js.map +1 -0
- package/dist/runtime-hooks/ttsr-runner.d.ts +129 -0
- package/dist/runtime-hooks/ttsr-runner.d.ts.map +1 -0
- package/dist/runtime-hooks/ttsr-runner.js +193 -0
- package/dist/runtime-hooks/ttsr-runner.js.map +1 -0
- package/dist/runtime-hooks/ttsr-scope.d.ts +115 -0
- package/dist/runtime-hooks/ttsr-scope.d.ts.map +1 -0
- package/dist/runtime-hooks/ttsr-scope.js +378 -0
- package/dist/runtime-hooks/ttsr-scope.js.map +1 -0
- package/dist/sandbox/unified-exec.d.ts.map +1 -1
- package/dist/sandbox/unified-exec.js +6 -1
- package/dist/sandbox/unified-exec.js.map +1 -1
- package/dist/skills/cli-anything.d.ts.map +1 -1
- package/dist/skills/cli-anything.js +4 -1
- package/dist/skills/cli-anything.js.map +1 -1
- package/dist/skills/limits.d.ts +86 -0
- package/dist/skills/limits.d.ts.map +1 -0
- package/dist/skills/limits.js +140 -0
- package/dist/skills/limits.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +29 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/storage/session-artifacts.d.ts +105 -0
- package/dist/storage/session-artifacts.d.ts.map +1 -0
- package/dist/storage/session-artifacts.js +198 -0
- package/dist/storage/session-artifacts.js.map +1 -0
- package/dist/tools/workspace-pack.d.ts +97 -0
- package/dist/tools/workspace-pack.d.ts.map +1 -0
- package/dist/tools/workspace-pack.js +228 -0
- package/dist/tools/workspace-pack.js.map +1 -0
- package/dist/tui/composer-history.d.ts +99 -0
- package/dist/tui/composer-history.d.ts.map +1 -0
- package/dist/tui/composer-history.js +169 -0
- package/dist/tui/composer-history.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ports-bridge — business-level bridge functions for the 4 zombie ports.
|
|
3
|
+
*
|
|
4
|
+
* The audit finding #8 surfaced that four port primitives shipped under
|
|
5
|
+
* `src/ports/*` are library-only — they're re-exported from `src/lib.ts`
|
|
6
|
+
* but no internal consumer reaches them. They were never callable from
|
|
7
|
+
* the daemon's RPC surface, because each requires dependency injection
|
|
8
|
+
* (converse callback, search provider, graph store, dom adapter) that
|
|
9
|
+
* a JSON-RPC frame cannot transport.
|
|
10
|
+
*
|
|
11
|
+
* This module is the missing seam:
|
|
12
|
+
*
|
|
13
|
+
* - Each port is a *library primitive*. The bridge constructs the
|
|
14
|
+
* dependency-injected primitive INTERNALLY from business-level inputs
|
|
15
|
+
* (a chatdev waterfall spec + query, a research query, a text blob to
|
|
16
|
+
* ingest into a memory graph, a DOM snapshot to locate against).
|
|
17
|
+
*
|
|
18
|
+
* - The daemon RPC handlers (src/daemon/rpc-handlers/ports-rpc.ts) call
|
|
19
|
+
* these bridge functions one-to-one. They are the *only* in-process
|
|
20
|
+
* consumer; the bridge does not call them back. No cycle.
|
|
21
|
+
*
|
|
22
|
+
* - Each bridge function follows the QB#3 honest-stub policy: if a real
|
|
23
|
+
* dependency cannot be reached (no LLM provider for chatdev, no
|
|
24
|
+
* search provider for deer-flow), the bridge returns an `ok:false`
|
|
25
|
+
* envelope with a verbatim `reason` string. It NEVER silently
|
|
26
|
+
* succeeds with mock data.
|
|
27
|
+
*
|
|
28
|
+
* - For mem0-graph and scrapling-adaptive the in-memory backends are
|
|
29
|
+
* always available, so the bridge wires those by default and only
|
|
30
|
+
* surfaces failure when the input itself is malformed.
|
|
31
|
+
*
|
|
32
|
+
* QB bars honoured:
|
|
33
|
+
* QB#3 honest stub — no silent success
|
|
34
|
+
* QB#7 zero module-level mutable state — every bridge call constructs
|
|
35
|
+
* its own primitives
|
|
36
|
+
* QB#10 errors surface verbatim
|
|
37
|
+
* QB#13 no direct env reads except through the audit-friendly
|
|
38
|
+
* `WOTANN_*` knobs documented per-function
|
|
39
|
+
* QB#19 every type/exports is consumed by at least one handler
|
|
40
|
+
*
|
|
41
|
+
* Imports use ESM `.js` suffixes; strict TypeScript with ES2022 target.
|
|
42
|
+
*/
|
|
43
|
+
import { promises as fsp } from "node:fs";
|
|
44
|
+
import { dirname } from "node:path";
|
|
45
|
+
import { runWaterfall, defaultWaterfall, fixtureConverse, DEFAULT_ROLES, } from "../ports/chatdev-waterfall.js";
|
|
46
|
+
import { runResearch, fixturePipeline, } from "../ports/deer-flow-research.js";
|
|
47
|
+
import { ingestText, neighborhood, createInMemoryGraphStore, } from "../ports/mem0-graph-memory.js";
|
|
48
|
+
import { adaptiveLocate, createInMemoryFingerprintStore, } from "../ports/scrapling-adaptive.js";
|
|
49
|
+
import { resolveWotannHomeSubdir } from "../utils/wotann-home.js";
|
|
50
|
+
import { writeFileAtomic } from "../utils/atomic-io.js";
|
|
51
|
+
/**
|
|
52
|
+
* Build a converse function that drives one phase through the runtime.
|
|
53
|
+
* On any provider error it surfaces the message verbatim (QB#10).
|
|
54
|
+
*/
|
|
55
|
+
function buildRuntimeConverse(runtime) {
|
|
56
|
+
return async (rolePair, inputArtifact, charter) => {
|
|
57
|
+
const phaseId = `${rolePair.initiator.id}->${rolePair.responder.id}`;
|
|
58
|
+
const systemPrompt = [
|
|
59
|
+
`You are pairing the roles ${rolePair.initiator.name} and ${rolePair.responder.name}.`,
|
|
60
|
+
`Their joint charter:`,
|
|
61
|
+
charter,
|
|
62
|
+
`Refine the incoming artifact and respond with the refined artifact only.`,
|
|
63
|
+
`Do not invent new sections; thread the artifact forward.`,
|
|
64
|
+
].join("\n");
|
|
65
|
+
let assistantText;
|
|
66
|
+
try {
|
|
67
|
+
assistantText = await runtime.complete(inputArtifact, systemPrompt);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return {
|
|
72
|
+
phaseId,
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: `runtime converse threw: ${msg}`,
|
|
75
|
+
transcript: [
|
|
76
|
+
{
|
|
77
|
+
speakerRoleId: rolePair.initiator.id,
|
|
78
|
+
text: inputArtifact,
|
|
79
|
+
meta: { kind: "runtime-input" },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
artifact: inputArtifact,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
phaseId,
|
|
87
|
+
ok: true,
|
|
88
|
+
reason: null,
|
|
89
|
+
transcript: [
|
|
90
|
+
{
|
|
91
|
+
speakerRoleId: rolePair.initiator.id,
|
|
92
|
+
text: inputArtifact,
|
|
93
|
+
meta: { kind: "runtime-input" },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
speakerRoleId: rolePair.responder.id,
|
|
97
|
+
text: assistantText,
|
|
98
|
+
meta: { kind: "runtime-output" },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
artifact: assistantText.length > 0 ? assistantText : inputArtifact,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function resolveRoleById(id) {
|
|
106
|
+
for (const r of Object.values(DEFAULT_ROLES)) {
|
|
107
|
+
if (r.id === id)
|
|
108
|
+
return r;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Bridge: run a ChatDev waterfall.
|
|
114
|
+
*
|
|
115
|
+
* @param spec Optional phase override. When absent the default 5-phase
|
|
116
|
+
* waterfall (Demand -> Design -> Implement -> Review -> Test)
|
|
117
|
+
* is used. Phase role ids must reference DEFAULT_ROLES.
|
|
118
|
+
* @param query The seed artifact handed to the first phase.
|
|
119
|
+
* @param options Runtime ref + fixture override flag.
|
|
120
|
+
*/
|
|
121
|
+
export async function runChatDevWaterfall(spec, query, options = {}) {
|
|
122
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
123
|
+
return { ok: false, reason: "query must be a non-empty string" };
|
|
124
|
+
}
|
|
125
|
+
const useRuntime = !options.forceFixture && options.runtime !== undefined;
|
|
126
|
+
// QB#3: when a runtime is requested but unreachable we never silently
|
|
127
|
+
// swap in fixtures — the caller asked for runtime mode. forceFixture
|
|
128
|
+
// is the explicit opt-in for fixture mode.
|
|
129
|
+
if (options.runtime !== undefined && options.forceFixture) {
|
|
130
|
+
// explicit fixture mode wins — caller asked for it
|
|
131
|
+
}
|
|
132
|
+
const converse = useRuntime
|
|
133
|
+
? buildRuntimeConverse(options.runtime)
|
|
134
|
+
: fixtureConverse();
|
|
135
|
+
let phases;
|
|
136
|
+
if (spec?.phases && spec.phases.length > 0) {
|
|
137
|
+
const built = [];
|
|
138
|
+
for (const p of spec.phases) {
|
|
139
|
+
const initiator = resolveRoleById(p.initiatorId);
|
|
140
|
+
const responder = resolveRoleById(p.responderId);
|
|
141
|
+
if (!initiator || !responder) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: `unknown role id in phase '${p.id}': initiatorId='${p.initiatorId}', responderId='${p.responderId}'. Valid: ${Object.keys(DEFAULT_ROLES).join(", ")}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
built.push({
|
|
148
|
+
id: p.id,
|
|
149
|
+
description: p.description,
|
|
150
|
+
initiator,
|
|
151
|
+
responder,
|
|
152
|
+
converse,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
phases = built;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Default waterfall — replace the fixture converser on every phase
|
|
159
|
+
// with the runtime-backed one when runtime mode is active.
|
|
160
|
+
const base = defaultWaterfall();
|
|
161
|
+
phases = base.map((ph) => ({ ...ph, converse }));
|
|
162
|
+
}
|
|
163
|
+
const waterfallOptions = spec?.globalCharter
|
|
164
|
+
? { seed: query, globalCharter: spec.globalCharter }
|
|
165
|
+
: { seed: query };
|
|
166
|
+
const result = await runWaterfall(phases, waterfallOptions);
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
result,
|
|
170
|
+
mode: useRuntime ? "runtime" : "fixture",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build a planner that asks the runtime to decompose the question into
|
|
175
|
+
* sub-questions and parses a JSON array out of the response. If parsing
|
|
176
|
+
* fails we fall back to one heuristic sub-question (the original
|
|
177
|
+
* question) — that's honest: we tried and got a malformed response, so
|
|
178
|
+
* we still do one researcher pass.
|
|
179
|
+
*/
|
|
180
|
+
function buildRuntimePlanner(runtime, maxSubQuestions) {
|
|
181
|
+
return async (query) => {
|
|
182
|
+
const systemPrompt = [
|
|
183
|
+
`You are decomposing a research query into sub-questions.`,
|
|
184
|
+
`Respond ONLY with a JSON array of up to ${maxSubQuestions} sub-questions.`,
|
|
185
|
+
`Format: [{ "id": "subq-1", "text": "...", "rationale": "..." }, ...]`,
|
|
186
|
+
].join("\n");
|
|
187
|
+
let response;
|
|
188
|
+
try {
|
|
189
|
+
response = await runtime.complete(query.question, systemPrompt);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
193
|
+
throw new Error(`planner runtime failed: ${msg}`);
|
|
194
|
+
}
|
|
195
|
+
const parsed = tryParseSubQuestions(response, maxSubQuestions);
|
|
196
|
+
if (parsed.length > 0)
|
|
197
|
+
return parsed;
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
id: "subq-1",
|
|
201
|
+
text: query.question,
|
|
202
|
+
rationale: `runtime planner returned no parseable sub-questions; using original question. Raw: ${response.slice(0, 200)}`,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function tryParseSubQuestions(raw, max) {
|
|
208
|
+
// Extract first JSON array between brackets.
|
|
209
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
210
|
+
if (!match)
|
|
211
|
+
return [];
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(match[0]);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
if (!Array.isArray(parsed))
|
|
220
|
+
return [];
|
|
221
|
+
const out = [];
|
|
222
|
+
for (let i = 0; i < parsed.length && out.length < max; i++) {
|
|
223
|
+
const entry = parsed[i];
|
|
224
|
+
if (typeof entry !== "object" || entry === null)
|
|
225
|
+
continue;
|
|
226
|
+
const obj = entry;
|
|
227
|
+
const text = typeof obj["text"] === "string" ? obj["text"] : null;
|
|
228
|
+
if (!text || text.length === 0)
|
|
229
|
+
continue;
|
|
230
|
+
const id = typeof obj["id"] === "string" && obj["id"].length > 0 ? obj["id"] : `subq-${out.length + 1}`;
|
|
231
|
+
const rationale = typeof obj["rationale"] === "string" ? obj["rationale"] : "";
|
|
232
|
+
out.push({ id, text, rationale });
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Build a researcher that gathers evidence via the SearchProvider and
|
|
238
|
+
* (optionally) summarises via the runtime. If the runtime is absent
|
|
239
|
+
* the summary is empty — honest: we didn't have an LLM to write one.
|
|
240
|
+
*/
|
|
241
|
+
function buildResearcher(searchProvider, runtime) {
|
|
242
|
+
return async (subQuestion) => {
|
|
243
|
+
let hits;
|
|
244
|
+
try {
|
|
245
|
+
hits = await searchProvider(subQuestion.text);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
249
|
+
return {
|
|
250
|
+
subQuestionId: subQuestion.id,
|
|
251
|
+
evidence: [],
|
|
252
|
+
summary: "",
|
|
253
|
+
error: `search provider threw: ${msg}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const evidence = hits.map((h) => ({
|
|
257
|
+
source: h.url,
|
|
258
|
+
snippet: h.snippet,
|
|
259
|
+
meta: {
|
|
260
|
+
title: h.title,
|
|
261
|
+
...(h.sourceEngine ? { engine: h.sourceEngine } : {}),
|
|
262
|
+
},
|
|
263
|
+
}));
|
|
264
|
+
let summary = "";
|
|
265
|
+
if (runtime !== undefined && evidence.length > 0) {
|
|
266
|
+
const systemPrompt = [
|
|
267
|
+
`You are summarising research evidence for ONE sub-question.`,
|
|
268
|
+
`Cite each piece by index. Be terse — 3-5 sentences max.`,
|
|
269
|
+
].join("\n");
|
|
270
|
+
const prompt = [
|
|
271
|
+
`Sub-question: ${subQuestion.text}`,
|
|
272
|
+
`Evidence:`,
|
|
273
|
+
...evidence.map((e, i) => `[${i + 1}] ${e.source}: ${e.snippet}`),
|
|
274
|
+
].join("\n");
|
|
275
|
+
try {
|
|
276
|
+
summary = await runtime.complete(prompt, systemPrompt);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
280
|
+
return {
|
|
281
|
+
subQuestionId: subQuestion.id,
|
|
282
|
+
evidence,
|
|
283
|
+
summary: "",
|
|
284
|
+
error: `summariser runtime failed: ${msg}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
subQuestionId: subQuestion.id,
|
|
290
|
+
evidence,
|
|
291
|
+
summary,
|
|
292
|
+
error: null,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build a reporter that synthesises findings into a final report. With
|
|
298
|
+
* no runtime ref the synthesis is a deterministic concatenation of the
|
|
299
|
+
* findings' summaries — honest stub.
|
|
300
|
+
*/
|
|
301
|
+
function buildReporter(runtime) {
|
|
302
|
+
return async (query, _subQuestions, findings) => {
|
|
303
|
+
const allCitations = [];
|
|
304
|
+
for (const f of findings) {
|
|
305
|
+
for (const c of f.evidence)
|
|
306
|
+
allCitations.push(c);
|
|
307
|
+
}
|
|
308
|
+
if (!runtime) {
|
|
309
|
+
const synthesis = findings
|
|
310
|
+
.filter((f) => f.summary.length > 0 || f.evidence.length > 0)
|
|
311
|
+
.map((f) => f.summary || `(${f.evidence.length} pieces of evidence)`)
|
|
312
|
+
.join("\n\n");
|
|
313
|
+
return { synthesis, citations: allCitations };
|
|
314
|
+
}
|
|
315
|
+
const systemPrompt = [
|
|
316
|
+
`You are writing the final research report.`,
|
|
317
|
+
`Cite each piece of evidence by its [source] index.`,
|
|
318
|
+
`Structure: brief intro, then per-aspect findings, then closing summary.`,
|
|
319
|
+
].join("\n");
|
|
320
|
+
const prompt = [
|
|
321
|
+
`Research question: ${query.question}`,
|
|
322
|
+
`Findings:`,
|
|
323
|
+
...findings.map((f, i) => `## Finding ${i + 1}\n${f.summary || "(no summary)"}`),
|
|
324
|
+
].join("\n\n");
|
|
325
|
+
try {
|
|
326
|
+
const synthesis = await runtime.complete(prompt, systemPrompt);
|
|
327
|
+
return { synthesis, citations: allCitations };
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
331
|
+
throw new Error(`reporter runtime failed: ${msg}`);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Bridge: run the deer-flow research pipeline.
|
|
337
|
+
*
|
|
338
|
+
* Requires (at minimum) a `searchProvider` for evidence gathering. With
|
|
339
|
+
* just the search provider the bridge still produces a complete report:
|
|
340
|
+
* the planner returns one sub-question (the user query), the researcher
|
|
341
|
+
* returns the raw hits as evidence, and the reporter concatenates the
|
|
342
|
+
* raw evidence. With a `runtime` ref the planner+reporter become
|
|
343
|
+
* LLM-driven and the researcher gains an evidence-summarisation step.
|
|
344
|
+
*
|
|
345
|
+
* When neither is supplied AND `forceFixture` is false, returns ok:false
|
|
346
|
+
* with reason "no search provider supplied". This is QB#3 — research
|
|
347
|
+
* without retrieval is hallucination.
|
|
348
|
+
*/
|
|
349
|
+
export async function runDeerFlowResearch(query, options = {}) {
|
|
350
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
351
|
+
return { ok: false, reason: "query must be a non-empty string" };
|
|
352
|
+
}
|
|
353
|
+
const maxSubQuestions = options.maxSubQuestions ?? 3;
|
|
354
|
+
if (!Number.isFinite(maxSubQuestions) || maxSubQuestions <= 0) {
|
|
355
|
+
return { ok: false, reason: "maxSubQuestions must be a positive integer" };
|
|
356
|
+
}
|
|
357
|
+
const useFixture = options.forceFixture === true || options.searchProvider === undefined;
|
|
358
|
+
if (useFixture && !options.forceFixture) {
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
reason: "no search provider supplied. Pass `searchProvider` (a SearchProvider callback) or set `forceFixture:true` to run the deterministic placeholder pipeline.",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
let pipeline;
|
|
365
|
+
if (options.forceFixture === true) {
|
|
366
|
+
pipeline = fixturePipeline();
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
const searchProvider = options.searchProvider;
|
|
370
|
+
pipeline = {
|
|
371
|
+
planner: options.runtime
|
|
372
|
+
? buildRuntimePlanner(options.runtime, maxSubQuestions)
|
|
373
|
+
: (q) => [
|
|
374
|
+
{
|
|
375
|
+
id: "subq-1",
|
|
376
|
+
text: q.question,
|
|
377
|
+
rationale: "no runtime planner; using original question",
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
researcher: buildResearcher(searchProvider, options.runtime),
|
|
381
|
+
reporter: buildReporter(options.runtime),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const researchQuery = {
|
|
385
|
+
question: query,
|
|
386
|
+
maxSubQuestions,
|
|
387
|
+
};
|
|
388
|
+
const outcome = await runResearch(researchQuery, pipeline);
|
|
389
|
+
return {
|
|
390
|
+
ok: true,
|
|
391
|
+
outcome,
|
|
392
|
+
mode: options.forceFixture === true ? "fixture" : "runtime",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async function loadGraphSnapshot(path) {
|
|
396
|
+
try {
|
|
397
|
+
const raw = await fsp.readFile(path, "utf8");
|
|
398
|
+
const parsed = JSON.parse(raw);
|
|
399
|
+
if (!parsed || typeof parsed !== "object")
|
|
400
|
+
return null;
|
|
401
|
+
const entities = Array.isArray(parsed.entities) ? parsed.entities : [];
|
|
402
|
+
const edges = Array.isArray(parsed.edges) ? parsed.edges : [];
|
|
403
|
+
return {
|
|
404
|
+
entities: entities.filter((e) => typeof e === "object" &&
|
|
405
|
+
e !== null &&
|
|
406
|
+
typeof e.id === "string" &&
|
|
407
|
+
typeof e.type === "string"),
|
|
408
|
+
edges: edges.filter((e) => typeof e === "object" &&
|
|
409
|
+
e !== null &&
|
|
410
|
+
typeof e.from === "string" &&
|
|
411
|
+
typeof e.to === "string" &&
|
|
412
|
+
typeof e.relation === "string"),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function rehydrateStore(store, snapshot) {
|
|
420
|
+
for (const e of snapshot.entities)
|
|
421
|
+
store.addEntity(e);
|
|
422
|
+
for (const ed of snapshot.edges)
|
|
423
|
+
store.addEdge(ed);
|
|
424
|
+
}
|
|
425
|
+
function snapshotStore(store) {
|
|
426
|
+
const entities = store.getEntities();
|
|
427
|
+
const edges = [];
|
|
428
|
+
for (const e of entities) {
|
|
429
|
+
for (const out of store.getEdgesFrom(e.id))
|
|
430
|
+
edges.push(out);
|
|
431
|
+
}
|
|
432
|
+
return { entities, edges };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Bridge: ingest text into a mem0-style graph memory.
|
|
436
|
+
*
|
|
437
|
+
* Uses `createInMemoryGraphStore` by default (no persistence). When
|
|
438
|
+
* `persist:true` the store is rehydrated from + flushed back to
|
|
439
|
+
* `${WOTANN_HOME}/mem-graph.json`. Returns the extracted triples plus a
|
|
440
|
+
* 2-hop neighborhood walk for every subject as a stable read view.
|
|
441
|
+
*/
|
|
442
|
+
export async function ingestTextToMemGraph(text, options = {}) {
|
|
443
|
+
if (typeof text !== "string") {
|
|
444
|
+
return { ok: false, reason: "text must be a string" };
|
|
445
|
+
}
|
|
446
|
+
if (text.trim().length === 0) {
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
triples: [],
|
|
450
|
+
entitiesAdded: 0,
|
|
451
|
+
edgesAdded: 0,
|
|
452
|
+
neighborhoods: [],
|
|
453
|
+
persisted: false,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const store = createInMemoryGraphStore();
|
|
457
|
+
const persist = options.persist === true;
|
|
458
|
+
const path = options.persistencePath ?? resolveWotannHomeSubdir("mem-graph.json");
|
|
459
|
+
if (persist) {
|
|
460
|
+
const snapshot = await loadGraphSnapshot(path);
|
|
461
|
+
if (snapshot)
|
|
462
|
+
rehydrateStore(store, snapshot);
|
|
463
|
+
}
|
|
464
|
+
const ingestOptions = options.turnId !== undefined && options.llm !== undefined
|
|
465
|
+
? { turnId: options.turnId, llm: options.llm }
|
|
466
|
+
: options.turnId !== undefined
|
|
467
|
+
? { turnId: options.turnId }
|
|
468
|
+
: options.llm !== undefined
|
|
469
|
+
? { llm: options.llm }
|
|
470
|
+
: {};
|
|
471
|
+
const result = await ingestText(store, text, ingestOptions);
|
|
472
|
+
if (!result.ok) {
|
|
473
|
+
return { ok: false, reason: result.reason };
|
|
474
|
+
}
|
|
475
|
+
// Build 2-hop neighborhood for each subject. Deduplicate by entity id.
|
|
476
|
+
const seen = new Set();
|
|
477
|
+
const neighborhoods = [];
|
|
478
|
+
for (const t of result.triples) {
|
|
479
|
+
if (seen.has(t.subject.id))
|
|
480
|
+
continue;
|
|
481
|
+
seen.add(t.subject.id);
|
|
482
|
+
neighborhoods.push({
|
|
483
|
+
entityId: t.subject.id,
|
|
484
|
+
edges: neighborhood(store, t.subject.id, 2),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
let persisted = false;
|
|
488
|
+
if (persist) {
|
|
489
|
+
try {
|
|
490
|
+
const out = snapshotStore(store);
|
|
491
|
+
const json = JSON.stringify(out, null, 2);
|
|
492
|
+
const parent = dirname(path);
|
|
493
|
+
await fsp.mkdir(parent, { recursive: true });
|
|
494
|
+
writeFileAtomic(path, json);
|
|
495
|
+
persisted = true;
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
499
|
+
return { ok: false, reason: `persist failed: ${msg}` };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
triples: result.triples,
|
|
505
|
+
entitiesAdded: result.entitiesAdded,
|
|
506
|
+
edgesAdded: result.edgesAdded,
|
|
507
|
+
neighborhoods,
|
|
508
|
+
persisted,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function buildBridgeTree(snapshot) {
|
|
512
|
+
let counter = 0;
|
|
513
|
+
const all = [];
|
|
514
|
+
function build(raw, parent, siblingIndex) {
|
|
515
|
+
const node = {
|
|
516
|
+
id: counter++,
|
|
517
|
+
tag: raw.tag,
|
|
518
|
+
classes: Array.isArray(raw.classes) ? [...raw.classes] : [],
|
|
519
|
+
text: typeof raw.text === "string" ? raw.text : "",
|
|
520
|
+
parent,
|
|
521
|
+
siblingIndex,
|
|
522
|
+
children: [],
|
|
523
|
+
};
|
|
524
|
+
all.push(node);
|
|
525
|
+
if (Array.isArray(raw.children)) {
|
|
526
|
+
raw.children.forEach((c, i) => {
|
|
527
|
+
node.children.push(build(c, node, i));
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return node;
|
|
531
|
+
}
|
|
532
|
+
const root = build(snapshot.root, null, 0);
|
|
533
|
+
return { root, all };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Tiny CSS-shaped selector: supports `tag`, `.class`, `tag.class`, and
|
|
537
|
+
* descendant combinators ` ` (space). No pseudo-classes, no attribute
|
|
538
|
+
* selectors. Sufficient for the bridge's locate-by-fingerprint use case;
|
|
539
|
+
* stronger selectors are a future port-side change.
|
|
540
|
+
*/
|
|
541
|
+
function querySelectorAll(nodes, selector) {
|
|
542
|
+
const trimmed = selector.trim();
|
|
543
|
+
if (trimmed.length === 0)
|
|
544
|
+
return [];
|
|
545
|
+
const parts = trimmed.split(/\s+/);
|
|
546
|
+
function matchOne(part, node) {
|
|
547
|
+
// Split into tag + classes
|
|
548
|
+
const tagMatch = part.match(/^([a-zA-Z][a-zA-Z0-9-]*)?/);
|
|
549
|
+
const tag = tagMatch?.[1] ?? "";
|
|
550
|
+
const classes = part.match(/\.([a-zA-Z0-9_-]+)/g);
|
|
551
|
+
if (tag.length > 0 && node.tag !== tag)
|
|
552
|
+
return false;
|
|
553
|
+
if (classes) {
|
|
554
|
+
for (const c of classes) {
|
|
555
|
+
const className = c.slice(1);
|
|
556
|
+
if (!node.classes.includes(className))
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
function isAncestor(ancestor, descendant) {
|
|
563
|
+
let p = descendant.parent;
|
|
564
|
+
while (p) {
|
|
565
|
+
if (p === ancestor)
|
|
566
|
+
return true;
|
|
567
|
+
p = p.parent;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
// Match the last part directly, then walk back through earlier parts.
|
|
572
|
+
const lastPart = parts[parts.length - 1] ?? "";
|
|
573
|
+
const candidates = nodes.filter((n) => matchOne(lastPart, n));
|
|
574
|
+
if (parts.length === 1)
|
|
575
|
+
return candidates;
|
|
576
|
+
return candidates.filter((c) => {
|
|
577
|
+
let current = c;
|
|
578
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
579
|
+
const part = parts[i];
|
|
580
|
+
if (part === undefined)
|
|
581
|
+
return false;
|
|
582
|
+
// Walk up looking for an ancestor matching part[i].
|
|
583
|
+
let found = null;
|
|
584
|
+
let p = current?.parent ?? null;
|
|
585
|
+
while (p) {
|
|
586
|
+
if (matchOne(part, p)) {
|
|
587
|
+
found = p;
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
p = p.parent;
|
|
591
|
+
}
|
|
592
|
+
if (!found)
|
|
593
|
+
return false;
|
|
594
|
+
current = found;
|
|
595
|
+
}
|
|
596
|
+
return true;
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function buildAdapter(root, allNodes) {
|
|
600
|
+
return {
|
|
601
|
+
query: (selector) => querySelectorAll(allNodes, selector),
|
|
602
|
+
queryFrom: (rootNode, selector) => {
|
|
603
|
+
const subset = [];
|
|
604
|
+
function walk(n) {
|
|
605
|
+
subset.push(n);
|
|
606
|
+
for (const c of n.children)
|
|
607
|
+
walk(c);
|
|
608
|
+
}
|
|
609
|
+
walk(rootNode);
|
|
610
|
+
return querySelectorAll(subset, selector);
|
|
611
|
+
},
|
|
612
|
+
getTag: (n) => n.tag,
|
|
613
|
+
getClasses: (n) => n.classes,
|
|
614
|
+
getText: (n) => n.text,
|
|
615
|
+
getParent: (n) => n.parent,
|
|
616
|
+
getSiblingIndex: (n) => n.siblingIndex,
|
|
617
|
+
walkAll: () => allNodes,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Bridge: adaptively locate a node by label within a DOM snapshot.
|
|
622
|
+
*
|
|
623
|
+
* The caller submits:
|
|
624
|
+
* - a DomSnapshot (a JSON tree of nodes)
|
|
625
|
+
* - a query label
|
|
626
|
+
* - either:
|
|
627
|
+
* * a `selector` string, in which case the bridge runs adaptiveLocate
|
|
628
|
+
* (which saves a fingerprint AND returns the located node), OR
|
|
629
|
+
* * just a label, in which case the bridge runs relocate against
|
|
630
|
+
* a previously-saved fingerprint. Since the store is per-call,
|
|
631
|
+
* the snapshot must include `seeds` to pre-populate the store.
|
|
632
|
+
*
|
|
633
|
+
* Returns the located node's tag, classes, and a text preview, along
|
|
634
|
+
* with the similarity score and whether the original selector was used.
|
|
635
|
+
*
|
|
636
|
+
* Per-call state: the FingerprintStore is created fresh inside this
|
|
637
|
+
* function (QB#7). For session-scoped fingerprints, a follow-up RPC
|
|
638
|
+
* surface can pass a long-lived store ref — that's deferred to the
|
|
639
|
+
* scrapling-adaptive port itself, not this bridge.
|
|
640
|
+
*/
|
|
641
|
+
export async function adaptiveLocateOnDom(domSnapshot, query, options = {}) {
|
|
642
|
+
if (!domSnapshot || typeof domSnapshot !== "object" || !domSnapshot.root) {
|
|
643
|
+
return { ok: false, reason: "domSnapshot must include a root node" };
|
|
644
|
+
}
|
|
645
|
+
if (!query || typeof query.label !== "string" || query.label.length === 0) {
|
|
646
|
+
return { ok: false, reason: "query.label must be a non-empty string" };
|
|
647
|
+
}
|
|
648
|
+
const { all } = buildBridgeTree(domSnapshot);
|
|
649
|
+
if (all.length === 0) {
|
|
650
|
+
return { ok: false, reason: "snapshot produced zero nodes" };
|
|
651
|
+
}
|
|
652
|
+
const root = all[0];
|
|
653
|
+
if (!root) {
|
|
654
|
+
return { ok: false, reason: "snapshot produced zero nodes" };
|
|
655
|
+
}
|
|
656
|
+
const adapter = buildAdapter(root, all);
|
|
657
|
+
const store = createInMemoryFingerprintStore();
|
|
658
|
+
// Pre-seed the store with any caller-supplied (label, selector) pairs.
|
|
659
|
+
if (domSnapshot.seeds) {
|
|
660
|
+
for (const seed of domSnapshot.seeds) {
|
|
661
|
+
const result = adaptiveLocate(adapter, store, seed.label, seed.selector, {});
|
|
662
|
+
if (!result.ok) {
|
|
663
|
+
// Seeds that miss don't kill the whole call — they just don't
|
|
664
|
+
// contribute fingerprints. We log the miss in the reason field
|
|
665
|
+
// only if the eventual locate fails.
|
|
666
|
+
void result;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const relocateOptions = options.minScore !== undefined ? { minScore: options.minScore } : {};
|
|
671
|
+
const result = query.selector
|
|
672
|
+
? adaptiveLocate(adapter, store, query.label, query.selector, relocateOptions)
|
|
673
|
+
: (() => {
|
|
674
|
+
const saved = store.load(query.label);
|
|
675
|
+
if (!saved) {
|
|
676
|
+
return {
|
|
677
|
+
ok: false,
|
|
678
|
+
reason: `no selector supplied and no fingerprint saved under label "${query.label}". Provide a selector or pre-seed via domSnapshot.seeds.`,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return adaptiveLocate(adapter, store, query.label, saved.originalSelector, relocateOptions);
|
|
682
|
+
})();
|
|
683
|
+
if (!result.ok) {
|
|
684
|
+
return { ok: false, reason: result.reason };
|
|
685
|
+
}
|
|
686
|
+
const text = result.node.text;
|
|
687
|
+
return {
|
|
688
|
+
ok: true,
|
|
689
|
+
label: query.label,
|
|
690
|
+
node: {
|
|
691
|
+
tag: result.node.tag,
|
|
692
|
+
classes: result.node.classes,
|
|
693
|
+
textPreview: text.length > 80 ? `${text.slice(0, 77)}...` : text,
|
|
694
|
+
},
|
|
695
|
+
score: result.score,
|
|
696
|
+
viaOriginalSelector: result.viaOriginalSelector,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
// ── Test-only exports (QB#14 real-code-path) ─────────────
|
|
700
|
+
export const _testOnly = {
|
|
701
|
+
tryParseSubQuestions,
|
|
702
|
+
buildRuntimeConverse,
|
|
703
|
+
buildRuntimePlanner,
|
|
704
|
+
buildResearcher,
|
|
705
|
+
buildReporter,
|
|
706
|
+
buildBridgeTree,
|
|
707
|
+
buildAdapter,
|
|
708
|
+
querySelectorAll,
|
|
709
|
+
loadGraphSnapshot,
|
|
710
|
+
snapshotStore,
|
|
711
|
+
};
|
|
712
|
+
//# sourceMappingURL=ports-bridge.js.map
|