pluribus-context 0.3.22 → 0.3.27

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 (95) hide show
  1. package/CHANGELOG.md +33 -3
  2. package/README.md +9 -2
  3. package/docs/community-review-packet.md +10 -2
  4. package/docs/context-budget-receipts.md +152 -0
  5. package/docs/context-input-evidence.md +397 -0
  6. package/docs/context-receipts-for-agent-observability.md +177 -0
  7. package/docs/orchestration-search-receipts.md +102 -0
  8. package/docs/portability-fidelity-report.md +4 -2
  9. package/examples/agent-skills/context-receipts/README.md +22 -0
  10. package/examples/agent-skills/context-receipts/SKILL.md +89 -0
  11. package/examples/context-input-evidence/AGENTS.md +12 -0
  12. package/examples/context-input-evidence/agent-overlay-log.jsonl +4 -0
  13. package/examples/context-input-evidence/agent-overlay-otel-trace.json +548 -0
  14. package/examples/context-input-evidence/agent-overlay-receipt.ndjson +3 -0
  15. package/examples/context-input-evidence/agentgateway-progressive-disclosure-otel-trace.json +393 -0
  16. package/examples/context-input-evidence/agentgateway-progressive-disclosure-receipt.ndjson +4 -0
  17. package/examples/context-input-evidence/brain-remediation-otel-trace.json +645 -0
  18. package/examples/context-input-evidence/brain-remediation-receipt.ndjson +7 -0
  19. package/examples/context-input-evidence/claudekit-mcp-manager-otel-trace.json +417 -0
  20. package/examples/context-input-evidence/claudekit-mcp-manager-receipt.ndjson +5 -0
  21. package/examples/context-input-evidence/cli-progressive-disclosure-otel-trace.json +399 -0
  22. package/examples/context-input-evidence/cli-progressive-disclosure-receipt.ndjson +4 -0
  23. package/examples/context-input-evidence/compaction-otel-trace.json +711 -0
  24. package/examples/context-input-evidence/compaction-receipt.ndjson +6 -0
  25. package/examples/context-input-evidence/context-selection-otel-trace.json +627 -0
  26. package/examples/context-input-evidence/context-selection-receipt.ndjson +7 -0
  27. package/examples/context-input-evidence/convert-agent-overlay-log.mjs +156 -0
  28. package/examples/context-input-evidence/convert-agentgateway-progressive-disclosure-log.mjs +251 -0
  29. package/examples/context-input-evidence/convert-brain-remediation-log.mjs +241 -0
  30. package/examples/context-input-evidence/convert-claudekit-mcp-manager-log.mjs +253 -0
  31. package/examples/context-input-evidence/convert-cli-progressive-disclosure-log.mjs +251 -0
  32. package/examples/context-input-evidence/convert-compaction-log.mjs +224 -0
  33. package/examples/context-input-evidence/convert-context-selection-log.mjs +247 -0
  34. package/examples/context-input-evidence/convert-mcp-tool-search-log.mjs +242 -0
  35. package/examples/context-input-evidence/convert-memory-consolidation-log.mjs +240 -0
  36. package/examples/context-input-evidence/convert-memory-governance-delete-log.mjs +223 -0
  37. package/examples/context-input-evidence/convert-memory-log.mjs +226 -0
  38. package/examples/context-input-evidence/convert-memory-provenance-log.mjs +263 -0
  39. package/examples/context-input-evidence/convert-secret-scanning-log.mjs +233 -0
  40. package/examples/context-input-evidence/convert-session-log.mjs +186 -0
  41. package/examples/context-input-evidence/convert-skill-log.mjs +161 -0
  42. package/examples/context-input-evidence/convert-skill-registry-log.mjs +246 -0
  43. package/examples/context-input-evidence/convert-skill-routing-log.mjs +253 -0
  44. package/examples/context-input-evidence/convert-subagent-context-budget-log.mjs +267 -0
  45. package/examples/context-input-evidence/convert-subagent-delegation-log.mjs +264 -0
  46. package/examples/context-input-evidence/export-otel-trace.mjs +128 -0
  47. package/examples/context-input-evidence/generate-receipt.mjs +188 -0
  48. package/examples/context-input-evidence/mcp-tool-search-otel-trace.json +477 -0
  49. package/examples/context-input-evidence/mcp-tool-search-receipt.ndjson +5 -0
  50. package/examples/context-input-evidence/memory-consolidation-otel-trace.json +492 -0
  51. package/examples/context-input-evidence/memory-consolidation-receipt.ndjson +4 -0
  52. package/examples/context-input-evidence/memory-governance-delete-otel-trace.json +614 -0
  53. package/examples/context-input-evidence/memory-governance-delete-receipt.ndjson +5 -0
  54. package/examples/context-input-evidence/memory-otel-trace.json +645 -0
  55. package/examples/context-input-evidence/memory-provenance-otel-trace.json +711 -0
  56. package/examples/context-input-evidence/memory-provenance-receipt.ndjson +5 -0
  57. package/examples/context-input-evidence/memory-receipt.ndjson +4 -0
  58. package/examples/context-input-evidence/otel-trace.json +1119 -0
  59. package/examples/context-input-evidence/receipt.ndjson +6 -0
  60. package/examples/context-input-evidence/sample-agentgateway-progressive-disclosure-log.jsonl +5 -0
  61. package/examples/context-input-evidence/sample-brain-remediation-log.jsonl +9 -0
  62. package/examples/context-input-evidence/sample-claudekit-mcp-manager-log.jsonl +6 -0
  63. package/examples/context-input-evidence/sample-cli-progressive-disclosure-log.jsonl +5 -0
  64. package/examples/context-input-evidence/sample-compaction-log.jsonl +7 -0
  65. package/examples/context-input-evidence/sample-context-selection-log.jsonl +7 -0
  66. package/examples/context-input-evidence/sample-mcp-tool-search-log.jsonl +6 -0
  67. package/examples/context-input-evidence/sample-memory-consolidation-log.jsonl +5 -0
  68. package/examples/context-input-evidence/sample-memory-governance-delete-log.jsonl +6 -0
  69. package/examples/context-input-evidence/sample-memory-provenance-log.jsonl +6 -0
  70. package/examples/context-input-evidence/sample-memory-retrieval-log.jsonl +6 -0
  71. package/examples/context-input-evidence/sample-secret-scanning-log.jsonl +7 -0
  72. package/examples/context-input-evidence/sample-session-log.jsonl +6 -0
  73. package/examples/context-input-evidence/sample-skill-registry-log.jsonl +5 -0
  74. package/examples/context-input-evidence/sample-skill-routing-log.jsonl +7 -0
  75. package/examples/context-input-evidence/sample-subagent-context-budget-log.jsonl +6 -0
  76. package/examples/context-input-evidence/sample-subagent-delegation-log.jsonl +5 -0
  77. package/examples/context-input-evidence/secret-scanning-otel-trace.json +794 -0
  78. package/examples/context-input-evidence/secret-scanning-receipt.ndjson +6 -0
  79. package/examples/context-input-evidence/session-otel-trace.json +411 -0
  80. package/examples/context-input-evidence/session-receipt.ndjson +2 -0
  81. package/examples/context-input-evidence/skill-invocation-log.jsonl +4 -0
  82. package/examples/context-input-evidence/skill-otel-trace.json +548 -0
  83. package/examples/context-input-evidence/skill-receipt.ndjson +3 -0
  84. package/examples/context-input-evidence/skill-registry-otel-trace.json +471 -0
  85. package/examples/context-input-evidence/skill-registry-receipt.ndjson +5 -0
  86. package/examples/context-input-evidence/skill-routing-otel-trace.json +567 -0
  87. package/examples/context-input-evidence/skill-routing-receipt.ndjson +6 -0
  88. package/examples/context-input-evidence/subagent-context-budget-otel-trace.json +507 -0
  89. package/examples/context-input-evidence/subagent-context-budget-receipt.ndjson +5 -0
  90. package/examples/context-input-evidence/subagent-delegation-otel-trace.json +388 -0
  91. package/examples/context-input-evidence/subagent-delegation-receipt.ndjson +4 -0
  92. package/package.json +6 -2
  93. package/schemas/audit-result.schema.json +409 -71
  94. package/src/commands/audit.js +64 -3
  95. package/src/utils/version.js +1 -1
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const inputPath = process.argv[2] ? resolve(process.argv[2]) : join(here, 'sample-subagent-context-budget-log.jsonl');
9
+ const receiptPath = process.argv[3] ? resolve(process.argv[3]) : join(here, 'subagent-context-budget-receipt.ndjson');
10
+ const tracePath = process.argv[4] ? resolve(process.argv[4]) : join(here, 'subagent-context-budget-otel-trace.json');
11
+
12
+ function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value ?? '').digest('hex')}`;
14
+ }
15
+
16
+ function hashRef(value) {
17
+ return sha256(value ?? '').slice(0, 19);
18
+ }
19
+
20
+ function readJsonl(path) {
21
+ return readFileSync(path, 'utf8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter(Boolean)
25
+ .map((line, index) => {
26
+ try {
27
+ return JSON.parse(line);
28
+ } catch (error) {
29
+ throw new Error(`Invalid JSONL at ${path}:${index + 1}: ${error.message}`);
30
+ }
31
+ });
32
+ }
33
+
34
+ function unixNano(isoTimestamp) {
35
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
36
+ }
37
+
38
+ function otelValue(value) {
39
+ if (typeof value === 'boolean') return { boolValue: value };
40
+ if (typeof value === 'number' && Number.isInteger(value)) return { intValue: String(value) };
41
+ if (typeof value === 'number') return { doubleValue: value };
42
+ if (value == null) return { stringValue: '' };
43
+ return { stringValue: String(value) };
44
+ }
45
+
46
+ function attributesToOtel(attributes) {
47
+ return Object.entries(attributes).map(([key, value]) => ({ key, value: otelValue(value) }));
48
+ }
49
+
50
+ function tokenBucket(value) {
51
+ if (value < 1_000) return 'under_1k';
52
+ if (value < 10_000) return 'under_10k';
53
+ if (value < 50_000) return 'under_50k';
54
+ if (value < 100_000) return 'under_100k';
55
+ return 'over_100k';
56
+ }
57
+
58
+ function countBucket(value) {
59
+ if (value === 0) return 'zero';
60
+ if (value <= 5) return 'under_5';
61
+ if (value <= 25) return 'under_25';
62
+ if (value <= 100) return 'under_100';
63
+ if (value <= 500) return 'under_500';
64
+ return 'over_500';
65
+ }
66
+
67
+ function ratioBucket(numerator, denominator) {
68
+ const ratio = denominator > 0 ? numerator / denominator : 0;
69
+ if (ratio < 0.25) return 'under_25_percent';
70
+ if (ratio < 0.5) return 'under_50_percent';
71
+ if (ratio < 0.75) return 'under_75_percent';
72
+ if (ratio < 0.9) return 'under_90_percent';
73
+ return 'over_90_percent';
74
+ }
75
+
76
+ const records = readJsonl(inputPath);
77
+ const session = records.find((record) => record.type === 'session.start');
78
+ const budget = records.find((record) => record.type === 'subagent.boot.context_budget.evaluated');
79
+ const loaded = records.filter((record) => record.type === 'subagent.context_component.loaded');
80
+ const suppressed = records.filter((record) => record.type === 'subagent.context_component.suppressed');
81
+ const completed = records.find((record) => record.type === 'subagent.boot.completed');
82
+
83
+ if (!session || !budget || loaded.length === 0 || !completed) {
84
+ throw new Error(`Expected session.start, subagent.boot.context_budget.evaluated, loaded components, and subagent.boot.completed records in ${inputPath}`);
85
+ }
86
+
87
+ const selectedComponentCount = loaded.reduce((total, record) => total + record.selected_count, 0);
88
+ const suppressedComponentCount = [...loaded, ...suppressed].reduce((total, record) => total + record.suppressed_count, 0);
89
+ const loadedTokenCount = loaded.reduce((total, record) => total + record.token_count, 0);
90
+
91
+ const traceSeed = `${session.session_id}:${session.conversation_id}:subagent-context-budget`;
92
+ const traceId = sha256(traceSeed).replace('sha256:', '').slice(0, 32);
93
+ const spanId = sha256(`${traceSeed}:span`).replace('sha256:', '').slice(0, 16);
94
+
95
+ const events = [
96
+ {
97
+ trace_id: traceId,
98
+ span_id: spanId,
99
+ name: 'subagent.boot.context_budget.evaluated',
100
+ time: budget.time,
101
+ attributes: {
102
+ 'session.id': session.session_id,
103
+ 'gen_ai.conversation.id': session.conversation_id,
104
+ 'agent.name': session.agent,
105
+ 'subagent.id_hash': hashRef(budget.subagent_id),
106
+ 'subagent.role_hash': hashRef(budget.subagent_role),
107
+ 'subagent.tools.policy': budget.tools_policy,
108
+ 'subagent.context.parent_token_bucket': tokenBucket(budget.parent_context_token_count),
109
+ 'subagent.context.startup_token_bucket': tokenBucket(budget.subagent_startup_context_token_count),
110
+ 'subagent.context.window_token_bucket': tokenBucket(budget.total_context_window_tokens),
111
+ 'subagent.context.startup_ratio_bucket': ratioBucket(budget.subagent_startup_context_token_count, budget.total_context_window_tokens),
112
+ 'subagent.mcp.server_count_bucket': countBucket(budget.mcp_server_count),
113
+ 'subagent.mcp.tool_schema_count_bucket': countBucket(budget.mcp_tool_schema_count),
114
+ 'subagent.skill.listing_count_bucket': countBucket(budget.skill_listing_count),
115
+ 'subagent.rule.count_bucket': countBucket(budget.project_rule_count),
116
+ 'subagent.memory.index_count_bucket': countBucket(budget.memory_index_count),
117
+ 'privacy.raw_skill_listing_recorded': false,
118
+ 'privacy.raw_mcp_schemas_recorded': false,
119
+ 'privacy.raw_project_rules_recorded': false,
120
+ 'privacy.raw_memory_index_recorded': false
121
+ }
122
+ },
123
+ ...loaded.map((record) => ({
124
+ trace_id: traceId,
125
+ span_id: spanId,
126
+ name: 'subagent.context_component.loaded',
127
+ time: record.time,
128
+ attributes: {
129
+ 'session.id': session.session_id,
130
+ 'gen_ai.conversation.id': session.conversation_id,
131
+ 'subagent.id_hash': hashRef(budget.subagent_id),
132
+ 'subagent.context.component': record.component,
133
+ 'subagent.context.component_reason_hash': hashRef(record.reason),
134
+ 'subagent.context.candidate_count_bucket': countBucket(record.candidate_count),
135
+ 'subagent.context.selected_count_bucket': countBucket(record.selected_count),
136
+ 'subagent.context.suppressed_count_bucket': countBucket(record.suppressed_count),
137
+ 'subagent.context.token_bucket': tokenBucket(record.token_count),
138
+ 'subagent.context.component_sample_hash': sha256(record.raw_component_sample),
139
+ 'privacy.raw_component_recorded': false
140
+ }
141
+ })),
142
+ ...suppressed.map((record) => ({
143
+ trace_id: traceId,
144
+ span_id: spanId,
145
+ name: 'subagent.context_component.suppressed',
146
+ time: record.time,
147
+ attributes: {
148
+ 'session.id': session.session_id,
149
+ 'gen_ai.conversation.id': session.conversation_id,
150
+ 'subagent.id_hash': hashRef(budget.subagent_id),
151
+ 'subagent.context.component': record.component,
152
+ 'subagent.context.component_reason_hash': hashRef(record.reason),
153
+ 'subagent.context.candidate_count_bucket': countBucket(record.candidate_count),
154
+ 'subagent.context.selected_count_bucket': countBucket(record.selected_count),
155
+ 'subagent.context.suppressed_count_bucket': countBucket(record.suppressed_count),
156
+ 'subagent.context.token_bucket': tokenBucket(record.token_count),
157
+ 'subagent.context.component_sample_hash': sha256(record.raw_component_sample),
158
+ 'privacy.raw_component_recorded': false
159
+ }
160
+ })),
161
+ {
162
+ trace_id: traceId,
163
+ span_id: spanId,
164
+ name: 'subagent.boot.completed',
165
+ time: completed.time,
166
+ attributes: {
167
+ 'session.id': session.session_id,
168
+ 'gen_ai.conversation.id': session.conversation_id,
169
+ 'subagent.id_hash': hashRef(budget.subagent_id),
170
+ 'subagent.boot.status': completed.status,
171
+ 'subagent.context.startup_token_bucket': tokenBucket(completed.startup_context_token_count),
172
+ 'subagent.context.remaining_token_bucket': tokenBucket(completed.remaining_context_token_count),
173
+ 'subagent.context.selected_component_count_bucket': countBucket(selectedComponentCount),
174
+ 'subagent.context.suppressed_component_count_bucket': countBucket(suppressedComponentCount),
175
+ 'subagent.context.loaded_token_bucket': tokenBucket(loadedTokenCount),
176
+ 'subagent.boot.first_task_hash': sha256(completed.first_task),
177
+ 'subagent.boot.mitigation_hash': sha256(completed.mitigation),
178
+ 'subagent.boot.audit_gap': completed.audit_gap
179
+ }
180
+ }
181
+ ];
182
+
183
+ writeFileSync(receiptPath, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`);
184
+
185
+ const trace = {
186
+ resourceSpans: [
187
+ {
188
+ resource: {
189
+ attributes: attributesToOtel({
190
+ 'service.name': 'pluribus-subagent-context-budget-receipt-demo',
191
+ 'service.version': '0.0.0-fixture',
192
+ 'deployment.environment.name': 'local-fixture'
193
+ })
194
+ },
195
+ scopeSpans: [
196
+ {
197
+ scope: {
198
+ name: 'pluribus.context_input_evidence.subagent_context_budget_demo',
199
+ version: '0.0.0-fixture'
200
+ },
201
+ spans: [
202
+ {
203
+ traceId,
204
+ spanId,
205
+ parentSpanId: '',
206
+ name: 'agent.subagent.boot.context_budget',
207
+ kind: 1,
208
+ startTimeUnixNano: unixNano(budget.time),
209
+ endTimeUnixNano: unixNano(completed.time),
210
+ attributes: attributesToOtel({
211
+ 'session.id': session.session_id,
212
+ 'gen_ai.conversation.id': session.conversation_id,
213
+ 'agent.name': session.agent,
214
+ 'workspace.name': session.workspace,
215
+ 'gen_ai.request.model': session.model,
216
+ 'subagent.tools.policy': budget.tools_policy,
217
+ 'subagent.context.receipt.scope': 'startup_budget_before_first_task'
218
+ }),
219
+ events: events.map((event) => ({
220
+ name: event.name,
221
+ timeUnixNano: unixNano(event.time),
222
+ attributes: attributesToOtel(event.attributes)
223
+ }))
224
+ }
225
+ ]
226
+ }
227
+ ]
228
+ }
229
+ ]
230
+ };
231
+
232
+ writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`);
233
+
234
+ const forbiddenRawStrings = [
235
+ 'Acme-Co',
236
+ 'sk_live_private_fixture',
237
+ 'sk_live_gateway_fixture',
238
+ 'finance@acme.example',
239
+ 'private-enterprise-mcp',
240
+ 'support.ticket.search',
241
+ 'TICKET-private-001',
242
+ 'Stripe prod incident',
243
+ '/private/work/acme',
244
+ 'PAY-1234',
245
+ 'private support ops'
246
+ ];
247
+ const exportedText = `${events.map((event) => JSON.stringify(event)).join('\n')}\n${JSON.stringify(trace)}`;
248
+ const rawTextCopiedToReceipt = forbiddenRawStrings.some((value) => exportedText.includes(value));
249
+
250
+ const summary = {
251
+ schema: 'pluribus.subagentContextBudgetReceipt.demo.v0',
252
+ eventCount: events.length,
253
+ loadedComponentEvents: loaded.length,
254
+ suppressedComponentEvents: suppressed.length,
255
+ startupContextRatioBucket: events[0].attributes['subagent.context.startup_ratio_bucket'],
256
+ mcpToolSchemaCountBucket: events[0].attributes['subagent.mcp.tool_schema_count_bucket'],
257
+ skillListingCountBucket: events[0].attributes['subagent.skill.listing_count_bucket'],
258
+ selectedComponentCountBucket: events.at(-1).attributes['subagent.context.selected_component_count_bucket'],
259
+ suppressedComponentCountBucket: events.at(-1).attributes['subagent.context.suppressed_component_count_bucket'],
260
+ includesFirstTaskHash: Boolean(events.at(-1).attributes['subagent.boot.first_task_hash']),
261
+ rawTextCopiedToReceipt,
262
+ receiptPath: 'examples/context-input-evidence/subagent-context-budget-receipt.ndjson',
263
+ tracePath: 'examples/context-input-evidence/subagent-context-budget-otel-trace.json',
264
+ lesson: 'Subagent context-budget receipts should prove which startup components were eagerly loaded, selected, or suppressed before the first task, without exporting raw tool schemas, skill listings, memory, rules, prompts, paths, or secrets.'
265
+ };
266
+
267
+ console.log(JSON.stringify(summary, null, 2));
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const inputPath = process.argv[2] ? resolve(process.argv[2]) : join(here, 'sample-subagent-delegation-log.jsonl');
9
+ const receiptPath = process.argv[3] ? resolve(process.argv[3]) : join(here, 'subagent-delegation-receipt.ndjson');
10
+ const tracePath = process.argv[4] ? resolve(process.argv[4]) : join(here, 'subagent-delegation-otel-trace.json');
11
+
12
+ function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value ?? '').digest('hex')}`;
14
+ }
15
+
16
+ function hashRef(value) {
17
+ return sha256(value ?? '').slice(0, 19);
18
+ }
19
+
20
+ function readJsonl(path) {
21
+ return readFileSync(path, 'utf8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter(Boolean)
25
+ .map((line, index) => {
26
+ try {
27
+ return JSON.parse(line);
28
+ } catch (error) {
29
+ throw new Error(`Invalid JSONL at ${path}:${index + 1}: ${error.message}`);
30
+ }
31
+ });
32
+ }
33
+
34
+ function unixNano(isoTimestamp) {
35
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
36
+ }
37
+
38
+ function otelValue(value) {
39
+ if (typeof value === 'boolean') return { boolValue: value };
40
+ if (typeof value === 'number' && Number.isInteger(value)) return { intValue: String(value) };
41
+ if (typeof value === 'number') return { doubleValue: value };
42
+ if (value == null) return { stringValue: '' };
43
+ return { stringValue: String(value) };
44
+ }
45
+
46
+ function attributesToOtel(attributes) {
47
+ return Object.entries(attributes).map(([key, value]) => ({ key, value: otelValue(value) }));
48
+ }
49
+
50
+ function tokenBucket(value) {
51
+ if (value < 1_000) return 'under_1k';
52
+ if (value < 10_000) return 'under_10k';
53
+ if (value < 50_000) return 'under_50k';
54
+ return 'over_50k';
55
+ }
56
+
57
+ function lineBucket(value) {
58
+ if (value < 50) return 'under_50';
59
+ if (value < 250) return 'under_250';
60
+ if (value < 1_000) return 'under_1k';
61
+ return 'over_1k';
62
+ }
63
+
64
+ function byteBucket(value) {
65
+ if (value < 10_000) return 'under_10k';
66
+ if (value < 100_000) return 'under_100k';
67
+ if (value < 1_000_000) return 'under_1m';
68
+ return 'over_1m';
69
+ }
70
+
71
+ const records = readJsonl(inputPath);
72
+ const session = records.find((record) => record.type === 'session.start');
73
+ const delegation = records.find((record) => record.type === 'subagent.delegation.requested');
74
+ const capture = records.find((record) => record.type === 'subagent.tool_output.captured');
75
+ const summary = records.find((record) => record.type === 'subagent.summary.returned');
76
+ const budget = records.find((record) => record.type === 'parent.context_budget.evaluated');
77
+
78
+ if (!session || !delegation || !capture || !summary || !budget) {
79
+ throw new Error(`Expected session.start, subagent.delegation.requested, subagent.tool_output.captured, subagent.summary.returned, and parent.context_budget.evaluated records in ${inputPath}`);
80
+ }
81
+
82
+ const traceSeed = `${session.session_id}:${session.conversation_id}:subagent-delegation`;
83
+ const traceId = sha256(traceSeed).replace('sha256:', '').slice(0, 32);
84
+ const spanId = sha256(`${traceSeed}:span`).replace('sha256:', '').slice(0, 16);
85
+ const childSpanId = sha256(`${traceSeed}:child`).replace('sha256:', '').slice(0, 16);
86
+
87
+ const events = [
88
+ {
89
+ trace_id: traceId,
90
+ span_id: spanId,
91
+ name: 'subagent.delegation.requested',
92
+ time: delegation.time,
93
+ attributes: {
94
+ 'session.id': session.session_id,
95
+ 'gen_ai.conversation.id': session.conversation_id,
96
+ 'agent.name': session.agent,
97
+ 'subagent.delegation.operation_hash': hashRef(delegation.operation),
98
+ 'subagent.delegation.reason_hash': sha256(delegation.delegation_reason),
99
+ 'subagent.delegation.estimated_output_line_bucket': lineBucket(delegation.estimated_output_lines),
100
+ 'subagent.delegation.parent_context_before_bucket': tokenBucket(delegation.parent_context_before_tokens),
101
+ 'subagent.delegation.threshold_policy': 'delegate_outputs_over_50_lines',
102
+ 'privacy.raw_parent_prompt_recorded': false,
103
+ 'privacy.raw_operation_output_recorded': false
104
+ }
105
+ },
106
+ {
107
+ trace_id: traceId,
108
+ span_id: childSpanId,
109
+ name: 'subagent.tool_output.captured',
110
+ time: capture.time,
111
+ attributes: {
112
+ 'session.id': session.session_id,
113
+ 'gen_ai.conversation.id': session.conversation_id,
114
+ 'subagent.role': capture.subagent_role,
115
+ 'subagent.operation_hash': hashRef(capture.operation),
116
+ 'subagent.tool.exit_code': capture.exit_code,
117
+ 'subagent.tool.output_line_bucket': lineBucket(capture.output_lines),
118
+ 'subagent.tool.output_byte_bucket': byteBucket(capture.output_bytes),
119
+ 'subagent.tool.output_hash': sha256(capture.raw_output_sample),
120
+ 'subagent.tool.path_hashes': capture.raw_paths.map((path) => hashRef(path)).join(','),
121
+ 'subagent.tool.output_entered_parent_context': false,
122
+ 'privacy.raw_tool_output_recorded': false,
123
+ 'privacy.raw_paths_recorded': false
124
+ }
125
+ },
126
+ {
127
+ trace_id: traceId,
128
+ span_id: spanId,
129
+ name: 'subagent.summary.returned',
130
+ time: summary.time,
131
+ attributes: {
132
+ 'session.id': session.session_id,
133
+ 'gen_ai.conversation.id': session.conversation_id,
134
+ 'subagent.summary.hash': sha256(summary.summary_hash_input),
135
+ 'subagent.summary.char_bucket': byteBucket(summary.summary_chars),
136
+ 'subagent.summary.bullet_count': summary.summary_bullets,
137
+ 'subagent.summary.parent_received_raw_output': summary.parent_received_raw_output,
138
+ 'subagent.summary.parent_received_raw_paths': summary.parent_received_raw_paths,
139
+ 'subagent.summary.boundary': 'bounded_summary_only',
140
+ 'privacy.raw_summary_recorded': false,
141
+ 'privacy.raw_child_output_recorded': false
142
+ }
143
+ },
144
+ {
145
+ trace_id: traceId,
146
+ span_id: spanId,
147
+ name: 'parent.context_budget.evaluated',
148
+ time: budget.time,
149
+ attributes: {
150
+ 'session.id': session.session_id,
151
+ 'gen_ai.conversation.id': session.conversation_id,
152
+ 'parent.context.after_bucket': tokenBucket(budget.parent_context_after_tokens),
153
+ 'parent.context.added_token_bucket': tokenBucket(budget.parent_added_tokens),
154
+ 'subagent.child_output_token_bucket': tokenBucket(budget.child_output_tokens),
155
+ 'subagent.context_savings.proven': budget.child_output_tokens > budget.parent_added_tokens,
156
+ 'subagent.delegation.audit_gap': 'receipt_proves_parent_boundary_not_summary_correctness',
157
+ 'privacy.raw_parent_note_recorded': false
158
+ }
159
+ }
160
+ ].sort((left, right) => Date.parse(left.time) - Date.parse(right.time));
161
+
162
+ writeFileSync(receiptPath, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`);
163
+
164
+ const trace = {
165
+ resourceSpans: [
166
+ {
167
+ resource: {
168
+ attributes: attributesToOtel({
169
+ 'service.name': 'pluribus-subagent-delegation-receipt-demo',
170
+ 'service.version': '0.0.0-fixture',
171
+ 'deployment.environment.name': 'local-fixture'
172
+ })
173
+ },
174
+ scopeSpans: [
175
+ {
176
+ scope: {
177
+ name: 'pluribus.context_input_evidence.subagent_delegation_demo',
178
+ version: '0.0.0-fixture'
179
+ },
180
+ spans: [
181
+ {
182
+ traceId,
183
+ spanId,
184
+ parentSpanId: '',
185
+ name: 'agent.session.parent_with_delegation',
186
+ kind: 1,
187
+ startTimeUnixNano: unixNano(delegation.time),
188
+ endTimeUnixNano: unixNano(budget.time),
189
+ attributes: attributesToOtel({
190
+ 'session.id': session.session_id,
191
+ 'gen_ai.conversation.id': session.conversation_id,
192
+ 'agent.name': session.agent,
193
+ 'workspace.name': session.workspace,
194
+ 'gen_ai.request.model': session.model,
195
+ 'subagent.delegation.policy': 'large_tool_output_to_isolated_subagent'
196
+ }),
197
+ events: events
198
+ .filter((event) => event.span_id === spanId)
199
+ .map((event) => ({
200
+ name: event.name,
201
+ timeUnixNano: unixNano(event.time),
202
+ attributes: attributesToOtel(event.attributes)
203
+ }))
204
+ },
205
+ {
206
+ traceId,
207
+ spanId: childSpanId,
208
+ parentSpanId: spanId,
209
+ name: 'agent.subagent.validation_runner',
210
+ kind: 1,
211
+ startTimeUnixNano: unixNano(capture.time),
212
+ endTimeUnixNano: unixNano(summary.time),
213
+ attributes: attributesToOtel({
214
+ 'session.id': session.session_id,
215
+ 'gen_ai.conversation.id': session.conversation_id,
216
+ 'subagent.role': capture.subagent_role,
217
+ 'subagent.output_boundary': 'raw_output_isolated_summary_returned'
218
+ }),
219
+ events: events
220
+ .filter((event) => event.span_id === childSpanId)
221
+ .map((event) => ({
222
+ name: event.name,
223
+ timeUnixNano: unixNano(event.time),
224
+ attributes: attributesToOtel(event.attributes)
225
+ }))
226
+ }
227
+ ]
228
+ }
229
+ ]
230
+ }
231
+ ]
232
+ };
233
+
234
+ writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`);
235
+
236
+ const forbiddenRawStrings = [
237
+ 'Acme-Co',
238
+ 'private-checkout-api',
239
+ 'AZ-PRIVATE-7781',
240
+ 'jane@acme.example',
241
+ '/work/acme/private-checkout-api',
242
+ 'AcmeSecretPaymentTokenFixture',
243
+ 'Internal Azure trace',
244
+ 'private Module Federation route'
245
+ ];
246
+ const exportedText = `${events.map((event) => JSON.stringify(event)).join('\n')}\n${JSON.stringify(trace)}`;
247
+ const rawTextCopiedToReceipt = forbiddenRawStrings.some((value) => exportedText.includes(value));
248
+
249
+ const summaryOut = {
250
+ schema: 'pluribus.subagentDelegationReceipt.demo.v0',
251
+ eventCount: events.length,
252
+ parentEvents: events.filter((event) => event.span_id === spanId).length,
253
+ childEvents: events.filter((event) => event.span_id === childSpanId).length,
254
+ childOutputTokenBucket: budget.child_output_tokens >= 10000 ? tokenBucket(budget.child_output_tokens) : tokenBucket(budget.child_output_tokens),
255
+ parentAddedTokenBucket: tokenBucket(budget.parent_added_tokens),
256
+ provesRawOutputStayedOutOfParent: capture.output_lines > 50 && summary.parent_received_raw_output === false,
257
+ includesAuditGap: events.at(-1).attributes['subagent.delegation.audit_gap'],
258
+ rawTextCopiedToReceipt,
259
+ receiptPath: 'examples/context-input-evidence/subagent-delegation-receipt.ndjson',
260
+ tracePath: 'examples/context-input-evidence/subagent-delegation-otel-trace.json',
261
+ lesson: 'Delegating bulky commands to subagents still needs a receipt: prove raw child tool output stayed isolated and only a bounded summary crossed back to the parent.'
262
+ };
263
+
264
+ console.log(JSON.stringify(summaryOut, null, 2));
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const receiptPath = join(here, 'receipt.ndjson');
8
+ const outputPath = join(here, 'otel-trace.json');
9
+
10
+ function unixNano(isoTimestamp) {
11
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
12
+ }
13
+
14
+ function otelValue(value) {
15
+ if (typeof value === 'boolean') {
16
+ return { boolValue: value };
17
+ }
18
+
19
+ if (typeof value === 'number' && Number.isInteger(value)) {
20
+ return { intValue: String(value) };
21
+ }
22
+
23
+ if (typeof value === 'number') {
24
+ return { doubleValue: value };
25
+ }
26
+
27
+ if (typeof value === 'string') {
28
+ if (value === 'true' || value === 'false') {
29
+ return { boolValue: value === 'true' };
30
+ }
31
+
32
+ if (/^-?\d+$/.test(value)) {
33
+ return { intValue: value };
34
+ }
35
+
36
+ return { stringValue: value };
37
+ }
38
+
39
+ if (value == null) {
40
+ return { stringValue: '' };
41
+ }
42
+
43
+ return { stringValue: JSON.stringify(value) };
44
+ }
45
+
46
+ function attributesToOtel(attributes) {
47
+ return Object.entries(attributes).map(([key, value]) => ({
48
+ key,
49
+ value: otelValue(value)
50
+ }));
51
+ }
52
+
53
+ const events = readFileSync(receiptPath, 'utf8')
54
+ .trim()
55
+ .split('\n')
56
+ .filter(Boolean)
57
+ .map((line) => JSON.parse(line));
58
+
59
+ if (events.length === 0) {
60
+ throw new Error(`No context.input.loaded events found in ${receiptPath}`);
61
+ }
62
+
63
+ const sessionId = events[0].attributes['session.id'];
64
+ const eventTimes = events.map((event) => Date.parse(event.time));
65
+ const startTimeMs = Math.min(...eventTimes);
66
+ const endTimeMs = Math.max(...eventTimes) + 1;
67
+
68
+ const otlpTrace = {
69
+ resourceSpans: [
70
+ {
71
+ resource: {
72
+ attributes: attributesToOtel({
73
+ 'service.name': 'pluribus-context-input-evidence-demo',
74
+ 'service.version': '0.0.0-fixture',
75
+ 'deployment.environment.name': 'local-fixture'
76
+ })
77
+ },
78
+ scopeSpans: [
79
+ {
80
+ scope: {
81
+ name: 'pluribus.context_input_evidence.demo',
82
+ version: '0.0.0-fixture'
83
+ },
84
+ spans: [
85
+ {
86
+ traceId: '11111111111111111111111111111111',
87
+ spanId: '2222222222222222',
88
+ parentSpanId: '',
89
+ name: 'agent.session',
90
+ kind: 1,
91
+ startTimeUnixNano: `${BigInt(startTimeMs) * 1_000_000n}`,
92
+ endTimeUnixNano: `${BigInt(endTimeMs) * 1_000_000n}`,
93
+ attributes: attributesToOtel({
94
+ 'session.id': sessionId,
95
+ 'gen_ai.conversation.id': sessionId,
96
+ 'gen_ai.agent.name': 'context-input-evidence-demo',
97
+ 'gen_ai.operation.name': 'agent_session'
98
+ }),
99
+ events: events.map((event) => ({
100
+ name: event.name,
101
+ timeUnixNano: unixNano(event.time),
102
+ attributes: attributesToOtel(event.attributes)
103
+ }))
104
+ }
105
+ ]
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ };
111
+
112
+ writeFileSync(outputPath, `${JSON.stringify(otlpTrace, null, 2)}\n`);
113
+
114
+ const loadedEvents = otlpTrace.resourceSpans[0].scopeSpans[0].spans[0].events;
115
+ const suppressionPolicies = new Set(events.map((event) => event.attributes['context.input.duplicate.suppression_policy']));
116
+ const fullRenderStatuses = new Set(events.map((event) => event.attributes['context.input.delivered.full_render.status']));
117
+
118
+ console.log(JSON.stringify({
119
+ schema: 'pluribus.contextInputEvidence.otelTraceFixture.v0',
120
+ sourceReceipt: 'examples/context-input-evidence/receipt.ndjson',
121
+ otelTracePath: 'examples/context-input-evidence/otel-trace.json',
122
+ sessionId,
123
+ spanName: 'agent.session',
124
+ eventCount: loadedEvents.length,
125
+ suppressionPolicyCount: suppressionPolicies.size,
126
+ fullRenderStatusCount: fullRenderStatuses.size,
127
+ lesson: 'Context input evidence fits naturally as SpanEvents on a session/agent span, preserving privacy-first hashes and categorical receipt fields without logging raw prompt text.'
128
+ }, null, 2));