@xingjisoft/neutrino 0.6.2 → 0.7.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 (166) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +19 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/diagnostics.d.ts +91 -0
  43. package/dist/llm/diagnostics.js +242 -0
  44. package/dist/llm/plan.d.ts +148 -0
  45. package/dist/llm/retry.d.ts +80 -0
  46. package/dist/llm/risk.d.ts +21 -0
  47. package/dist/llm/risk.js +7 -0
  48. package/dist/llm/runtime.d.ts +189 -0
  49. package/dist/llm/runtime.js +77 -4
  50. package/dist/llm/search.d.ts +2 -0
  51. package/dist/llm/searchNetwork.d.ts +8 -0
  52. package/dist/llm/subagent.d.ts +112 -0
  53. package/dist/llm/subagent.js +4 -2
  54. package/dist/llm/tools.d.ts +20 -0
  55. package/dist/llm/tools.js +4 -1
  56. package/dist/llm/workspace.d.ts +6 -0
  57. package/dist/llm/workspace.js +271 -0
  58. package/dist/markdown.d.ts +58 -0
  59. package/dist/migration.d.ts +8 -0
  60. package/dist/persistence.d.ts +14 -0
  61. package/dist/plugins/catalog.d.ts +20 -0
  62. package/dist/plugins/catalog.js +45 -0
  63. package/dist/plugins/cli.d.ts +1 -0
  64. package/dist/plugins/cli.js +106 -0
  65. package/dist/plugins/host.d.ts +121 -0
  66. package/dist/plugins/host.js +450 -0
  67. package/dist/plugins/manager.d.ts +24 -0
  68. package/dist/plugins/manager.js +288 -0
  69. package/dist/plugins/package.d.ts +5 -0
  70. package/dist/plugins/package.js +53 -0
  71. package/dist/plugins/runner.d.ts +1 -0
  72. package/dist/plugins/runner.js +227 -0
  73. package/dist/plugins/session.d.ts +12 -0
  74. package/dist/plugins/session.js +97 -0
  75. package/dist/plugins/terminal.d.ts +22 -0
  76. package/dist/plugins/terminal.js +33 -0
  77. package/dist/plugins/types.d.ts +178 -0
  78. package/dist/plugins/types.js +6 -0
  79. package/dist/plugins/validation.d.ts +14 -0
  80. package/dist/plugins/validation.js +174 -0
  81. package/dist/plugins/web-sdk.d.ts +58 -0
  82. package/dist/plugins/web-sdk.js +1 -0
  83. package/dist/pro/agents.d.ts +34 -0
  84. package/dist/pro/config.d.ts +75 -0
  85. package/dist/pro/config.js +3 -2
  86. package/dist/pro/preflight.d.ts +21 -0
  87. package/dist/sanitize.d.ts +34 -0
  88. package/dist/screens/agentConversation.d.ts +28 -0
  89. package/dist/screens/agentHistory.d.ts +25 -0
  90. package/dist/screens/agentSelect.d.ts +16 -0
  91. package/dist/screens/chat.d.ts +292 -0
  92. package/dist/screens/chat.js +139 -21
  93. package/dist/screens/history.d.ts +23 -0
  94. package/dist/screens/modelSelect.d.ts +29 -0
  95. package/dist/screens/plugins.d.ts +29 -0
  96. package/dist/screens/plugins.js +196 -0
  97. package/dist/screens/settings.d.ts +42 -0
  98. package/dist/screens/start.d.ts +43 -0
  99. package/dist/screens/start.js +25 -17
  100. package/dist/screens/updatePrompt.d.ts +41 -0
  101. package/dist/selection.d.ts +75 -0
  102. package/dist/state.d.ts +26 -0
  103. package/dist/terminal/buffer.d.ts +47 -0
  104. package/dist/terminal/input.d.ts +70 -0
  105. package/dist/terminal/renderer.d.ts +43 -0
  106. package/dist/terminal/restore.d.ts +41 -0
  107. package/dist/terminal/screen.d.ts +57 -0
  108. package/dist/tray.d.ts +14 -0
  109. package/dist/ui.d.ts +123 -0
  110. package/dist/update.d.ts +71 -0
  111. package/dist/updateWatch.d.ts +43 -0
  112. package/dist/web/approvalQueue.d.ts +8 -0
  113. package/dist/web/approvalQueue.js +59 -0
  114. package/dist/web/auth.d.ts +14 -0
  115. package/dist/web/clientState.d.ts +16 -0
  116. package/dist/web/folderPicker.d.ts +34 -0
  117. package/dist/web/folderPicker.js +544 -0
  118. package/dist/web/hub.d.ts +213 -0
  119. package/dist/web/hub.js +14 -2
  120. package/dist/web/localApi.d.ts +11 -0
  121. package/dist/web/localApi.js +260 -0
  122. package/dist/web/plugins.d.ts +8 -0
  123. package/dist/web/plugins.js +64 -0
  124. package/dist/web/server.d.ts +81 -0
  125. package/dist/web/server.js +127 -19
  126. package/dist/web/service.d.ts +7 -0
  127. package/dist/web/service.js +1 -0
  128. package/dist/web/session.d.ts +176 -0
  129. package/dist/web/session.js +153 -19
  130. package/dist/web/settings.d.ts +65 -0
  131. package/dist/web/terminalBridge.d.ts +15 -0
  132. package/dist/web/terminalBridge.js +8 -2
  133. package/dist/width.d.ts +6 -0
  134. package/docs/plugins/README.md +126 -0
  135. package/docs/plugins/agent-sdk.md +92 -0
  136. package/docs/plugins/architecture.md +64 -0
  137. package/docs/plugins/compatibility.md +34 -0
  138. package/docs/plugins/development.md +55 -0
  139. package/docs/plugins/manifest.md +59 -0
  140. package/docs/plugins/manifest.schema.json +286 -0
  141. package/docs/plugins/security.md +53 -0
  142. package/docs/plugins/skills-prompts.md +69 -0
  143. package/docs/plugins/ui-sdk.md +109 -0
  144. package/examples/local-api.py +105 -0
  145. package/examples/plugins/cards/card.html +6 -0
  146. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  147. package/examples/plugins/composer/editor.html +10 -0
  148. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  149. package/examples/plugins/function-tools/agent.js +32 -0
  150. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  151. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  152. package/examples/plugins/menus-tabs/panel.html +6 -0
  153. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  154. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  155. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  156. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  157. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  158. package/package.json +66 -42
  159. package/web/LOCAL-API.md +65 -0
  160. package/web/README.md +27 -6
  161. package/web/app.js +201 -32
  162. package/web/i18n.js +15 -0
  163. package/web/index.html +3 -1
  164. package/web/plugins.css +10 -0
  165. package/web/plugins.js +230 -0
  166. package/web/style.css +11 -2
