@xhulz/nina 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +1 -1
- package/releases/0.34.0/core/GAPS.md +125 -0
- package/releases/0.34.0/core/answered.json +29 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
- package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.34.0/core/tree/.claude/router.md +198 -0
- package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.34.0/core/vocabulary.json +13 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/agentrun.mjs +2 -3
- package/src/commands/export.mjs +9 -7
- package/src/commands/langfuse.mjs +25 -10
- package/src/commands/stats.mjs +7 -5
- package/src/langfuse.mjs +31 -11
- package/src/transcripts.mjs +27 -1
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* `nina langfuse` — the keys, set once, and which projects send their runs to Langfuse on their own.
|
|
3
3
|
*
|
|
4
4
|
* nina langfuse login [--host <url>] ask for the keys, check them against Langfuse, keep them
|
|
5
|
-
* nina langfuse on [--project <dir>] [--content] send this project's runs after every turn
|
|
5
|
+
* nina langfuse on [--project <dir>] [--content | --prompts] send this project's runs after every turn
|
|
6
6
|
* nina langfuse off [--project <dir>]
|
|
7
7
|
* nina langfuse status
|
|
8
8
|
*
|
|
9
9
|
* Once a project is on, nobody runs an export: the `lessons` detector every composed project runs
|
|
10
10
|
* already snapshots the project each turn, and it starts the export in the background whenever a run
|
|
11
11
|
* is ready to go. What it sends is metadata unless the project is turned on with `--content`, which adds
|
|
12
|
-
* each stage's own context
|
|
12
|
+
* each stage's own context, or with `--prompts`, which adds only what passed between the agents: the
|
|
13
|
+
* prompt each stage was given and the report it handed back — see `src/langfuse.mjs`.
|
|
13
14
|
*
|
|
14
15
|
* The keys live in `~/.nina/exports/langfuse.json`, readable by the owner only, beside the record of what
|
|
15
16
|
* was sent; `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_HOST` override them. They are asked
|
|
@@ -21,20 +22,26 @@
|
|
|
21
22
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
22
23
|
import { join, resolve } from 'node:path';
|
|
23
24
|
import { createInterface } from 'node:readline';
|
|
24
|
-
import { DEFAULT_HOST, projectOf } from '../langfuse.mjs';
|
|
25
|
+
import { DEFAULT_HOST, contextMode, projectOf } from '../langfuse.mjs';
|
|
25
26
|
import { exportsDir, slugFor } from '../paths.mjs';
|
|
26
27
|
import { projectName } from './stats.mjs';
|
|
27
28
|
|
|
28
29
|
/** Where the keys and the switches live. */
|
|
29
30
|
export const configPath = () => join(exportsDir(), 'langfuse.json');
|
|
30
31
|
|
|
32
|
+
/** What a project sends, in words, by `contextMode`. */
|
|
33
|
+
const LABELS = { full: "with each stage's own context", prompts: "with each stage's prompt and report", none: 'as metadata only' };
|
|
34
|
+
|
|
35
|
+
/** The same, as `status` lists it. */
|
|
36
|
+
const SHORT = { full: 'with context', prompts: 'with prompts and reports', none: 'metadata only' };
|
|
37
|
+
|
|
31
38
|
/** Where the background export leaves what it last did, per project. */
|
|
32
39
|
export const statusPath = (slug) => join(exportsDir(), 'langfuse', `${slug}.status.json`);
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* The configuration on disk: keys, host, and the projects that are on.
|
|
36
43
|
*
|
|
37
|
-
* @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean, since: string}>}>}
|
|
44
|
+
* @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean|'prompts', since: string}>}>}
|
|
38
45
|
*/
|
|
39
46
|
export async function readConfig() {
|
|
40
47
|
try {
|
|
@@ -113,7 +120,7 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
113
120
|
}
|
|
114
121
|
await writeConfig({ ...config, host, publicKey, secretKey });
|
|
115
122
|
console.log(` ✓ the keys open "${opened.project}" at ${host}; kept in ${configPath()}, readable by you only`);
|
|
116
|
-
console.log(' next, in each project that should send: `nina langfuse on` (add `--content` for each stage\'s
|
|
123
|
+
console.log(' next, in each project that should send: `nina langfuse on` (add `--prompts` for what passed between the agents, or `--content` for each stage\'s whole context)\n');
|
|
117
124
|
return 0;
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -123,13 +130,21 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
123
130
|
console.error(' no keys yet — `nina langfuse login` first\n');
|
|
124
131
|
return 2;
|
|
125
132
|
}
|
|
126
|
-
|
|
133
|
+
if (argv.includes('--content') && argv.includes('--prompts')) {
|
|
134
|
+
console.error(' --content already sends the prompts; pass one of the two\n');
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
const content = argv.includes('--content') ? true : argv.includes('--prompts') ? 'prompts' : false;
|
|
127
138
|
await writeConfig({ ...config, projects: { ...config.projects, [slug]: { content, since: new Date().toISOString() } } });
|
|
128
|
-
console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${content
|
|
129
|
-
if (content) {
|
|
139
|
+
console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${LABELS[contextMode({ content }) ?? 'none']}`);
|
|
140
|
+
if (content === true) {
|
|
130
141
|
console.log(" each stage's prompt, messages, tool calls and report go with it — code and whatever the stages read included;");
|
|
131
142
|
console.log(' obvious secrets are masked, and nothing more is: turn it on only where that code may leave this machine');
|
|
132
143
|
}
|
|
144
|
+
if (content === 'prompts') {
|
|
145
|
+
console.log(" each stage's prompt and report go with it, and nothing it read, wrote or called between them;");
|
|
146
|
+
console.log(' a prompt carries whatever the orchestrator put in it, diffs and code included, and only obvious secrets are masked');
|
|
147
|
+
}
|
|
133
148
|
console.log(` the runs from before now stay here; \`nina export --langfuse --project ${slug}\` sends them, once`);
|
|
134
149
|
console.log(' it runs from the `lessons` detector in this project\'s hooks — `nina check` says whether they are wired\n');
|
|
135
150
|
return 0;
|
|
@@ -150,12 +165,12 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
150
165
|
for (const [name, setting] of on) {
|
|
151
166
|
const last = JSON.parse(await readFile(statusPath(name), 'utf8').catch(() => 'null'));
|
|
152
167
|
const said = !last ? 'nothing sent yet' : last.error ? `✗ ${last.at}: ${last.error}` : `last sent ${last.at}: ${last.spans} trace(s), ${last.scores} score(s)`;
|
|
153
|
-
console.log(` ${projectName(name)}: ${setting
|
|
168
|
+
console.log(` ${projectName(name)}: ${SHORT[contextMode(setting) ?? 'none']}, since ${setting.since.slice(0, 10)} — ${said}`);
|
|
154
169
|
}
|
|
155
170
|
console.log('');
|
|
156
171
|
return 0;
|
|
157
172
|
}
|
|
158
173
|
|
|
159
|
-
console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content] | off [--project <dir>] | status\n');
|
|
174
|
+
console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content | --prompts] | off [--project <dir>] | status\n');
|
|
160
175
|
return 2;
|
|
161
176
|
}
|
package/src/commands/stats.mjs
CHANGED
|
@@ -224,8 +224,8 @@ function costReport(records) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
* How each stage's rounds spent their context: turns, tool calls sent per turn,
|
|
228
|
-
* re-read when the round began and at its largest. Every turn is billed the whole context again, so the
|
|
227
|
+
* How each stage's rounds spent their context: turns, tool calls sent per turn, the context a turn
|
|
228
|
+
* re-read when the round began and at its largest, and how long the prompt that opened the round was. Every turn is billed the whole context again, so the
|
|
229
229
|
* cost of a round is its turns times its context, and a round that starts where a long one ended pays for
|
|
230
230
|
* all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
|
|
231
231
|
*
|
|
@@ -236,8 +236,9 @@ function contextReport(records) {
|
|
|
236
236
|
if (shaped.length === 0) return;
|
|
237
237
|
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
|
|
238
238
|
const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
|
|
239
|
-
|
|
240
|
-
console.log(
|
|
239
|
+
const chars = (xs) => (xs.length ? median(xs).toLocaleString('en-US') : '—');
|
|
240
|
+
console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended; the prompt it was handed is in characters'));
|
|
241
|
+
console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}${'prompt'.padStart(9)}`));
|
|
241
242
|
const byRole = new Map();
|
|
242
243
|
for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
|
|
243
244
|
for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
|
|
@@ -251,7 +252,8 @@ function contextReport(records) {
|
|
|
251
252
|
const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
|
|
252
253
|
console.log(
|
|
253
254
|
` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
|
|
254
|
-
`${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}
|
|
255
|
+
`${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}` +
|
|
256
|
+
`${chars(group.map((r) => r.prompt_chars).filter((n) => typeof n === 'number')).padStart(9)}`,
|
|
255
257
|
);
|
|
256
258
|
});
|
|
257
259
|
}
|
package/src/langfuse.mjs
CHANGED
|
@@ -100,31 +100,47 @@ function usageAttributes(tokens, model, priced) {
|
|
|
100
100
|
return out;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* How much of a stage's context a project sends, from its switch in the Langfuse configuration: all of it
|
|
105
|
+
* (`--content`), only what passed between the agents (`--prompts`), or none.
|
|
106
|
+
*
|
|
107
|
+
* @param {{content?: boolean|string}|undefined} setting - The project's entry, if it is on.
|
|
108
|
+
* @returns {'full'|'prompts'|null}
|
|
109
|
+
*/
|
|
110
|
+
export function contextMode(setting) {
|
|
111
|
+
return setting?.content === 'prompts' ? 'prompts' : setting?.content ? 'full' : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A round's shape, as metadata on its root: how many turns and calls it took, and how large its context and prompt were. */
|
|
115
|
+
const SHAPE = ['turns', 'tool_calls', 'context_start', 'context_peak', 'prompt_chars'];
|
|
116
|
+
|
|
103
117
|
/**
|
|
104
118
|
* The OTLP span at the root of a dispatch's trace. Without its context, a run that spent tokens is a
|
|
105
119
|
* generation carrying the run's whole usage and estimated cost, and one that did not is a span. With it,
|
|
106
120
|
* the root is the agent — its prompt in, its report out — and the usage is on the messages beneath it,
|
|
107
|
-
* so no sum counts a token twice.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
121
|
+
* so no sum counts a token twice. With the prompt and report alone, nothing is beneath it, so the root is
|
|
122
|
+
* a generation again, carrying the usage beside them. Its verdict is the score beside it rather than an
|
|
123
|
+
* attribute on it: a run whose verdict is read only later can still be given one, since that is a first
|
|
124
|
+
* score and not a second span.
|
|
110
125
|
*
|
|
111
126
|
* @param {object} record - A snapshot record.
|
|
112
127
|
* @param {string} project - The name the project goes by in Langfuse.
|
|
113
128
|
* @param {{prompt: string, report: string}} [context] - What the stage was given and handed back.
|
|
129
|
+
* @param {{usage?: boolean}} [options] - `usage`: the root carries the usage even with its context.
|
|
114
130
|
* @returns {object|null} Null for a record with no time to place it at.
|
|
115
131
|
*/
|
|
116
|
-
export function spanOf(record, project, context = null) {
|
|
132
|
+
export function spanOf(record, project, context = null, { usage = false } = {}) {
|
|
117
133
|
const start = nanos(record.ts);
|
|
118
134
|
if (!start) return null;
|
|
119
135
|
const end = nanos(record.result_ts) ?? start;
|
|
120
|
-
const tokens = !context && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
|
|
136
|
+
const tokens = (!context || usage) && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
|
|
121
137
|
const attributes = [
|
|
122
138
|
attribute('langfuse.session.id', record.session ?? 'no-session'),
|
|
123
139
|
attribute('langfuse.trace.name', record.role),
|
|
124
140
|
attribute('langfuse.trace.tags', ['nina', project]),
|
|
125
141
|
attribute('langfuse.trace.metadata.project', project),
|
|
126
142
|
attribute('langfuse.trace.metadata.role', record.role),
|
|
127
|
-
attribute('langfuse.observation.type',
|
|
143
|
+
attribute('langfuse.observation.type', tokens ? 'generation' : context ? 'agent' : 'span'),
|
|
128
144
|
attribute('langfuse.observation.metadata.role', record.role),
|
|
129
145
|
];
|
|
130
146
|
if (typeof record.issues === 'number') attributes.push(attribute('langfuse.observation.metadata.issues', record.issues));
|
|
@@ -133,6 +149,7 @@ export function spanOf(record, project, context = null) {
|
|
|
133
149
|
if (record.effort) attributes.push(attribute('langfuse.observation.metadata.effort', record.effort));
|
|
134
150
|
// A round after the first is the same agent resumed after it reported; the trace says which.
|
|
135
151
|
if (typeof record.round === 'number') attributes.push(attribute('langfuse.observation.metadata.round', record.round));
|
|
152
|
+
for (const key of SHAPE) if (typeof record[key] === 'number') attributes.push(attribute(`langfuse.observation.metadata.${key}`, record[key]));
|
|
136
153
|
if (tokens) attributes.push(...usageAttributes(tokens, record.usage_model, record.usage_model));
|
|
137
154
|
if (context) {
|
|
138
155
|
attributes.push(attribute('langfuse.observation.input', context.prompt ?? ''));
|
|
@@ -144,23 +161,26 @@ export function spanOf(record, project, context = null) {
|
|
|
144
161
|
/**
|
|
145
162
|
* Everything a dispatch sends: the root, and — when its context goes too — a generation per message it
|
|
146
163
|
* wrote, with that message's own tokens and cost, and an observation per tool it called, with what went
|
|
147
|
-
* in and what came back.
|
|
148
|
-
*
|
|
164
|
+
* in and what came back. With `prompts`, only what passed between the agents goes: the root, with the
|
|
165
|
+
* prompt the stage was given and the report it handed back, and none of what it read, wrote or called in
|
|
166
|
+
* between. Every text is masked (`redact`), cut to a length, and has the owner's home directory written
|
|
167
|
+
* as `~`.
|
|
149
168
|
*
|
|
150
169
|
* @param {object} record - A snapshot record.
|
|
151
170
|
* @param {string} project - The name the project goes by in Langfuse.
|
|
152
171
|
* @param {object|null} run - From `readRun`, or null to send the record alone.
|
|
153
172
|
* @param {{text?: number, tool?: number}} [limits] - How much of each text goes.
|
|
173
|
+
* @param {{prompts?: boolean}} [options] - `prompts`: the prompt and the report, and nothing beneath them.
|
|
154
174
|
* @returns {object[]} Spans; empty for a record with no time to place it at.
|
|
155
175
|
*/
|
|
156
|
-
export function spansOf(record, project, run, limits = {}) {
|
|
176
|
+
export function spansOf(record, project, run, limits = {}, { prompts = false } = {}) {
|
|
157
177
|
const textChars = limits.text ?? TEXT_CHARS;
|
|
158
178
|
const toolChars = limits.tool ?? TOOL_CHARS;
|
|
159
179
|
const home = homedir();
|
|
160
180
|
const clean = (text, chars) => clip(redact(String(text ?? '')).replaceAll(home, '~'), chars);
|
|
161
|
-
const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null);
|
|
181
|
+
const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null, { usage: prompts });
|
|
162
182
|
if (!root) return [];
|
|
163
|
-
if (!run) return [root];
|
|
183
|
+
if (!run || prompts) return [root];
|
|
164
184
|
const child = (id, name, start, end, attributes) => ({
|
|
165
185
|
traceId: root.traceId,
|
|
166
186
|
spanId: hexId(`${record.dispatch_id}|${id}`, 16),
|
package/src/transcripts.mjs
CHANGED
|
@@ -632,7 +632,7 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
632
632
|
if (
|
|
633
633
|
run.agent_read_bytes === size &&
|
|
634
634
|
!('resumes' in run) &&
|
|
635
|
-
[...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r && 'turns' in r)
|
|
635
|
+
[...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r && 'turns' in r && 'prompt_chars' in r)
|
|
636
636
|
) {
|
|
637
637
|
continue;
|
|
638
638
|
}
|
|
@@ -662,6 +662,8 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
662
662
|
written: new Set(),
|
|
663
663
|
/** The tool calls the round made, by id: over its turns, how many calls each turn sent at once. */
|
|
664
664
|
calls: new Set(),
|
|
665
|
+
/** How long the prompt that opened the round was, in characters; never the prompt. */
|
|
666
|
+
promptChars: null,
|
|
665
667
|
});
|
|
666
668
|
const rounds = [fresh()];
|
|
667
669
|
const roundOf = roundsOf();
|
|
@@ -681,6 +683,14 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
681
683
|
}
|
|
682
684
|
}
|
|
683
685
|
const round = rounds[n - 1];
|
|
686
|
+
if (round.promptChars === null && line.includes('"type":"user"')) {
|
|
687
|
+
try {
|
|
688
|
+
const said = promptOf(JSON.parse(line), n);
|
|
689
|
+
if (said !== null) round.promptChars = said.length;
|
|
690
|
+
} catch {
|
|
691
|
+
// A torn line is not the prompt.
|
|
692
|
+
}
|
|
693
|
+
}
|
|
684
694
|
const hasSkill = line.includes('"Skill"');
|
|
685
695
|
const hasHandback = line.includes('SubagentHandback');
|
|
686
696
|
for (const m of line.matchAll(/"type":"tool_use","id":"([^"]+)"/g)) round.calls.add(m[1]);
|
|
@@ -770,6 +780,7 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
770
780
|
record.tool_calls = round.calls.size;
|
|
771
781
|
record.context_start = shape.start;
|
|
772
782
|
record.context_peak = shape.peak;
|
|
783
|
+
record.prompt_chars = round.promptChars;
|
|
773
784
|
record.usage_model = spent.model;
|
|
774
785
|
record.effort = spent.effort;
|
|
775
786
|
record.files_touched = round.written.size;
|
|
@@ -802,6 +813,21 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
802
813
|
}
|
|
803
814
|
}
|
|
804
815
|
|
|
816
|
+
/**
|
|
817
|
+
* What a round was told, from one transcript row, or null when the row is not it. The dispatch's prompt
|
|
818
|
+
* opens the first round; a later round opens with the message that resumed the run, which Claude Code
|
|
819
|
+
* marks as meta, while a meta row in the first round is a reminder. The snapshot measures it and the
|
|
820
|
+
* Langfuse export sends it, so both read it here.
|
|
821
|
+
*
|
|
822
|
+
* @param {object} row - A parsed transcript row.
|
|
823
|
+
* @param {number} round - The round the row is in, from 1.
|
|
824
|
+
* @returns {string|null}
|
|
825
|
+
*/
|
|
826
|
+
export function promptOf(row, round) {
|
|
827
|
+
const content = row?.message?.content;
|
|
828
|
+
return row?.type === 'user' && (round > 1 || !row.isMeta) && typeof content === 'string' ? content : null;
|
|
829
|
+
}
|
|
830
|
+
|
|
805
831
|
/**
|
|
806
832
|
* How a round spent its context: how many turns it took, and how large the context each turn re-read was
|
|
807
833
|
* when the round began and at its largest. Every turn is billed the whole context again, so a round that
|