@linnlabs/linnkit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testkit.js CHANGED
@@ -105,11 +105,14 @@ __export(runtime_kernel_exports, {
105
105
  UserNode: () => UserNode,
106
106
  WaitUserNode: () => WaitUserNode,
107
107
  agentHasToolTrigger: () => agentHasToolTrigger,
108
+ appendStreamingProviderReasoningDetails: () => appendStreamingProviderReasoningDetails,
108
109
  applySystemReminders: () => applySystemReminders,
109
110
  audit: () => audit_exports,
110
111
  budgetWarningTrigger: () => budgetWarningTrigger,
111
112
  childRunTrace: () => child_run_trace_exports,
112
113
  childRuns: () => child_runs_exports,
114
+ compactProviderReasoningDetails: () => compactProviderReasoningDetails,
115
+ compactReasoningDetailsInValue: () => compactReasoningDetailsInValue,
113
116
  computeToolIdempotencyKey: () => computeToolIdempotencyKey,
114
117
  consoleAudit: () => consoleAudit,
115
118
  contextBudgetWarningTemplate: () => contextBudgetWarningTemplate,
@@ -194,7 +197,6 @@ var Logger = class {
194
197
  constructor(moduleName) {
195
198
  this.moduleName = moduleName;
196
199
  }
197
- moduleName;
198
200
  debug(message, data) {
199
201
  this.log(0 /* DEBUG */, "debug", message, data);
200
202
  }
@@ -1182,7 +1184,6 @@ var GraphExecutor = class {
1182
1184
  };
1183
1185
  this.telemetryPort = config.telemetryPort ?? noopTelemetry;
1184
1186
  }
1185
- checkpointer;
1186
1187
  nodes = /* @__PURE__ */ new Map();
1187
1188
  ephemeralLocals = /* @__PURE__ */ new Map();
1188
1189
  config;
@@ -4575,6 +4576,22 @@ var ToolNode = class {
4575
4576
  });
4576
4577
  }
4577
4578
  async run(state) {
4579
+ const events = [];
4580
+ while (true) {
4581
+ const result = await this.runNextPendingToolCall(state);
4582
+ if (Array.isArray(result.events) && result.events.length > 0) {
4583
+ events.push(...result.events);
4584
+ }
4585
+ if (result.kind === "route" && result.nextNodeId === "tool") {
4586
+ continue;
4587
+ }
4588
+ return {
4589
+ ...result,
4590
+ events
4591
+ };
4592
+ }
4593
+ }
4594
+ async runNextPendingToolCall(state) {
4578
4595
  const calls = state.local?.pendingToolCalls ?? [];
4579
4596
  const signalRaw = state.local?.signal;
4580
4597
  if (isAbortSignal(signalRaw) && signalRaw.aborted) {
@@ -4782,18 +4799,25 @@ var ToolNode = class {
4782
4799
  rawArguments: context.call.function?.arguments,
4783
4800
  parsedArguments: context.toolArgs
4784
4801
  });
4802
+ const remainingCalls = context.calls.slice(1);
4785
4803
  context.state.local = buildErrorLocalState({
4786
4804
  local: context.local,
4787
- remainingCalls: context.calls.slice(1),
4805
+ remainingCalls,
4788
4806
  conversationId: context.conversationId,
4789
4807
  turnId: context.turnId,
4790
4808
  runtimeEvents: context.bridge.getRuntimeEvents(),
4791
4809
  nextProtocolErrorCount: fuse.nextCount
4792
4810
  });
4793
- if (fuse.shouldFuse) {
4811
+ if (fuse.shouldFuse && remainingCalls.length === 0) {
4794
4812
  throw createToolProtocolFuseError(fuse.nextCount, context.exec.error);
4795
4813
  }
4796
- return { kind: "route", nextNodeId: "llm", events: context.bridge.getRuntimeEvents() };
4814
+ return {
4815
+ kind: "route",
4816
+ // 同一个 assistant.tool_calls batch 必须为每个 call 产出 tool_output。
4817
+ // 出错时也继续消费剩余 call,ToolNode.run 会在本节点内 drain 完 batch 再回 LLM。
4818
+ nextNodeId: remainingCalls.length > 0 ? "tool" : "llm",
4819
+ events: context.bridge.getRuntimeEvents()
4820
+ };
4797
4821
  }
4798
4822
  };