@@ -1,4 +1,5 @@
1
1
  import { INTENSITY_PARAMS, intensityForModel, modelUsesReasoning } from "../config.js";
2
+ import { DiagnosticRequest, diagnosticHash } from "./diagnostics.js";
2
3
  import { currentTask, TaskControlError } from "./runtime.js";
3
4
  import { inputTokenBound, DEFAULT_OUTPUT_TOKENS } from "./context.js";
4
5
  import { HttpStatusError, describeError, isRetriableFailure, retryDelayMs, retryPolicy, waitBeforeRetry, } from "./retry.js";
@@ -35,7 +36,7 @@ const SUMMARY_SYSTEM_PROMPT = "You compress the conversation history of an AI co
35
36
  * token already reached the user, and nesting two retry loops would multiply
36
37
  * both the request count and the total wait by the attempt budget.
37
38
  */
38
- async function postStream(url, init) {
39
+ async function postStream(url, init, diagnostic) {
39
40
  const controller = new AbortController();
40
41
  const abort = () => controller.abort(init.signal?.reason);
41
42
  if (init.signal?.aborted)
@@ -44,7 +45,11 @@ async function postStream(url, init) {
44
45
  init.signal?.addEventListener("abort", abort, { once: true });
45
46
  const timer = setTimeout(() => controller.abort(new Error(`request timed out after ${CONNECT_TIMEOUT_MS / 1000}s`)), CONNECT_TIMEOUT_MS);
46
47
  try {
47
- return await fetch(url, { ...init, signal: controller.signal });
48
+ if (diagnostic && typeof init.body === "string")
49
+ diagnostic.dispatch(init.body);
50
+ const response = await fetch(url, { ...init, signal: controller.signal });
51
+ diagnostic?.headers(response.status);
52
+ return response;
48
53
  }
49
54
  finally {
50
55
  clearTimeout(timer);
@@ -64,6 +69,7 @@ function resolvePolicy(options) {
64
69
  */
65
70
  async function pauseForRetry(attempt, policy, error, options) {
66
71
  const delayMs = retryDelayMs(attempt - 1, policy);
72
+ options.diagnosticObserver?.retry(delayMs);
67
73
  options.onRetry?.({ attempt: attempt + 1, attempts: policy.attempts, delayMs, reason: describeError(error) });
68
74
  return waitBeforeRetry(attempt - 1, policy, options.cancelled);
69
75
  }
@@ -111,7 +117,8 @@ export class LLMClient {
111
117
  { role: "user", content: transcript },
112
118
  ];
113
119
  let out = "";
114
- for await (const chunk of this.streamTurn(messages, undefined, options))
120
+ const streamOptions = currentTask()?.runtime.diagnostics ? { ...options, diagnostic: { ...options.diagnostic, role: "compression" } } : options;
121
+ for await (const chunk of this.streamTurn(messages, undefined, streamOptions))
115
122
  out += chunk;
116
123
  const text = out.trim();
117
124
  if (this.outcome?.status === "failed" || !text || text.startsWith("[Error"))
@@ -141,6 +148,11 @@ export class LLMClient {
141
148
  this.anthropicContent = [];
142
149
  const controller = new AbortController();
143
150
  const runtime = currentTask()?.runtime;
151
+ const diagnostic = runtime?.diagnostics ? new DiagnosticRequest(runtime.diagnostics, { role: currentTask()?.depth ? "worker" : "main", ...options.diagnostic }, currentTask()) : undefined;
152
+ if (diagnostic)
153
+ runtime.diagnostics.emit("model_configuration", { request_id: diagnostic.requestId,
154
+ model_config_hash: diagnosticHash({ model: this.model.name, provider: this.provider, intensity: this.intensity, professional: this.professional, temperature: this.temperature, top_p: this.top_p }),
155
+ output_budget: options.maxOutputTokens ?? (runtime ? DEFAULT_OUTPUT_TOKENS : null) }, currentTask());
144
156
  const taskSignal = currentTask()?.signal;
145
157
  const release = runtime?.activity();
146
158
  const rootAbort = () => controller.abort(taskSignal?.reason);
@@ -157,10 +169,18 @@ export class LLMClient {
157
169
  if (options.cancelled?.())
158
170
  controller.abort();
159
171
  }, 100) : null;
160
- const requestOptions = { ...options, maxOutputTokens: options.maxOutputTokens ?? (runtime ? DEFAULT_OUTPUT_TOKENS : undefined), signal: controller.signal, cancelled };
172
+ const requestOptions = { ...options, ...(diagnostic ? { diagnosticObserver: diagnostic } : {}), maxOutputTokens: options.maxOutputTokens ?? (runtime ? DEFAULT_OUTPUT_TOKENS : undefined), signal: controller.signal, cancelled };
161
173
  try {
162
174
  if (controller.signal.aborted)
163
175
  return;
176
+ if (tools && runtime?.plugins) {
177
+ tools = runtime.availableTools(tools);
178
+ const suffix = await runtime.plugins.requestSuffix({ provider: this.provider, model: this.model.name, role: currentTask()?.role ?? "main", toolCount: tools.length });
179
+ if (suffix) {
180
+ messages = messages.map((message, index) => index === 0 && message.role === "system" ? { ...message, content: (message.content ?? "") + suffix } : message);
181
+ runtime.note("plugin_request_suffix", { suffix });
182
+ }
183
+ }
164
184
  if (this.provider === "Anthropic") {
165
185
  yield* this.streamAnthropic(messages, tools, requestOptions);
166
186
  return;
@@ -179,6 +199,7 @@ export class LLMClient {
179
199
  this.outcome = { status: "failed", error: { code: "output_limit", message: "Model output reached its limit before completion; no tool calls from this response were executed" } };
180
200
  }
181
201
  this.outcome ??= { status: cancelled() ? "cancelled" : "completed" };
202
+ diagnostic?.end(this.outcome.status);
182
203
  taskSignal?.removeEventListener("abort", rootAbort);
183
204
  release?.();
184
205
  if (timer)
@@ -275,7 +296,7 @@ export class LLMClient {
275
296
  headers,
276
297
  body: JSON.stringify(payload),
277
298
  signal: options.signal,
278
- });
299
+ }, options.diagnosticObserver);
279
300
  if (!resp.ok) {
280
301
  // Thrown rather than printed: a 502/503/504/429 is as transient as a
281
302
  // reset and deserves the same retries, while a 400/401 must surface at
@@ -290,6 +311,13 @@ export class LLMClient {
290
311
  if (!message)
291
312
  throw new Error("endpoint did not return a valid model completion");
292
313
  this.reasoningContent = message.reasoning_content ?? message.reasoning ?? "";
314
+ if (this.reasoningContent)
315
+ options.diagnosticObserver?.content("reasoning");
316
+ if (message.content)
317
+ options.diagnosticObserver?.content("text");
318
+ if (message.tool_calls?.length)
319
+ options.diagnosticObserver?.content("tool");
320
+ options.diagnosticObserver?.usageUpdate(body.usage);
293
321
  this.toolCalls = (message.tool_calls ?? []).map((call) => ({
294
322
  id: call.id, name: call.function.name, arguments: call.function.arguments,
295
323
  }));
@@ -338,6 +366,7 @@ export class LLMClient {
338
366
  continue;
339
367
  }
340
368
  const usage = chunk.usage;
369
+ options.diagnosticObserver?.usageUpdate(usage);
341
370
  if (usage) {
342
371
  this.usage = {
343
372
  prompt: usage.prompt_tokens ?? 0,
@@ -352,12 +381,18 @@ export class LLMClient {
352
381
  // DeepSeek calls it reasoning_content; other compatible endpoints use
353
382
  // the shorter `reasoning`. Neither is part of the visible reply.
354
383
  const reasoning = delta.reasoning_content ?? delta.reasoning;
355
- if (typeof reasoning === "string" && reasoning)
384
+ if (typeof reasoning === "string" && reasoning) {
385
+ options.diagnosticObserver?.content("reasoning");
356
386
  this.reasoningContent += reasoning;
387
+ }
357
388
  const content = delta.content;
358
- if (typeof content === "string" && content)
389
+ if (typeof content === "string" && content) {
390
+ options.diagnosticObserver?.content("text");
359
391
  yield content;
392
+ }
360
393
  const toolCalls = delta.tool_calls;
394
+ if (toolCalls?.length)
395
+ options.diagnosticObserver?.content("tool");
361
396
  if (toolCalls) {
362
397
  for (const tc of toolCalls) {
363
398
  const i = tc.index ?? 0;
@@ -486,7 +521,7 @@ export class LLMClient {
486
521
  headers,
487
522
  body: JSON.stringify(payload),
488
523
  signal: options.signal,
489
- });
524
+ }, options.diagnosticObserver);
490
525
  if (!resp.ok) {
491
526
  // Thrown rather than printed: a 502/503/504/429 is as transient as a
492
527
  // reset and deserves the same retries, while a 400/401 must surface at
@@ -499,6 +534,13 @@ export class LLMClient {
499
534
  if (!Array.isArray(body.content))
500
535
  throw new Error("endpoint did not return a valid model completion");
501
536
  this.captureAnthropicContent(body.content);
537
+ options.diagnosticObserver?.usageUpdate(body.usage);
538
+ if (this.reasoningContent)
539
+ options.diagnosticObserver?.content("reasoning");
540
+ if (this.toolCalls.length)
541
+ options.diagnosticObserver?.content("tool");
542
+ if (body.content.some(block => block.type === "text"))
543
+ options.diagnosticObserver?.content("text");
502
544
  this.finishReason = body.stop_reason ?? null;
503
545
  if (body.usage) {
504
546
  const prompt = body.usage.input_tokens ?? 0;
@@ -546,6 +588,13 @@ export class LLMClient {
546
588
  }
547
589
  const event = chunk;
548
590
  const type = event.type;
591
+ options.diagnosticObserver?.usageUpdate(event.message?.usage ?? event.usage);
592
+ if (event.content_block?.type === "tool_use" || event.delta?.partial_json)
593
+ options.diagnosticObserver?.content("tool");
594
+ if (event.delta?.thinking || event.content_block?.type === "thinking")
595
+ options.diagnosticObserver?.content("reasoning");
596
+ if (event.delta?.text || event.content_block?.text)
597
+ options.diagnosticObserver?.content("text");
549
598
  if (event.delta?.stop_reason)
550
599
  this.finishReason = event.delta.stop_reason;
551
600
  if (type === "error")
@@ -0,0 +1,14 @@
1
+ import type { Executor, ExecuteResult, ResolvedProvider } from "@cloudflare/codemode/browser";
2
+ interface Limits {
3
+ cpuMs: number;
4
+ memoryBytes: number;
5
+ idleMs: number;
6
+ }
7
+ /** The worker owns QuickJS; only named tool RPCs can reach the host. */
8
+ export declare class LocalCodeExecutor implements Executor {
9
+ private signal?;
10
+ private limits;
11
+ constructor(signal?: AbortSignal | undefined, limits?: Partial<Limits>);
12
+ execute(code: string, providersOrFns: ResolvedProvider[] | Record<string, (...args: unknown[]) => Promise<unknown>>): Promise<ExecuteResult>;
13
+ }
14
+ export {};
@@ -0,0 +1,15 @@
1
+ import { type ToolDefinition } from "./tools.js";
2
+ import type { PlanState } from "./plan.js";
3
+ export declare const CODE_TOOL = "execute_ts";
4
+ export declare class ProgramToolDenied extends Error {
5
+ }
6
+ interface ProgramOptions {
7
+ signal?: AbortSignal;
8
+ call(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<string>;
9
+ onPlan(plan: PlanState): void;
10
+ onStep(index: number, status: "active" | "done"): void;
11
+ }
12
+ export declare function getCodeToolDefinition(): ToolDefinition;
13
+ /** Only the program's return, bounded logs and completion ledger enter LLM context. */
14
+ export declare function executeCodeProgram(args: Record<string, unknown>, options: ProgramOptions): Promise<string>;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { LLMClient, StreamOptions, UsageInfo } from "./client.js";
2
+ import type { ToolDefinition } from "./tools.js";
3
+ import { type TaskRuntime } from "./runtime.js";
4
+ export interface ContextMessage {
5
+ role: string;
6
+ content?: string;
7
+ tool_calls?: unknown[];
8
+ tool_call_id?: string;
9
+ }
10
+ /** Spend only the root's reserved report request; local evidence remains the fallback. */
11
+ export declare function finalTaskReport(runtime: TaskRuntime, createClient: () => LLMClient, onUsage?: (usage: UsageInfo) => void, messages?: readonly ContextMessage[]): Promise<string>;
12
+ /** Context scheduling estimate for mixed ASCII/CJK; provider usage is also checked. */
13
+ export declare function estimateTokens(value: unknown): number;
14
+ export declare const DEFAULT_OUTPUT_TOKENS = 16384;
15
+ export declare function outputAllowance(capacity: number): number;
16
+ /** Reserve a byte-based upper estimate, including framing, when usage is unknown. */
17
+ export declare function inputTokenBound(value: unknown): number;
18
+ /** Compact complete assistant/tool groups before any request, including wrap-up. */
19
+ export declare function prepareContext<T extends ContextMessage>(options: {
20
+ messages: T[];
21
+ tools: ToolDefinition[];
22
+ capacity: number;
23
+ lastPromptTokens?: number;
24
+ preserveBrief?: boolean;
25
+ createClient(): LLMClient;
26
+ stream?: StreamOptions;
27
+ onUsage?(usage: UsageInfo): void;
28
+ onProgress?(): void;
29
+ }): Promise<void>;
@@ -0,0 +1,91 @@
1
+ export type DiagnosticRole = "main" | "planning" | "worker" | "verification" | "approval" | "compression" | "integration";
2
+ export interface DiagnosticScope {
3
+ runId: string;
4
+ parentRunId?: string;
5
+ role: string;
6
+ }
7
+ export interface DiagnosticRequestContext {
8
+ request_id?: string;
9
+ task_id?: string;
10
+ role?: DiagnosticRole;
11
+ }
12
+ export interface DiagnosticEvent {
13
+ seq: number;
14
+ trace_id: string;
15
+ at_ms: number;
16
+ event: string;
17
+ mode?: string;
18
+ role?: string;
19
+ task_id?: string;
20
+ scope_run_id?: string;
21
+ parent_scope_run_id?: string;
22
+ data: Record<string, unknown>;
23
+ }
24
+ export declare const diagnosticHash: (value: unknown) => string;
25
+ /** Opt-in metadata only. I/O failures and buffer pressure never change task decisions. */
26
+ export declare class DiagnosticRecorder {
27
+ readonly file: string;
28
+ private options;
29
+ readonly traceId: string;
30
+ private began;
31
+ private sequence;
32
+ private identifiers;
33
+ private buffer;
34
+ private bufferedBytes;
35
+ private scheduled?;
36
+ private writing?;
37
+ private closing?;
38
+ private closed;
39
+ private unavailable;
40
+ private scopes;
41
+ dropped: number;
42
+ ioErrors: number;
43
+ bytesWritten: number;
44
+ enqueueMs: number;
45
+ writeMs: number;
46
+ constructor(file: string, options?: {
47
+ traceId?: string;
48
+ mode?: string;
49
+ source?: "offline_scripted_transport" | "runtime_transport";
50
+ maxBufferedBytes?: number;
51
+ flushIntervalMs?: number;
52
+ });
53
+ id(kind: string): string;
54
+ associate(scope: DiagnosticScope, context: {
55
+ role?: string;
56
+ task_id?: string;
57
+ }): void;
58
+ context(scope?: DiagnosticScope): {
59
+ role?: string;
60
+ task_id?: string;
61
+ scope_run_id?: string;
62
+ parent_scope_run_id?: string;
63
+ };
64
+ emit(event: string, data?: Record<string, unknown>, scope?: DiagnosticScope): void;
65
+ span(event: string, data?: Record<string, unknown>, scope?: DiagnosticScope): {
66
+ id: string;
67
+ end(extra?: Record<string, unknown>): void;
68
+ };
69
+ flush(): Promise<void>;
70
+ close(): Promise<void>;
71
+ }
72
+ /** One logical stream, with distinct attempts and first reasoning/text/tool events. */
73
+ export declare class DiagnosticRequest {
74
+ private trace;
75
+ private context;
76
+ private scope?;
77
+ readonly requestId: string;
78
+ private attemptId?;
79
+ private attemptNumber;
80
+ private first;
81
+ private usage;
82
+ constructor(trace: DiagnosticRecorder, context: DiagnosticRequestContext, scope?: DiagnosticScope | undefined);
83
+ private emit;
84
+ dispatch(body: string): void;
85
+ headers(status: number): void;
86
+ content(kind: "reasoning" | "text" | "tool"): void;
87
+ usageUpdate(value: unknown): void;
88
+ retry(delayMs: number): void;
89
+ private finishAttempt;
90
+ end(status: string): void;
91
+ }
@@ -0,0 +1,242 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { performance } from "node:perf_hooks";
5
+ export const diagnosticHash = (value) => createHash("sha256")
6
+ .update(typeof value === "string" ? value : JSON.stringify(value) ?? "null").digest("hex");
7
+ const bytes = (value) => Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value) ?? "", "utf8");
8
+ const stringFields = new Set(["request_id", "attempt_id", "wait_id", "span_id", "tool_id", "tool_name", "task_id", "verifies",
9
+ "engine_run_id", "scope_run_id", "parent_scope_run_id", "role", "status", "reason_code", "resource", "artifact_kind", "mode", "provider", "source"]);
10
+ function safeData(data, depth = 0) {
11
+ const result = Object.create(null);
12
+ for (const [key, value] of Object.entries(data)) {
13
+ if (typeof value === "number" && Number.isFinite(value) || typeof value === "boolean" || value === null)
14
+ result[key] = value;
15
+ else if (typeof value === "string" && stringFields.has(key))
16
+ result[key] = value.slice(0, 160);
17
+ else if (typeof value === "string" && key.endsWith("_hash") && /^[a-f\d]{64}$/.test(value))
18
+ result[key] = value;
19
+ else if (Array.isArray(value) && ["dependencies", "blocker_ids"].includes(key))
20
+ result[key] = value.slice(0, 128).filter(v => typeof v === "string" && /^[a-zA-Z\d_.:-]{1,100}$/.test(v));
21
+ else if (value && typeof value === "object" && !Array.isArray(value) && depth < 2)
22
+ result[key] = safeData(value, depth + 1);
23
+ }
24
+ return result;
25
+ }
26
+ /** Opt-in metadata only. I/O failures and buffer pressure never change task decisions. */
27
+ export class DiagnosticRecorder {
28
+ file;
29
+ options;
30
+ traceId;
31
+ began = performance.now();
32
+ sequence = 0;
33
+ identifiers = 0;
34
+ buffer = [];
35
+ bufferedBytes = 0;
36
+ scheduled;
37
+ writing;
38
+ closing;
39
+ closed = false;
40
+ unavailable = false;
41
+ scopes = new Map();
42
+ dropped = 0;
43
+ ioErrors = 0;
44
+ bytesWritten = 0;
45
+ enqueueMs = 0;
46
+ writeMs = 0;
47
+ constructor(file, options = {}) {
48
+ this.file = file;
49
+ this.options = options;
50
+ this.traceId = options.traceId ?? randomUUID();
51
+ try {
52
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
53
+ fs.writeFileSync(file, "", { flag: "wx", mode: 0o600 });
54
+ }
55
+ catch {
56
+ this.ioErrors++;
57
+ this.unavailable = true;
58
+ }
59
+ this.emit("trace_start", { mode: options.mode ?? "unknown", source: options.source ?? "runtime_transport" });
60
+ }
61
+ id(kind) { return `${kind}-${++this.identifiers}`; }
62
+ associate(scope, context) {
63
+ this.scopes.set(scope.runId, { ...this.scopes.get(scope.runId), ...context, parent_scope_run_id: scope.parentRunId });
64
+ }
65
+ context(scope) {
66
+ if (!scope)
67
+ return {};
68
+ let record = this.scopes.get(scope.runId), parent = scope.parentRunId;
69
+ for (let depth = 0; !record?.task_id && parent && depth < 32; depth++) {
70
+ const inherited = this.scopes.get(parent);
71
+ if (inherited?.task_id)
72
+ record = { ...record, task_id: inherited.task_id };
73
+ parent = inherited?.parent_scope_run_id;
74
+ }
75
+ return { role: record?.role ?? scope.role, task_id: record?.task_id, scope_run_id: scope.runId, parent_scope_run_id: scope.parentRunId };
76
+ }
77
+ emit(event, data = {}, scope) {
78
+ if (this.closed)
79
+ return;
80
+ if (this.unavailable) {
81
+ this.dropped++;
82
+ return;
83
+ }
84
+ const started = performance.now();
85
+ const context = this.context(scope);
86
+ const sanitized = safeData(data);
87
+ const record = { seq: ++this.sequence, trace_id: this.traceId, at_ms: performance.now() - this.began,
88
+ event, mode: this.options.mode, ...context,
89
+ ...(typeof sanitized.role === "string" ? { role: sanitized.role } : {}),
90
+ ...(typeof sanitized.task_id === "string" ? { task_id: sanitized.task_id } : {}), data: sanitized };
91
+ const line = JSON.stringify(record) + "\n", size = Buffer.byteLength(line);
92
+ if (size > 8192 || this.bufferedBytes + size > (this.options.maxBufferedBytes ?? 512_000))
93
+ this.dropped++;
94
+ else {
95
+ this.buffer.push(line);
96
+ this.bufferedBytes += size;
97
+ }
98
+ this.enqueueMs += performance.now() - started;
99
+ if (!this.scheduled && !this.writing)
100
+ this.scheduled = setTimeout(() => { this.scheduled = undefined; void this.flush(); }, this.options.flushIntervalMs ?? 25);
101
+ }
102
+ span(event, data = {}, scope) {
103
+ const spanId = this.id("span"), began = performance.now();
104
+ this.emit(`${event}_start`, { ...data, span_id: spanId }, scope);
105
+ let ended = false;
106
+ return { id: spanId, end: extra => { if (!ended) {
107
+ ended = true;
108
+ this.emit(`${event}_end`, { ...data, ...extra, span_id: spanId, duration_ms: performance.now() - began }, scope);
109
+ } } };
110
+ }
111
+ async flush() {
112
+ if (this.writing)
113
+ return this.writing;
114
+ if (!this.buffer.length || this.unavailable)
115
+ return;
116
+ const content = this.buffer.join("");
117
+ this.buffer = [];
118
+ this.bufferedBytes = 0;
119
+ const began = performance.now();
120
+ this.writing = (async () => {
121
+ try {
122
+ // Deleting a run must not cause a late write to recreate it.
123
+ if (!fs.existsSync(this.file)) {
124
+ this.unavailable = true;
125
+ this.dropped += content.split("\n").length - 1;
126
+ return;
127
+ }
128
+ const handle = await fs.promises.open(this.file, "r+");
129
+ try {
130
+ const contentBytes = Buffer.from(content), start = (await handle.stat()).size;
131
+ for (let offset = 0; offset < contentBytes.length;) {
132
+ const written = await handle.write(contentBytes, offset, contentBytes.length - offset, start + offset);
133
+ if (!written.bytesWritten)
134
+ throw new Error("trace write made no progress");
135
+ offset += written.bytesWritten;
136
+ }
137
+ }
138
+ finally {
139
+ await handle.close();
140
+ }
141
+ this.bytesWritten += Buffer.byteLength(content);
142
+ }
143
+ catch {
144
+ this.ioErrors++;
145
+ this.unavailable = true;
146
+ }
147
+ finally {
148
+ this.writeMs += performance.now() - began;
149
+ }
150
+ })();
151
+ try {
152
+ await this.writing;
153
+ }
154
+ finally {
155
+ this.writing = undefined;
156
+ if (this.buffer.length && !this.closed && !this.unavailable && !this.scheduled)
157
+ this.scheduled = setTimeout(() => { this.scheduled = undefined; void this.flush(); }, this.options.flushIntervalMs ?? 25);
158
+ }
159
+ }
160
+ close() {
161
+ if (this.closing)
162
+ return this.closing;
163
+ this.emit("trace_summary", { dropped_events: this.dropped, io_errors: this.ioErrors, enqueue_ms: this.enqueueMs, write_ms: this.writeMs });
164
+ this.closed = true;
165
+ if (this.scheduled) {
166
+ clearTimeout(this.scheduled);
167
+ this.scheduled = undefined;
168
+ }
169
+ this.closing = (async () => { await this.writing; while (this.buffer.length && !this.unavailable)
170
+ await this.flush(); })();
171
+ return this.closing;
172
+ }
173
+ }
174
+ /** One logical stream, with distinct attempts and first reasoning/text/tool events. */
175
+ export class DiagnosticRequest {
176
+ trace;
177
+ context;
178
+ scope;
179
+ requestId;
180
+ attemptId;
181
+ attemptNumber = 0;
182
+ first = new Set();
183
+ usage = {};
184
+ constructor(trace, context, scope) {
185
+ this.trace = trace;
186
+ this.context = context;
187
+ this.scope = scope;
188
+ this.requestId = context.request_id ?? trace.id("request");
189
+ this.emit("model_stream_start");
190
+ }
191
+ emit(event, data = {}) {
192
+ this.trace.emit(event, { request_id: this.requestId, role: this.context.role ?? "main", task_id: this.context.task_id, ...data }, this.scope);
193
+ }
194
+ dispatch(body) {
195
+ this.finishAttempt("abandoned");
196
+ this.attemptId = this.trace.id("attempt");
197
+ this.attemptNumber++;
198
+ this.first.clear();
199
+ this.usage = {};
200
+ const payload = JSON.parse(body);
201
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
202
+ this.emit("provider_dispatch", { attempt_id: this.attemptId, attempt: this.attemptNumber, payload_bytes: bytes(body), payload_hash: diagnosticHash(body),
203
+ system_bytes: bytes(payload.system ?? "") + messages.filter(m => m.role === "system").reduce((n, m) => n + bytes(m), 0),
204
+ user_bytes: messages.filter(m => m.role === "user").reduce((n, m) => n + bytes(m), 0),
205
+ assistant_bytes: messages.filter(m => m.role === "assistant").reduce((n, m) => n + bytes(m), 0),
206
+ tool_result_bytes: messages.filter(m => m.role === "tool").reduce((n, m) => n + bytes(m), 0),
207
+ reasoning_echo_bytes: messages.reduce((n, m) => n + bytes(m.reasoning_content ?? ""), 0),
208
+ tool_schema_bytes: bytes(payload.tools ?? []), streaming: payload.stream !== false });
209
+ }
210
+ headers(status) { this.emit("provider_headers", { attempt_id: this.attemptId, http_status: status }); }
211
+ content(kind) {
212
+ if (!this.first.has("content")) {
213
+ this.first.add("content");
214
+ this.emit("provider_first_content", { attempt_id: this.attemptId });
215
+ }
216
+ if (!this.first.has(kind)) {
217
+ this.first.add(kind);
218
+ this.emit(`provider_first_${kind}`, { attempt_id: this.attemptId });
219
+ }
220
+ }
221
+ usageUpdate(value) {
222
+ if (!value || typeof value !== "object")
223
+ return;
224
+ const v = value, prompt = v.prompt_tokens ?? v.input_tokens, completion = v.completion_tokens ?? v.output_tokens;
225
+ const details = v.prompt_tokens_details;
226
+ const completionDetails = v.completion_tokens_details;
227
+ const entries = { input_tokens: prompt, output_tokens: completion, cached_input_tokens: details?.cached_tokens ?? v.prompt_cache_hit_tokens ?? v.cache_read_input_tokens,
228
+ reasoning_tokens: completionDetails?.reasoning_tokens };
229
+ for (const [key, count] of Object.entries(entries))
230
+ if (typeof count === "number" && Number.isFinite(count))
231
+ this.usage[key] = count;
232
+ }
233
+ retry(delayMs) { this.finishAttempt("retry"); this.emit("model_retry_wait", { delay_ms: delayMs }); }
234
+ finishAttempt(status) {
235
+ if (!this.attemptId)
236
+ return;
237
+ this.emit("provider_attempt_end", { attempt_id: this.attemptId, status, input_tokens: this.usage.input_tokens ?? null,
238
+ output_tokens: this.usage.output_tokens ?? null, cached_input_tokens: this.usage.cached_input_tokens ?? null, reasoning_tokens: this.usage.reasoning_tokens ?? null });
239
+ this.attemptId = undefined;
240
+ }
241
+ end(status) { this.finishAttempt(status); this.emit("model_stream_end", { status }); }
242
+ }