weftens 0.1.1 → 0.1.3
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/README.md +4 -3
- package/agents/agent2060.md +1 -1
- package/bin/weftens.js +19 -66
- package/index.js +40 -32
- package/package.json +9 -6
- package/scripts/run-core-tests.mjs +50 -0
- package/src/agent-router.js +12 -5
- package/src/format.js +90 -66
- package/src/invoke.js +37 -4
- package/src/registry.js +163 -160
- package/src/specialists.js +50 -0
- package/content-org/.preflight/telemetry.jsonl +0 -6
- package/content-org/briefs/BRIEFS.md +0 -47
- package/content-org/config.mjs +0 -78
- package/content-org/cost.mjs +0 -36
- package/content-org/lead.js +0 -64
- package/content-org/metrics.mjs +0 -166
- package/content-org/orchestrator.mjs +0 -246
- package/content-org/package.json +0 -14
- package/content-org/producer.mjs +0 -59
- package/content-org/publish.mjs +0 -34
- package/content-org/review.mjs +0 -32
- package/content-org/studio/README.md +0 -27
- package/content-org/studio/assemble.sh +0 -48
- package/content-org/test.mjs +0 -110
package/README.md
CHANGED
|
@@ -48,8 +48,9 @@ whoever manages your ads, or paste this to an AI assistant that can use your com
|
|
|
48
48
|
- **Two seats are not in this repo.** `richy` (representation) and `pio` (prospect research) are
|
|
49
49
|
clients for separate services that aren't open source. Without those services configured they
|
|
50
50
|
return sample data that says so in its own output.
|
|
51
|
-
- **The content pipeline is
|
|
52
|
-
|
|
51
|
+
- **The content pipeline is not in this package.** Ten seats are defined in `agents/`, but the
|
|
52
|
+
pipeline they run through isn't published — it emitted placeholder output, so shipping it would
|
|
53
|
+
have implied a capability that doesn't exist. The `content` seat says so when you call it.
|
|
53
54
|
|
|
54
55
|
Free, MIT licensed, no account, no billing.
|
|
55
56
|
|
|
@@ -63,7 +64,7 @@ npm test
|
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
That's the whole install. Everything lives in this one repo — the deterministic auditors
|
|
66
|
-
(`agent-cores/`)
|
|
67
|
+
(`agent-cores/`) and the agent seat definitions (`agents/`).
|
|
67
68
|
A fresh clone runs on its own; there is no second checkout and no external directory. (Verified by
|
|
68
69
|
cloning to an unrelated path and running the suite there — the earlier version of this claim was
|
|
69
70
|
false: the end-to-end test resolved `agent-cores/` from the repo's *parent*, so it only passed on the
|
package/agents/agent2060.md
CHANGED
|
@@ -15,7 +15,7 @@ unless it names that evidence.
|
|
|
15
15
|
|
|
16
16
|
## Runtime — this seat runs the real engine
|
|
17
17
|
|
|
18
|
-
The canonical runtime is the Agent2060 engine at `
|
|
18
|
+
The canonical runtime is the Agent2060 engine at a local checkoutagent2060` (policy `AGENT2060.md`,
|
|
19
19
|
schema `schemas/agent2060-response.schema.json`). Do not free-form the output shape when the engine
|
|
20
20
|
is available — run it, with cwd set to the repo root:
|
|
21
21
|
|
package/bin/weftens.js
CHANGED
|
@@ -17,44 +17,20 @@ import readline from "node:readline";
|
|
|
17
17
|
import { orchestrate } from "../src/weftens.js";
|
|
18
18
|
import { isChangeSheet, formatChangeSheet } from "../src/format.js";
|
|
19
19
|
import { askWeftens } from "../src/agent.js";
|
|
20
|
-
import {
|
|
21
|
-
import { PioClient, FixturePioClient } from "../src/pio-client.js";
|
|
20
|
+
import { buildSpecialistClients } from "../src/specialists.js";
|
|
22
21
|
import { route } from "../src/agent-router.js";
|
|
23
22
|
import { invoke } from "../src/invoke.js";
|
|
24
23
|
import { REGISTRY, dispatchableAgents } from "../src/registry.js";
|
|
25
24
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
function envGroup(label, names) {
|
|
30
|
-
const set = names.filter((n) => process.env[n]);
|
|
31
|
-
if (set.length === 0) return null;
|
|
32
|
-
if (set.length < names.length) {
|
|
33
|
-
const missing = names.filter((n) => !process.env[n]).join(", ");
|
|
34
|
-
throw new Error(`${label} is partially configured — set ${missing} (or unset ${set.join(", ")} to run with sample data)`);
|
|
35
|
-
}
|
|
36
|
-
return Object.fromEntries(names.map((n) => [n, process.env[n]]));
|
|
37
|
-
}
|
|
38
|
-
|
|
25
|
+
// Client construction lives in src/specialists.js so the CLI and the SDK build them the
|
|
26
|
+
// same way. The CLI adds one thing: a stderr warning when a specialist falls back to
|
|
27
|
+
// sample data, so the person watching knows before they read a number.
|
|
39
28
|
function buildAgents() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const pio = pioEnv
|
|
46
|
-
? new PioClient({
|
|
47
|
-
baseUrl: pioEnv.WEFTENS_PIO_URL,
|
|
48
|
-
member: pioEnv.WEFTENS_PIO_MEMBER,
|
|
49
|
-
password: pioEnv.WEFTENS_PIO_PASSWORD,
|
|
50
|
-
tenant: process.env.WEFTENS_PIO_TENANT ?? null,
|
|
51
|
-
})
|
|
52
|
-
: new FixturePioClient();
|
|
53
|
-
const fixtures = [!richyEnv && "Richy", !pioEnv && "PIO"].filter(Boolean);
|
|
54
|
-
if (fixtures.length > 0) {
|
|
55
|
-
process.stderr.write(`weftens: ${fixtures.join(" and ")} not configured — using clearly-flagged sample data\n`);
|
|
56
|
-
}
|
|
57
|
-
return { richy, pio };
|
|
29
|
+
return buildSpecialistClients({
|
|
30
|
+
onFixture: (names) =>
|
|
31
|
+
process.stderr.write(`weftens: ${names.join(" and ")} not configured — using clearly-flagged sample data
|
|
32
|
+
`),
|
|
33
|
+
});
|
|
58
34
|
}
|
|
59
35
|
|
|
60
36
|
function parseFlags(args) {
|
|
@@ -169,39 +145,10 @@ async function cmdSend(args, { routeOnly } = {}) {
|
|
|
169
145
|
}
|
|
170
146
|
if (routeOnly) return;
|
|
171
147
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
// clients when unconfigured), so refusing categorically made the two headline seats
|
|
177
|
-
// look dead. Now `send richy --org NAME "<question>"` does the real thing.
|
|
178
|
-
if (decision.agent.tier === 1 && decision.agent.wire) {
|
|
179
|
-
// An org is genuinely required — Richy audits a specific organization, PIO reports on one —
|
|
180
|
-
// but demanding a FLAG for it is a door that doesn't open for a plain sentence. Whoever is
|
|
181
|
-
// running this almost always means the same org every time, so it's config, not an argument:
|
|
182
|
-
// set WEFTENS_ORG once and `send richy "audit our listings"` just works.
|
|
183
|
-
org = org ?? process.env.WEFTENS_ORG ?? null;
|
|
184
|
-
website = website ?? process.env.WEFTENS_WEBSITE ?? null;
|
|
185
|
-
if (!org) {
|
|
186
|
-
process.stdout.write(
|
|
187
|
-
`\n[orchestrator] ${decision.agent.name} answers about a specific organization — which one?\n` +
|
|
188
|
-
` once: weftens send ${decision.agent.name} --org "Acme Co" [--website acme.com] "${request.text || "your question"}"\n` +
|
|
189
|
-
` always: set WEFTENS_ORG="Acme Co" (and optionally WEFTENS_WEBSITE) — then this command works as typed\n`
|
|
190
|
-
);
|
|
191
|
-
process.exitCode = 1;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const intent = decision.agent.name === "richy" ? "representation" : "operations";
|
|
195
|
-
const result = await orchestrate(
|
|
196
|
-
{ org: { name: org, ...(website ? { website } : {}) }, intent, question: request.text || undefined },
|
|
197
|
-
buildAgents()
|
|
198
|
-
);
|
|
199
|
-
process.stdout.write(`\n[orchestrator] ran ${decision.agent.name} over ${org} (intent: ${intent})\n`);
|
|
200
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const outcome = await invoke(decision.agent, { text: request.text, inputFile, provider });
|
|
148
|
+
// Specialist handling (org resolution, intent mapping, orchestrate) lives in invoke() so
|
|
149
|
+
// the CLI and the SDK cannot disagree about what invoking richy does. This used to be a
|
|
150
|
+
// second copy here, and the SDK regressed against it.
|
|
151
|
+
const outcome = await invoke(decision.agent, { text: request.text, inputFile, provider, org, website });
|
|
205
152
|
process.stdout.write(`\n[${outcome.path}] ${outcome.note}\n`);
|
|
206
153
|
if (outcome.result !== undefined) {
|
|
207
154
|
// A change sheet renders for a human by default — the whole point of one is that a
|
|
@@ -211,7 +158,13 @@ async function cmdSend(args, { routeOnly } = {}) {
|
|
|
211
158
|
} else {
|
|
212
159
|
process.stdout.write(`\n--- result ---\n${typeof outcome.result === "string" ? outcome.result : JSON.stringify(outcome.result, null, 2)}\n`);
|
|
213
160
|
}
|
|
161
|
+
return;
|
|
214
162
|
}
|
|
163
|
+
|
|
164
|
+
// No result means the run produced nothing usable — a missing input file, a provider CLI
|
|
165
|
+
// that isn't installed, a specialist with no org. All of those exited 0 before, which made
|
|
166
|
+
// the tool unscriptable and made the fresh-clone CI step incapable of failing.
|
|
167
|
+
process.exitCode = 1;
|
|
215
168
|
}
|
|
216
169
|
|
|
217
170
|
async function main() {
|
package/index.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
// Weftens SDK — the programmatic front door. The same deterministic routing and invoke
|
|
2
|
-
// the CLI uses, exposed as an importable API so Weftens can be embedded in other code.
|
|
3
|
-
//
|
|
4
|
-
// import { weftens, route, invoke, REGISTRY } from "weftens";
|
|
5
|
-
//
|
|
6
|
-
// // one call: route a request and run the chosen agent
|
|
7
|
-
// const { decision, outcome } = await weftens("audit our facebook ads", { inputFile: "meta.csv" });
|
|
8
|
-
//
|
|
9
|
-
// // or drive the pieces yourself
|
|
10
|
-
// const decision = route({ text: "who are the funders for this grant" });
|
|
11
|
-
// const outcome = await invoke(decision.agent, { text: "..." });
|
|
12
|
-
|
|
13
|
-
import { route } from "./src/agent-router.js";
|
|
14
|
-
import { invoke } from "./src/invoke.js";
|
|
15
|
-
import { REGISTRY, agentByName, dispatchableAgents } from "./src/registry.js";
|
|
16
|
-
import { complete, loadProviders } from "./src/provider.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
1
|
+
// Weftens SDK — the programmatic front door. The same deterministic routing and invoke
|
|
2
|
+
// the CLI uses, exposed as an importable API so Weftens can be embedded in other code.
|
|
3
|
+
//
|
|
4
|
+
// import { weftens, route, invoke, REGISTRY } from "weftens";
|
|
5
|
+
//
|
|
6
|
+
// // one call: route a request and run the chosen agent
|
|
7
|
+
// const { decision, outcome } = await weftens("audit our facebook ads", { inputFile: "meta.csv" });
|
|
8
|
+
//
|
|
9
|
+
// // or drive the pieces yourself
|
|
10
|
+
// const decision = route({ text: "who are the funders for this grant" });
|
|
11
|
+
// const outcome = await invoke(decision.agent, { text: "..." });
|
|
12
|
+
|
|
13
|
+
import { route } from "./src/agent-router.js";
|
|
14
|
+
import { invoke } from "./src/invoke.js";
|
|
15
|
+
import { REGISTRY, agentByName, dispatchableAgents } from "./src/registry.js";
|
|
16
|
+
import { complete, loadProviders } from "./src/provider.js";
|
|
17
|
+
import { isChangeSheet, formatChangeSheet } from "./src/format.js";
|
|
18
|
+
import { buildSpecialistClients } from "./src/specialists.js";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
route, invoke, REGISTRY, agentByName, dispatchableAgents, complete, loadProviders,
|
|
22
|
+
// The renderer is part of the product, not a CLI detail: an SDK consumer gets the same
|
|
23
|
+
// human-readable change sheet the terminal shows, instead of having to build one.
|
|
24
|
+
isChangeSheet, formatChangeSheet,
|
|
25
|
+
buildSpecialistClients,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Route a request and run the chosen agent — the one-call front door.
|
|
30
|
+
* @param request a string (free text) or { text?, agent? } (agent = direct address)
|
|
31
|
+
* @param opts { inputFile?, provider?, dry?, org?, website? } — passed through to invoke
|
|
32
|
+
* @returns { decision, outcome } outcome is null when nothing routed
|
|
33
|
+
*/
|
|
34
|
+
export async function weftens(request, opts = {}) {
|
|
35
|
+
const req = typeof request === "string" ? { text: request } : request;
|
|
36
|
+
const decision = route(req);
|
|
37
|
+
if (!decision.agent) return { decision, outcome: null };
|
|
38
|
+
const outcome = await invoke(decision.agent, { text: req.text, ...opts });
|
|
39
|
+
return { decision, outcome };
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weftens",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weftens \u2014 one front door that routes a marketing/representation request to the right agent and runs it on your own AI subscription (bring-your-own-model). Usable as a CLI or an SDK.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
|
-
"./cli": "./bin/weftens.js"
|
|
9
|
+
"./cli": "./bin/weftens.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
12
13
|
"weftens": "bin/weftens.js"
|
|
13
14
|
},
|
|
14
15
|
"license": "MIT",
|
|
15
16
|
"files": [
|
|
17
|
+
"scripts",
|
|
16
18
|
"bin",
|
|
17
19
|
"src",
|
|
18
20
|
"agents",
|
|
19
21
|
"agent-cores",
|
|
20
|
-
"content-org",
|
|
21
22
|
"index.js",
|
|
22
23
|
"providers.json.example",
|
|
23
24
|
"README.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"!**/.preflight",
|
|
27
|
+
"!**/.preflight/**"
|
|
25
28
|
],
|
|
26
29
|
"scripts": {
|
|
27
30
|
"test": "npm run test:unit && npm run test:cores",
|
|
28
31
|
"test:unit": "vitest run",
|
|
29
|
-
"test:cores": "node
|
|
32
|
+
"test:cores": "node scripts/run-core-tests.mjs",
|
|
30
33
|
"start": "node bin/weftens.js"
|
|
31
34
|
},
|
|
32
35
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
36
|
+
"node": ">=20"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
39
|
"vitest": "^2.0.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the agent-cores test files under Node's built-in runner.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists rather than `node --test "agent-cores/**/test/*.test.mjs"`: glob expansion
|
|
5
|
+
// inside `node --test` needs Node 21+, and this package supports Node 20. Passing an explicit
|
|
6
|
+
// file list works everywhere, so the supported range in package.json is a claim CI can
|
|
7
|
+
// actually check rather than an aspiration.
|
|
8
|
+
//
|
|
9
|
+
// The cores are deliberately zero-dependency and run under `node --test` (not vitest) so a
|
|
10
|
+
// single auditor can be handed to someone who has nothing but Node installed. Keep it that way.
|
|
11
|
+
|
|
12
|
+
import { readdirSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
const CORES = join(ROOT, "agent-cores");
|
|
19
|
+
|
|
20
|
+
function findTests(dir, found = []) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
} catch {
|
|
25
|
+
return found; // a missing agent-cores/ is a packaging problem, reported below, not a crash here
|
|
26
|
+
}
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const full = join(dir, e.name);
|
|
29
|
+
if (e.isDirectory()) {
|
|
30
|
+
if (e.name === "node_modules") continue;
|
|
31
|
+
findTests(full, found);
|
|
32
|
+
} else if (/\.test\.m?js$/.test(e.name)) {
|
|
33
|
+
found.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = findTests(CORES).sort();
|
|
40
|
+
|
|
41
|
+
if (files.length === 0) {
|
|
42
|
+
// Silence here would be indistinguishable from "all core tests passed" — exactly the failure
|
|
43
|
+
// this repo already shipped once: a green check over a step that could not fail.
|
|
44
|
+
console.error(`No core test files found under ${CORES}. Expected at least one.`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Running ${files.length} core test files under node --test`);
|
|
49
|
+
const result = spawnSync(process.execPath, ["--test", ...files], { stdio: "inherit", cwd: ROOT });
|
|
50
|
+
process.exit(result.status ?? 1);
|
package/src/agent-router.js
CHANGED
|
@@ -41,14 +41,21 @@ function hintRegex(hint) {
|
|
|
41
41
|
// "we need people ops" -> pio (matched: people)
|
|
42
42
|
// They still count, at a third of a point, so a request with real signal alongside them
|
|
43
43
|
// routes as before — but a lone weak hit can no longer clear the confidence floor.
|
|
44
|
+
// Only words that (a) are actually declared as a hint by some agent and (b) carry almost no
|
|
45
|
+
// meaning alone in ordinary English. Five entries here previously matched nothing in the
|
|
46
|
+
// registry at all — proof the list had been written from intuition about English rather than
|
|
47
|
+
// from the data it operates on. `registry-invariants.test.js` now fails if that recurs.
|
|
44
48
|
const WEAK_HINTS = new Set([
|
|
45
49
|
"site", "online", "listing", "people", "post", "content", "brief", "topic", "entity",
|
|
46
|
-
"profile", "record", "page", "search", "report", "data", "review",
|
|
47
50
|
]);
|
|
48
51
|
|
|
49
|
-
// A route must earn at least this much to
|
|
50
|
-
//
|
|
52
|
+
// A route must earn at least this much to dispatch. One ordinary hint (1) clears it; a lone
|
|
53
|
+
// weak hint (0.5) does not; two weak hits (1.0) do. The old value was 0.34, which meant it
|
|
54
|
+
// silently took THREE weak hits — and the comment claimed two. The constant was tuned until
|
|
55
|
+
// two adversarial strings failed rather than derived from the rule, and it cost real requests:
|
|
56
|
+
// "review the site" was refused.
|
|
51
57
|
const CONFIDENCE_FLOOR = 1;
|
|
58
|
+
const WEAK_WEIGHT = 0.5;
|
|
52
59
|
|
|
53
60
|
// Longer (multi-word) hints weigh more — "made for advertising" is a far stronger
|
|
54
61
|
// signal than a bare "seo" — so specific phrases beat generic single words.
|
|
@@ -59,7 +66,7 @@ function scoreAgent(agent, text) {
|
|
|
59
66
|
for (const hint of agent.routeHints) {
|
|
60
67
|
if (hintRegex(hint).test(text)) {
|
|
61
68
|
if (hint.includes(" ")) { matched.push(hint); score += 3; }
|
|
62
|
-
else if (WEAK_HINTS.has(hint)) { weak.push(hint); score +=
|
|
69
|
+
else if (WEAK_HINTS.has(hint)) { weak.push(hint); score += WEAK_WEIGHT; }
|
|
63
70
|
else { matched.push(hint); score += 1; }
|
|
64
71
|
}
|
|
65
72
|
}
|
|
@@ -121,4 +128,4 @@ export function route(request = {}) {
|
|
|
121
128
|
};
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
export { REGISTRY };
|
|
131
|
+
export { REGISTRY, WEAK_HINTS };
|
package/src/format.js
CHANGED
|
@@ -1,66 +1,90 @@
|
|
|
1
|
-
// Human-readable rendering of an auditor's change sheet.
|
|
2
|
-
//
|
|
3
|
-
// The auditors always emitted correct JSON, and JSON is the right thing for a program to
|
|
4
|
-
// consume — but it is the wrong thing to put in front of the person deciding whether to
|
|
5
|
-
// act. The whole value of a change sheet is that a human can read one line and know what
|
|
6
|
-
// to do, why, and what it costs to be wrong; a wall of braces hides exactly that. So the
|
|
7
|
-
// CLI renders by default and `--json` returns the raw object for piping.
|
|
8
|
-
//
|
|
9
|
-
// Deliberately plain text: no colour, no box-drawing, no spinners. This output gets pasted
|
|
10
|
-
// into an email to a client or a ticket, and it has to survive being pasted.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
1
|
+
// Human-readable rendering of an auditor's change sheet.
|
|
2
|
+
//
|
|
3
|
+
// The auditors always emitted correct JSON, and JSON is the right thing for a program to
|
|
4
|
+
// consume — but it is the wrong thing to put in front of the person deciding whether to
|
|
5
|
+
// act. The whole value of a change sheet is that a human can read one line and know what
|
|
6
|
+
// to do, why, and what it costs to be wrong; a wall of braces hides exactly that. So the
|
|
7
|
+
// CLI renders by default and `--json` returns the raw object for piping.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately plain text: no colour, no box-drawing, no spinners. This output gets pasted
|
|
10
|
+
// into an email to a client or a ticket, and it has to survive being pasted.
|
|
11
|
+
|
|
12
|
+
// Currency comes from the report, not from an assumption. Ad exports are denominated in
|
|
13
|
+
// whatever the account uses; hardcoding "$" renders a GBP account as dollars, which is a
|
|
14
|
+
// wrong number on a page whose whole job is numbers. With no currency declared, print the
|
|
15
|
+
// bare amount rather than assert one.
|
|
16
|
+
const money = (n, currency) => {
|
|
17
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
18
|
+
const opts = currency
|
|
19
|
+
? { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
|
20
|
+
: { minimumFractionDigits: 2, maximumFractionDigits: 2 };
|
|
21
|
+
try {
|
|
22
|
+
return n.toLocaleString("en-US", opts);
|
|
23
|
+
} catch {
|
|
24
|
+
// An unrecognised currency code must not take down the render.
|
|
25
|
+
return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when a result looks like an auditor change sheet (rather than, say, a model answer
|
|
31
|
+
* or an orchestrator payload). Kept structural rather than name-based so a new core gets
|
|
32
|
+
* the rendering for free.
|
|
33
|
+
*/
|
|
34
|
+
export function isChangeSheet(result) {
|
|
35
|
+
return Boolean(result && typeof result === "object" && Array.isArray(result.changes));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render a change sheet as plain text. Never invents a field: anything missing is simply
|
|
40
|
+
* not printed, so a core that doesn't produce risk/evidence still renders cleanly.
|
|
41
|
+
*/
|
|
42
|
+
export function formatChangeSheet(result) {
|
|
43
|
+
// Defensive by design: this is the last thing before the user's terminal, and everything
|
|
44
|
+
// upstream of it (tryCore, the clients, complete) fails soft with a plain note. A
|
|
45
|
+
// malformed core payload must degrade, never throw a stack trace at someone auditing an
|
|
46
|
+
// ad account.
|
|
47
|
+
if (!isChangeSheet(result)) return "";
|
|
48
|
+
const out = [];
|
|
49
|
+
const currency = typeof result.currency === "string" ? result.currency : null;
|
|
50
|
+
const changes = result.changes.filter((c) => c && typeof c === "object");
|
|
51
|
+
const dropped = result.changes.length - changes.length;
|
|
52
|
+
|
|
53
|
+
const flagged = money(result.wastedSpendRecoverable, currency);
|
|
54
|
+
const header = [`${changes.length} proposed change${changes.length === 1 ? "" : "s"}`];
|
|
55
|
+
// "flagged", not "recoverable": the tool measured spend against a threshold, it did not
|
|
56
|
+
// establish that the money comes back. Some flagged terms convert — see the risk line.
|
|
57
|
+
if (flagged) header.push(`${flagged} of spend flagged`);
|
|
58
|
+
out.push(header.join(" | "));
|
|
59
|
+
|
|
60
|
+
const a = result.account && typeof result.account === "object" ? result.account : null;
|
|
61
|
+
if (a) {
|
|
62
|
+
const bits = [];
|
|
63
|
+
if (typeof a.totalCost === "number") bits.push(`spend ${money(a.totalCost, currency)}`);
|
|
64
|
+
if (typeof a.totalClicks === "number") bits.push(`${a.totalClicks} clicks`);
|
|
65
|
+
if (typeof a.totalConversions === "number") bits.push(`${a.totalConversions} conversions`);
|
|
66
|
+
if (typeof a.accountCPA === "number") bits.push(`CPA ${money(a.accountCPA, currency)}`);
|
|
67
|
+
if (bits.length) out.push(`account: ${bits.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
out.push("");
|
|
70
|
+
|
|
71
|
+
for (const c of changes) {
|
|
72
|
+
const amount = money(c.dollars, currency);
|
|
73
|
+
// A core that doesn't describe its change has nothing worth rendering as a headline;
|
|
74
|
+
// say so rather than printing "[object Object]".
|
|
75
|
+
const label = typeof c.change === "string" && c.change.trim() ? c.change : "(change not described)";
|
|
76
|
+
out.push(`${c.rank ?? "-"}. ${label}${amount ? ` ${amount}` : ""}`);
|
|
77
|
+
const line = (tag, v) => { if (typeof v === "string" && v.trim()) out.push(` ${tag} ${v}`); };
|
|
78
|
+
line("where ", c.where);
|
|
79
|
+
line("why ", c.why);
|
|
80
|
+
line("effect", c.expectedEffect);
|
|
81
|
+
line("risk ", c.risk);
|
|
82
|
+
out.push("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (dropped > 0) out.push(`(${dropped} malformed entr${dropped === 1 ? "y" : "ies"} skipped)`, "");
|
|
86
|
+
if (result.proposeOnly) {
|
|
87
|
+
out.push("Proposals only — nothing was sent to your ad account. Apply what you agree with.");
|
|
88
|
+
}
|
|
89
|
+
return out.join("\n");
|
|
90
|
+
}
|
package/src/invoke.js
CHANGED
|
@@ -60,10 +60,35 @@ async function tryCore(agent, inputFile) {
|
|
|
60
60
|
export async function invoke(agent, opts = {}) {
|
|
61
61
|
if (!agent) return { agent: null, path: "none", note: "no agent to invoke" };
|
|
62
62
|
|
|
63
|
+
// Specialist legs (richy, pio) answer about an ORGANIZATION, so they run through
|
|
64
|
+
// orchestrate() rather than as leaf agents. This used to return a note saying so and
|
|
65
|
+
// stop — which meant the CLI (which had its own copy of this logic) did the real work
|
|
66
|
+
// while the SDK returned a dead end for the same call. One implementation, both doors.
|
|
63
67
|
if (agent.tier === 1 && agent.wire) {
|
|
68
|
+
const org = opts.org ?? process.env.WEFTENS_ORG ?? null;
|
|
69
|
+
const website = opts.website ?? process.env.WEFTENS_WEBSITE ?? null;
|
|
70
|
+
if (!org) {
|
|
71
|
+
return {
|
|
72
|
+
agent: agent.name, path: "orchestrator", needs: "org",
|
|
73
|
+
note: `${agent.name} answers about a specific organization — pass an org.
|
|
74
|
+
` +
|
|
75
|
+
` once: weftens send ${agent.name} --org "Acme Co" [--website acme.com] "<question>"
|
|
76
|
+
` +
|
|
77
|
+
` always: set WEFTENS_ORG="Acme Co" (and optionally WEFTENS_WEBSITE)
|
|
78
|
+
` +
|
|
79
|
+
` SDK: weftens("<question>", { org: "Acme Co" })`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const { orchestrate } = await import("./weftens.js");
|
|
83
|
+
const { buildSpecialistClients } = await import("./specialists.js");
|
|
84
|
+
const intent = agent.name === "richy" ? "representation" : "operations";
|
|
85
|
+
const result = await orchestrate(
|
|
86
|
+
{ org: { name: org, ...(website ? { website } : {}) }, intent, question: opts.text || undefined },
|
|
87
|
+
opts.agents ?? buildSpecialistClients()
|
|
88
|
+
);
|
|
64
89
|
return {
|
|
65
|
-
agent: agent.name, path: "orchestrator",
|
|
66
|
-
note:
|
|
90
|
+
agent: agent.name, path: "orchestrator", result,
|
|
91
|
+
note: `ran ${agent.name} over ${org} (intent: ${intent})`,
|
|
67
92
|
};
|
|
68
93
|
}
|
|
69
94
|
|
|
@@ -73,9 +98,17 @@ export async function invoke(agent, opts = {}) {
|
|
|
73
98
|
// it. Say that plainly rather than implying the request drove the run. Defaults to
|
|
74
99
|
// dry-run: a real run is gated on the owner's open content decisions.
|
|
75
100
|
if (agent.tier === 1 && agent.leads === "content-org") {
|
|
76
|
-
|
|
101
|
+
// The pipeline is NOT part of the published package: it emitted placeholder output, and its
|
|
102
|
+
// config carried draft pricing and internal approval routing. Anyone who has a real one can
|
|
103
|
+
// point at it with WEFTENS_CONTENT_DIR. Overridable rather than hardcoded so the
|
|
104
|
+
// not-installed path — which is what every consumer gets — is actually testable.
|
|
105
|
+
const contentDir = process.env.WEFTENS_CONTENT_DIR || join(REPO, "content-org");
|
|
106
|
+
const leadPath = join(contentDir, "lead.js");
|
|
77
107
|
if (!existsSync(leadPath)) {
|
|
78
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
agent: agent.name, path: "content-pipeline",
|
|
110
|
+
note: "content pipeline not installed — it is not part of this package. Point WEFTENS_CONTENT_DIR at a checkout to enable it.",
|
|
111
|
+
};
|
|
79
112
|
}
|
|
80
113
|
const mod = await import(pathToFileURL(leadPath).href);
|
|
81
114
|
const dry = opts.dry !== false; // real run only when explicitly asked
|