4799
4823
 
@@ -5530,18 +5554,18 @@ function createClassification(category, reason, suggestedDelay, extras) {
5530
5554
  }
5531
5555
  var ErrorClassifier = class {
5532
5556
  static classify(error, context) {
5533
- const isRecord26 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5557
+ const isRecord27 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5534
5558
  const baseMsg = (error.message || "").toLowerCase();
5535
5559
  const causeMsg = (() => {
5536
5560
  const cause = error.cause;
5537
5561
  if (typeof cause === "string") return cause.toLowerCase();
5538
5562
  if (cause instanceof Error) return (cause.message || "").toLowerCase();
5539
- if (isRecord26(cause) && typeof cause["message"] === "string") return String(cause["message"]).toLowerCase();
5563
+ if (isRecord27(cause) && typeof cause["message"] === "string") return String(cause["message"]).toLowerCase();
5540
5564
  return "";
5541
5565
  })();
5542
5566
  const causeCode = (() => {
5543
5567
  const cause = error.cause;
5544
- if (isRecord26(cause) && typeof cause["code"] === "string") return String(cause["code"]);
5568
+ if (isRecord27(cause) && typeof cause["code"] === "string") return String(cause["code"]);
5545
5569
  return "";
5546
5570
  })();
5547
5571
  const errorMessage = `${baseMsg} ${causeMsg}`.trim();
@@ -6033,6 +6057,9 @@ __export(llm_exports, {
6033
6057
  LLMPolicyEngine: () => LLMPolicyEngine,
6034
6058
  LlmCaller: () => LlmCaller,
6035
6059
  ModelResolver: () => ModelResolver,
6060
+ appendStreamingProviderReasoningDetails: () => appendStreamingProviderReasoningDetails,
6061
+ compactProviderReasoningDetails: () => compactProviderReasoningDetails,
6062
+ compactReasoningDetailsInValue: () => compactReasoningDetailsInValue,
6036
6063
  createDefaultTokenizerPort: () => createDefaultTokenizerPort,
6037
6064
  defaultPolicyEngine: () => defaultPolicyEngine
6038
6065
  });
@@ -6422,6 +6449,90 @@ function assertToolCallsHaveValidJsonArguments(toolCalls) {
6422
6449
  }
6423
6450
  }
6424
6451
 
6452
+ // src/runtime-kernel/llm/reasoning-details.ts
6453
+ var mergeableTextFields = ["reasoning_content"];
6454
+ function isRecord20(value) {
6455
+ return !!value && typeof value === "object" && !Array.isArray(value);
6456
+ }
6457
+ function findMergeableTextField(detail) {
6458
+ if (!isRecord20(detail)) return void 0;
6459
+ if (typeof detail.provider !== "string") return void 0;
6460
+ if (typeof detail.type !== "string") return void 0;
6461
+ for (const field of mergeableTextFields) {
6462
+ if (typeof detail[field] === "string") {
6463
+ return field;
6464
+ }
6465
+ }
6466
+ return void 0;
6467
+ }
6468
+ function hasOnlyStableTextDetailFields(detail, textField) {
6469
+ const allowedKeys = /* @__PURE__ */ new Set(["provider", "type", textField]);
6470
+ return Object.keys(detail).every((key) => allowedKeys.has(key));
6471
+ }
6472
+ function canMergeTextDetails(previous, incoming) {
6473
+ if (!isRecord20(previous) || !isRecord20(incoming)) return false;
6474
+ const previousField = findMergeableTextField(previous);
6475
+ const incomingField = findMergeableTextField(incoming);
6476
+ if (!previousField || previousField !== incomingField) return false;
6477
+ return previous.provider === incoming.provider && previous.type === incoming.type && hasOnlyStableTextDetailFields(previous, previousField) && hasOnlyStableTextDetailFields(incoming, incomingField);
6478
+ }
6479
+ function mergeStreamingText(previous, incoming) {
6480
+ if (!previous) return incoming;
6481
+ if (!incoming) return previous;
6482
+ if (incoming.startsWith(previous)) {
6483
+ return incoming;
6484
+ }
6485
+ if (previous.endsWith(incoming)) {
6486
+ return previous;
6487
+ }
6488
+ return `${previous}${incoming}`;
6489
+ }
6490
+ function appendStreamingProviderReasoningDetails(existing, incoming) {
6491
+ const next = [...existing];
6492
+ for (const detail of incoming) {
6493
+ const previous = next[next.length - 1];
6494
+ if (canMergeTextDetails(previous, detail)) {
6495
+ const textField = findMergeableTextField(previous);
6496
+ if (textField && isRecord20(detail)) {
6497
+ const mergedText = mergeStreamingText(String(previous[textField]), String(detail[textField]));
6498
+ if (mergedText === previous[textField]) {
6499
+ continue;
6500
+ }
6501
+ next[next.length - 1] = {
6502
+ ...previous,
6503
+ [textField]: mergedText
6504
+ };
6505
+ continue;
6506
+ }
6507
+ }
6508
+ next.push(detail);
6509
+ }
6510
+ return next;
6511
+ }
6512
+ function compactProviderReasoningDetails(reasoningDetails) {
6513
+ return appendStreamingProviderReasoningDetails([], reasoningDetails);
6514
+ }
6515
+ function compactReasoningDetailsInValue(value) {
6516
+ return compactValue(value);
6517
+ }
6518
+ function compactValue(value) {
6519
+ if (Array.isArray(value)) {
6520
+ return value.map((item) => compactValue(item));
6521
+ }
6522
+ if (!isRecord20(value)) {
6523
+ return value;
6524
+ }
6525
+ const compacted = {};
6526
+ for (const [key, childValue] of Object.entries(value)) {
6527
+ if (key === "reasoning_details" && Array.isArray(childValue)) {
6528
+ compacted[key] = compactProviderReasoningDetails(childValue);
6529
+ continue;
6530
+ }
6531
+ compacted[key] = compactValue(childValue);
6532
+ }
6533
+ return compacted;
6534
+ }
6535
+
6425
6536
  // src/runtime-kernel/llm/streaming-adapter.ts
6426
6537
  async function callLlmStream(params) {
6427
6538
  const {
@@ -6434,7 +6545,7 @@ async function callLlmStream(params) {
6434
6545
  } = params;
6435
6546
  let fullResponse = "";
6436
6547
  let streamError = null;
6437
- const reasoningDetails = [];
6548
+ let reasoningDetails = [];
6438
6549
  const streamAnswerId = generateMessageId();
6439
6550
  let streamChunkSeq = 0;
6440
6551
  let capturedUsage = void 0;
@@ -6504,13 +6615,21 @@ async function callLlmStream(params) {
6504
6615
  const reasoning = isRecord19(chunk) ? chunk["reasoning_details"] : void 0;
6505
6616
  if (reasoning !== void 0) {
6506
6617
  const newReasoningDetails = Array.isArray(reasoning) ? reasoning : [reasoning];
6507
- reasoningDetails.push(...newReasoningDetails);
6508
- eventHandler({
6509
- type: "provider_sidecar",
6510
- id: generateMessageId(),
6511
- timestamp: Date.now(),
6512
- reasoning_details: newReasoningDetails
6513
- });
6618
+ const previousReasoningDetails = reasoningDetails;
6619
+ const previousLength = previousReasoningDetails.length;
6620
+ const compactedReasoningDetails = appendStreamingProviderReasoningDetails(reasoningDetails, newReasoningDetails);
6621
+ reasoningDetails = compactedReasoningDetails;
6622
+ const previousLastChanged = previousLength > 0 && compactedReasoningDetails[previousLength - 1] !== previousReasoningDetails[previousLength - 1];
6623
+ const emitFromIndex = previousLastChanged ? previousLength - 1 : previousLength;
6624
+ const emittedReasoningDetails = compactedReasoningDetails.slice(Math.max(0, emitFromIndex));
6625
+ if (emittedReasoningDetails.length > 0) {
6626
+ eventHandler({
6627
+ type: "provider_sidecar",
6628
+ id: generateMessageId(),
6629
+ timestamp: Date.now(),
6630
+ reasoning_details: emittedReasoningDetails
6631
+ });
6632
+ }
6514
6633
  }
6515
6634
  if (chunk.tool_calls) {
6516
6635
  emitThoughtComplete(thoughtSegmenter.onBoundary());
@@ -6936,7 +7055,6 @@ var DefaultTokenizerPort = class {
6936
7055
  constructor(config = {}) {
6937
7056
  this.config = config;
6938
7057
  }
6939
- config;
6940
7058
  estimateText(text, _modelId) {
6941
7059
  return TokenCalculator.estimateTokens(text, {
6942
7060
  encoding: this.config.encoding,
@@ -7078,8 +7196,6 @@ var FinalAnswerCollector = class {
7078
7196
  this.conversationId = conversationId;
7079
7197
  this.turnId = turnId;
7080
7198
  }
7081
- conversationId;
7082
- turnId;
7083
7199
  answerId;
7084
7200
  chunks = [];
7085
7201
  chunkCount = 0;
@@ -7127,7 +7243,7 @@ var FinalAnswerCollector = class {
7127
7243
  };
7128
7244
 
7129
7245
  // src/runtime-kernel/child-runs/childRunTraceSink.ts
7130
- function isRecord20(v) {
7246
+ function isRecord21(v) {
7131
7247
  return !!v && typeof v === "object" && !Array.isArray(v);
7132
7248
  }
7133
7249
  function getString2(obj, key) {
@@ -7160,7 +7276,7 @@ function createChildRunTraceSink(params) {
7160
7276
  const { publisher, conversationId, turnId } = params;
7161
7277
  const finalAnswerCollector = new FinalAnswerCollector(conversationId, turnId);
7162
7278
  const sink = (evt) => {
7163
- if (!isRecord20(evt)) {
7279
+ if (!isRecord21(evt)) {
7164
7280
  return [];
7165
7281
  }
7166
7282
  const type = getString2(evt, "type");
@@ -7829,7 +7945,7 @@ function runRecordToMeta(record) {
7829
7945
  errorIfAny: record.errorIfAny ? { ...record.errorIfAny } : void 0
7830
7946
  };
7831
7947
  }
7832
- function isRecord21(value) {
7948
+ function isRecord22(value) {
7833
7949
  return typeof value === "object" && value !== null && !Array.isArray(value);
7834
7950
  }
7835
7951
  function readStringField(record, key) {
@@ -7850,7 +7966,7 @@ function getRunIdFromMetadata(event) {
7850
7966
  return snakeCaseRunId;
7851
7967
  }
7852
7968
  const runContext = metadata["run_context"];
7853
- if (isRecord21(runContext)) {
7969
+ if (isRecord22(runContext)) {
7854
7970
  return readStringField(runContext, "runId") ?? readStringField(runContext, "run_id");
7855
7971
  }
7856
7972
  return void 0;
@@ -8068,7 +8184,7 @@ function runMetaFromRecord(record) {
8068
8184
  }
8069
8185
 
8070
8186
  // src/runtime-kernel/run-supervisor/runSupervisor.ts
8071
- function isRecord22(value) {
8187
+ function isRecord23(value) {
8072
8188
  return typeof value === "object" && value !== null && !Array.isArray(value);
8073
8189
  }
8074
8190
  function readStringField2(record, key) {
@@ -8085,7 +8201,7 @@ function readRunIdFromRuntimeEvent(event) {
8085
8201
  return directRunId;
8086
8202
  }
8087
8203
  const runContext = metadata["run_context"];
8088
- if (!isRecord22(runContext)) {
8204
+ if (!isRecord23(runContext)) {
8089
8205
  return void 0;
8090
8206
  }
8091
8207
  return readStringField2(runContext, "runId") ?? readStringField2(runContext, "run_id");
@@ -8097,7 +8213,7 @@ function readAwaitingUserReason(event) {
8097
8213
  if (typeof event.prompt === "string" && event.prompt.trim().length > 0) {
8098
8214
  return event.prompt;
8099
8215
  }
8100
- if (isRecord22(event.form)) {
8216
+ if (isRecord23(event.form)) {
8101
8217
  const prompt = readStringField2(event.form, "prompt");
8102
8218
  if (prompt && prompt.trim().length > 0) {
8103
8219
  return prompt;
@@ -8983,11 +9099,11 @@ new MessageFormatter();
8983
9099
 
8984
9100
  // src/context-manager/profiles/agent/utils/toolInteractionGroup.ts
8985
9101
  var CHECKPOINT_TOOL_NAME = "context_checkpoint";
8986
- function isRecord23(value) {
9102
+ function isRecord24(value) {
8987
9103
  return typeof value === "object" && value !== null && !Array.isArray(value);
8988
9104
  }
8989
9105
  function isToolCallRecord(value) {
8990
- if (!isRecord23(value)) {
9106
+ if (!isRecord24(value)) {
8991
9107
  return false;
8992
9108
  }
8993
9109
  return typeof value.id === "string" && value.id.trim().length > 0;
@@ -9005,7 +9121,7 @@ function safeParseArgs(rawArgs) {
9005
9121
  }
9006
9122
  try {
9007
9123
  const parsed = JSON.parse(rawArgs);
9008
- return isRecord23(parsed) ? parsed : {};
9124
+ return isRecord24(parsed) ? parsed : {};
9009
9125
  } catch {
9010
9126
  return {};
9011
9127
  }
@@ -9299,7 +9415,7 @@ var summarizeToolOutput = (toolName, output, toolSummaryProvider, toolArgs, conf
9299
9415
  };
9300
9416
 
9301
9417
  // src/context-manager/profiles/agent/utils/eventConverter.ts
9302
- function isRecord24(value) {
9418
+ function isRecord25(value) {
9303
9419
  return !!value && typeof value === "object" && !Array.isArray(value);
9304
9420
  }
9305
9421
  function isHistorySummaryEvent2(event) {
@@ -9357,10 +9473,10 @@ function convertEventToAiMessage(event) {
9357
9473
  timestamp: event.timestamp,
9358
9474
  metadata: {
9359
9475
  tool_calls: (() => {
9360
- const isRecord26 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9476
+ const isRecord27 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9361
9477
  const payload = event.payload;
9362
9478
  const toolCallsFromPayload = (() => {
9363
- if (!isRecord26(payload)) return void 0;
9479
+ if (!isRecord27(payload)) return void 0;
9364
9480
  const raw = payload["tool_calls"];
9365
9481
  return Array.isArray(raw) ? raw : void 0;
9366
9482
  })();
@@ -9379,9 +9495,9 @@ function convertEventToAiMessage(event) {
9379
9495
  ];
9380
9496
  })(),
9381
9497
  reasoning_details: (() => {
9382
- const isRecord26 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9498
+ const isRecord27 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
9383
9499
  const payload = event.payload;
9384
- if (!isRecord26(payload)) return void 0;
9500
+ if (!isRecord27(payload)) return void 0;
9385
9501
  const rd = payload["reasoning_details"];
9386
9502
  return Array.isArray(rd) ? rd : void 0;
9387
9503
  })()
@@ -9390,15 +9506,15 @@ function convertEventToAiMessage(event) {
9390
9506
  case "tool_output": {
9391
9507
  const observationFromPayload = (() => {
9392
9508
  const payload = event.payload;
9393
- if (!isRecord24(payload)) return void 0;
9509
+ if (!isRecord25(payload)) return void 0;
9394
9510
  const result = payload["result"];
9395
- if (!isRecord24(result)) return void 0;
9511
+ if (!isRecord25(result)) return void 0;
9396
9512
  const obs = result["observation"];
9397
9513
  return typeof obs === "string" && obs.trim().length > 0 ? obs : void 0;
9398
9514
  })();
9399
9515
  const rawOutput = (() => {
9400
9516
  const payload = event.payload;
9401
- if (isRecord24(payload)) {
9517
+ if (isRecord25(payload)) {
9402
9518
  const out = payload["output"];
9403
9519
  if (typeof out === "string" && out.trim().length > 0) {
9404
9520
  return out;
@@ -9523,7 +9639,7 @@ __export(utils_exports, {
9523
9639
  });
9524
9640
 
9525
9641
  // src/testkit/context-harness/replayHarness.ts
9526
- function isRecord25(value) {
9642
+ function isRecord26(value) {
9527
9643
  return !!value && typeof value === "object" && !Array.isArray(value);
9528
9644
  }
9529
9645
  function createReplayHarness(events) {
@@ -9535,7 +9651,7 @@ function createReplayHarness(events) {
9535
9651
  if (message.role !== "assistant" || message.type !== "tool_calls") {
9536
9652
  return false;
9537
9653
  }
9538
- if (!isRecord25(message.metadata)) {
9654
+ if (!isRecord26(message.metadata)) {
9539
9655
  return false;
9540
9656
  }
9541
9657
  return Array.isArray(message.metadata.tool_calls);