@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.
Files changed (130) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/releases/0.34.0/core/GAPS.md +125 -0
  4. package/releases/0.34.0/core/answered.json +29 -0
  5. package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  6. package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  7. package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  8. package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  9. package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  10. package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  11. package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  12. package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  13. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  14. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  15. package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
  16. package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
  17. package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
  18. package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
  19. package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
  20. package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
  22. package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
  23. package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
  24. package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
  27. package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
  29. package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
  30. package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
  31. package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.34.0/core/tree/.claude/router.md +198 -0
  34. package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
  35. package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
  36. package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
  37. package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
  38. package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
  39. package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.34.0/core/vocabulary.json +13 -0
  41. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  51. package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  52. package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  53. package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  55. package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  56. package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  57. package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  58. package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  59. package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
  60. package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
  61. package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  62. package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  63. package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
  64. package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
  65. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  66. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  67. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  68. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  69. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  70. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  71. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  72. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  73. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  74. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  75. package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  76. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  77. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  78. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  79. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  80. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  81. package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  82. package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  83. package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
  84. package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  85. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  86. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  87. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  88. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  89. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  90. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  91. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  92. package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  93. package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  94. package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  95. package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  96. package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
  97. package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  98. package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  99. package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  100. package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  101. package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  102. package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  103. package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  104. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  105. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  106. package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
  107. package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
  108. package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  109. package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  110. package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
  111. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  112. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  113. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  114. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  115. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  116. package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
  117. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  118. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  119. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  120. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  121. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  122. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  123. package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  124. package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  125. package/src/agentrun.mjs +2 -3
  126. package/src/commands/export.mjs +9 -7
  127. package/src/commands/langfuse.mjs +25 -10
  128. package/src/commands/stats.mjs +7 -5
  129. package/src/langfuse.mjs +31 -11
  130. 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 — see `src/langfuse.mjs`.
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 own context)\n');
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
- const content = argv.includes('--content');
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 ? "with each stage's own context" : 'as metadata only'}`);
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.content ? 'with context' : 'metadata only'}, since ${setting.since.slice(0, 10)} — ${said}`);
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
  }
@@ -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, and the context a 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
- 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'));
240
- 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)}`));
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. Its verdict is the score beside it rather than an attribute on it: a
108
- * run whose verdict is read only later can still be given one, since that is a first score and not a
109
- * second span.
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', context ? 'agent' : tokens ? 'generation' : 'span'),
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. Every text is masked (`redact`), cut to a length, and has the owner's home
148
- * directory written as `~`.
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),
@@ -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