@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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.cjs +118 -28
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +118 -28
- package/dist/cli.js.map +1 -1
- package/dist/context-manager.cjs +0 -4
- package/dist/context-manager.cjs.map +1 -1
- package/dist/context-manager.js +0 -4
- package/dist/context-manager.js.map +1 -1
- package/dist/{index-Cm-JbzTH.d.cts → index-BanRABEt.d.cts} +14 -3
- package/dist/{index-DRBWi1fy.d.ts → index-Z8NXKNwI.d.ts} +14 -3
- package/dist/index.cjs +146 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +146 -30
- package/dist/index.js.map +1 -1
- package/dist/quickstart.cjs +115 -25
- package/dist/quickstart.cjs.map +1 -1
- package/dist/quickstart.js +115 -25
- package/dist/quickstart.js.map +1 -1
- package/dist/runtime-kernel.cjs +142 -26
- package/dist/runtime-kernel.cjs.map +1 -1
- package/dist/runtime-kernel.d.cts +1 -1
- package/dist/runtime-kernel.d.ts +1 -1
- package/dist/runtime-kernel.js +140 -27
- package/dist/runtime-kernel.js.map +1 -1
- package/dist/testkit.cjs +155 -39
- package/dist/testkit.cjs.map +1 -1
- package/dist/testkit.js +155 -39
- package/dist/testkit.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.9.0] - 2026-05-22
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `appendStreamingProviderReasoningDetails` / `compactProviderReasoningDetails` / `compactReasoningDetailsInValue` in `@linnlabs/linnkit/runtime-kernel` — provider-agnostic helpers for merging adjacent streaming `reasoning_content` fragments.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Streaming `reasoning_details` now merges adjacent pure text reasoning fragments before returning the final LLM result and before emitting provider sidecar updates, preventing audit records from storing token-by-token reasoning fragments.
|
|
28
|
+
- `ToolNode` now drains every pending `assistant.tool_calls` item in the current batch before returning to the LLM, even when an earlier tool call fails. This preserves the protocol invariant that each tool call receives a corresponding tool output and avoids incomplete tool-call groups being dropped by downstream context assembly.
|
|
29
|
+
- Tool protocol fuse handling now waits until the current batch has been fully consumed before throwing, so one repeated protocol error cannot strand later tool calls in the same assistant message.
|
|
30
|
+
|
|
31
|
+
### Compatibility
|
|
32
|
+
|
|
33
|
+
- Minor bump because `@linnlabs/linnkit/runtime-kernel` has new public exports and ToolNode batch execution behavior is stricter.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
19
37
|
## [0.8.0] - 2026-05-13
|
|
20
38
|
|
|
21
39
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# linnkit
|
|
2
2
|
|
|
3
|
-
**A fine-grained context engineering framework for Agent applications — control every token sent to the model, with clear run lifecycle, audit records, and testable protocol boundaries.**
|
|
3
|
+
**A fine-grained context engineering framework for Agent applications — control every token sent to the model, with clear run lifecycle, audit records, and testable protocol boundaries.** It gives a small, auditable runtime for controlling the messages, tool results, summaries, and policies that shape every LLM call.
|
|
4
4
|
|
|
5
5
|
[中文文档](./README.zh-CN.md) · [Integration Guide](./docs/integration/README.md) · [Changelog](./CHANGELOG.md)
|
|
6
6
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# linnkit
|
|
2
2
|
|
|
3
|
-
**linnkit 是一个可以方便地精细化管理上下文的 Agent 框架,追求控制发给LLM的每一个 token,同时保留清晰的运行生命周期、审计记录和测试边界。**
|
|
3
|
+
**linnkit 是一个可以方便地精细化管理上下文的 Agent 框架,追求控制发给LLM的每一个 token,同时保留清晰的运行生命周期、审计记录和测试边界。** 它提供了一种小巧、可审计的运行时环境,可对消息、工具结果、摘要及策略进行管控,而正是这些要素决定了每一次大语言模型调用的行为。
|
|
4
4
|
|
|
5
5
|
[English](./README.md) · [接入文档](./docs/integration/README.md) · [更新日志](./CHANGELOG.md)
|
|
6
6
|
|
package/dist/cli.cjs
CHANGED
|
@@ -1084,7 +1084,6 @@ var Logger = class {
|
|
|
1084
1084
|
constructor(moduleName) {
|
|
1085
1085
|
this.moduleName = moduleName;
|
|
1086
1086
|
}
|
|
1087
|
-
moduleName;
|
|
1088
1087
|
debug(message, data) {
|
|
1089
1088
|
this.log(0 /* DEBUG */, "debug", message, data);
|
|
1090
1089
|
}
|
|
@@ -1166,7 +1165,6 @@ var GraphExecutor = class {
|
|
|
1166
1165
|
};
|
|
1167
1166
|
this.telemetryPort = config.telemetryPort ?? noopTelemetry;
|
|
1168
1167
|
}
|
|
1169
|
-
checkpointer;
|
|
1170
1168
|
nodes = /* @__PURE__ */ new Map();
|
|
1171
1169
|
ephemeralLocals = /* @__PURE__ */ new Map();
|
|
1172
1170
|
config;
|
|
@@ -4448,6 +4446,22 @@ var ToolNode = class {
|
|
|
4448
4446
|
});
|
|
4449
4447
|
}
|
|
4450
4448
|
async run(state) {
|
|
4449
|
+
const events = [];
|
|
4450
|
+
while (true) {
|
|
4451
|
+
const result = await this.runNextPendingToolCall(state);
|
|
4452
|
+
if (Array.isArray(result.events) && result.events.length > 0) {
|
|
4453
|
+
events.push(...result.events);
|
|
4454
|
+
}
|
|
4455
|
+
if (result.kind === "route" && result.nextNodeId === "tool") {
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
return {
|
|
4459
|
+
...result,
|
|
4460
|
+
events
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
async runNextPendingToolCall(state) {
|
|
4451
4465
|
const calls = state.local?.pendingToolCalls ?? [];
|
|
4452
4466
|
const signalRaw = state.local?.signal;
|
|
4453
4467
|
if (isAbortSignal(signalRaw) && signalRaw.aborted) {
|
|
@@ -4655,18 +4669,25 @@ var ToolNode = class {
|
|
|
4655
4669
|
rawArguments: context.call.function?.arguments,
|
|
4656
4670
|
parsedArguments: context.toolArgs
|
|
4657
4671
|
});
|
|
4672
|
+
const remainingCalls = context.calls.slice(1);
|
|
4658
4673
|
context.state.local = buildErrorLocalState({
|
|
4659
4674
|
local: context.local,
|
|
4660
|
-
remainingCalls
|
|
4675
|
+
remainingCalls,
|
|
4661
4676
|
conversationId: context.conversationId,
|
|
4662
4677
|
turnId: context.turnId,
|
|
4663
4678
|
runtimeEvents: context.bridge.getRuntimeEvents(),
|
|
4664
4679
|
nextProtocolErrorCount: fuse.nextCount
|
|
4665
4680
|
});
|
|
4666
|
-
if (fuse.shouldFuse) {
|
|
4681
|
+
if (fuse.shouldFuse && remainingCalls.length === 0) {
|
|
4667
4682
|
throw createToolProtocolFuseError(fuse.nextCount, context.exec.error);
|
|
4668
4683
|
}
|
|
4669
|
-
return {
|
|
4684
|
+
return {
|
|
4685
|
+
kind: "route",
|
|
4686
|
+
// 同一个 assistant.tool_calls batch 必须为每个 call 产出 tool_output。
|
|
4687
|
+
// 出错时也继续消费剩余 call,ToolNode.run 会在本节点内 drain 完 batch 再回 LLM。
|
|
4688
|
+
nextNodeId: remainingCalls.length > 0 ? "tool" : "llm",
|
|
4689
|
+
events: context.bridge.getRuntimeEvents()
|
|
4690
|
+
};
|
|
4670
4691
|
}
|
|
4671
4692
|
};
|
|
4672
4693
|
|
|
@@ -5162,18 +5183,18 @@ function createClassification(category, reason, suggestedDelay, extras) {
|
|
|
5162
5183
|
}
|
|
5163
5184
|
var ErrorClassifier = class {
|
|
5164
5185
|
static classify(error, context) {
|
|
5165
|
-
const
|
|
5186
|
+
const isRecord23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5166
5187
|
const baseMsg = (error.message || "").toLowerCase();
|
|
5167
5188
|
const causeMsg = (() => {
|
|
5168
5189
|
const cause = error.cause;
|
|
5169
5190
|
if (typeof cause === "string") return cause.toLowerCase();
|
|
5170
5191
|
if (cause instanceof Error) return (cause.message || "").toLowerCase();
|
|
5171
|
-
if (
|
|
5192
|
+
if (isRecord23(cause) && typeof cause["message"] === "string") return String(cause["message"]).toLowerCase();
|
|
5172
5193
|
return "";
|
|
5173
5194
|
})();
|
|
5174
5195
|
const causeCode = (() => {
|
|
5175
5196
|
const cause = error.cause;
|
|
5176
|
-
if (
|
|
5197
|
+
if (isRecord23(cause) && typeof cause["code"] === "string") return String(cause["code"]);
|
|
5177
5198
|
return "";
|
|
5178
5199
|
})();
|
|
5179
5200
|
const errorMessage = `${baseMsg} ${causeMsg}`.trim();
|
|
@@ -5849,6 +5870,67 @@ function assertToolCallsHaveValidJsonArguments(toolCalls) {
|
|
|
5849
5870
|
}
|
|
5850
5871
|
}
|
|
5851
5872
|
|
|
5873
|
+
// src/runtime-kernel/llm/reasoning-details.ts
|
|
5874
|
+
var mergeableTextFields = ["reasoning_content"];
|
|
5875
|
+
function isRecord18(value) {
|
|
5876
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5877
|
+
}
|
|
5878
|
+
function findMergeableTextField(detail) {
|
|
5879
|
+
if (!isRecord18(detail)) return void 0;
|
|
5880
|
+
if (typeof detail.provider !== "string") return void 0;
|
|
5881
|
+
if (typeof detail.type !== "string") return void 0;
|
|
5882
|
+
for (const field of mergeableTextFields) {
|
|
5883
|
+
if (typeof detail[field] === "string") {
|
|
5884
|
+
return field;
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
return void 0;
|
|
5888
|
+
}
|
|
5889
|
+
function hasOnlyStableTextDetailFields(detail, textField) {
|
|
5890
|
+
const allowedKeys = /* @__PURE__ */ new Set(["provider", "type", textField]);
|
|
5891
|
+
return Object.keys(detail).every((key) => allowedKeys.has(key));
|
|
5892
|
+
}
|
|
5893
|
+
function canMergeTextDetails(previous, incoming) {
|
|
5894
|
+
if (!isRecord18(previous) || !isRecord18(incoming)) return false;
|
|
5895
|
+
const previousField = findMergeableTextField(previous);
|
|
5896
|
+
const incomingField = findMergeableTextField(incoming);
|
|
5897
|
+
if (!previousField || previousField !== incomingField) return false;
|
|
5898
|
+
return previous.provider === incoming.provider && previous.type === incoming.type && hasOnlyStableTextDetailFields(previous, previousField) && hasOnlyStableTextDetailFields(incoming, incomingField);
|
|
5899
|
+
}
|
|
5900
|
+
function mergeStreamingText(previous, incoming) {
|
|
5901
|
+
if (!previous) return incoming;
|
|
5902
|
+
if (!incoming) return previous;
|
|
5903
|
+
if (incoming.startsWith(previous)) {
|
|
5904
|
+
return incoming;
|
|
5905
|
+
}
|
|
5906
|
+
if (previous.endsWith(incoming)) {
|
|
5907
|
+
return previous;
|
|
5908
|
+
}
|
|
5909
|
+
return `${previous}${incoming}`;
|
|
5910
|
+
}
|
|
5911
|
+
function appendStreamingProviderReasoningDetails(existing, incoming) {
|
|
5912
|
+
const next = [...existing];
|
|
5913
|
+
for (const detail of incoming) {
|
|
5914
|
+
const previous = next[next.length - 1];
|
|
5915
|
+
if (canMergeTextDetails(previous, detail)) {
|
|
5916
|
+
const textField = findMergeableTextField(previous);
|
|
5917
|
+
if (textField && isRecord18(detail)) {
|
|
5918
|
+
const mergedText = mergeStreamingText(String(previous[textField]), String(detail[textField]));
|
|
5919
|
+
if (mergedText === previous[textField]) {
|
|
5920
|
+
continue;
|
|
5921
|
+
}
|
|
5922
|
+
next[next.length - 1] = {
|
|
5923
|
+
...previous,
|
|
5924
|
+
[textField]: mergedText
|
|
5925
|
+
};
|
|
5926
|
+
continue;
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
next.push(detail);
|
|
5930
|
+
}
|
|
5931
|
+
return next;
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5852
5934
|
// src/runtime-kernel/llm/streaming-adapter.ts
|
|
5853
5935
|
async function callLlmStream(params) {
|
|
5854
5936
|
const {
|
|
@@ -5861,7 +5943,7 @@ async function callLlmStream(params) {
|
|
|
5861
5943
|
} = params;
|
|
5862
5944
|
let fullResponse = "";
|
|
5863
5945
|
let streamError = null;
|
|
5864
|
-
|
|
5946
|
+
let reasoningDetails = [];
|
|
5865
5947
|
const streamAnswerId = generateMessageId();
|
|
5866
5948
|
let streamChunkSeq = 0;
|
|
5867
5949
|
let capturedUsage = void 0;
|
|
@@ -5931,13 +6013,21 @@ async function callLlmStream(params) {
|
|
|
5931
6013
|
const reasoning = isRecord17(chunk) ? chunk["reasoning_details"] : void 0;
|
|
5932
6014
|
if (reasoning !== void 0) {
|
|
5933
6015
|
const newReasoningDetails = Array.isArray(reasoning) ? reasoning : [reasoning];
|
|
5934
|
-
reasoningDetails
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
6016
|
+
const previousReasoningDetails = reasoningDetails;
|
|
6017
|
+
const previousLength = previousReasoningDetails.length;
|
|
6018
|
+
const compactedReasoningDetails = appendStreamingProviderReasoningDetails(reasoningDetails, newReasoningDetails);
|
|
6019
|
+
reasoningDetails = compactedReasoningDetails;
|
|
6020
|
+
const previousLastChanged = previousLength > 0 && compactedReasoningDetails[previousLength - 1] !== previousReasoningDetails[previousLength - 1];
|
|
6021
|
+
const emitFromIndex = previousLastChanged ? previousLength - 1 : previousLength;
|
|
6022
|
+
const emittedReasoningDetails = compactedReasoningDetails.slice(Math.max(0, emitFromIndex));
|
|
6023
|
+
if (emittedReasoningDetails.length > 0) {
|
|
6024
|
+
eventHandler({
|
|
6025
|
+
type: "provider_sidecar",
|
|
6026
|
+
id: generateMessageId(),
|
|
6027
|
+
timestamp: Date.now(),
|
|
6028
|
+
reasoning_details: emittedReasoningDetails
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
5941
6031
|
}
|
|
5942
6032
|
if (chunk.tool_calls) {
|
|
5943
6033
|
emitThoughtComplete(thoughtSegmenter.onBoundary());
|
|
@@ -6508,7 +6598,7 @@ function runRecordToMeta(record) {
|
|
|
6508
6598
|
errorIfAny: record.errorIfAny ? { ...record.errorIfAny } : void 0
|
|
6509
6599
|
};
|
|
6510
6600
|
}
|
|
6511
|
-
function
|
|
6601
|
+
function isRecord19(value) {
|
|
6512
6602
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6513
6603
|
}
|
|
6514
6604
|
function readStringField(record, key) {
|
|
@@ -6529,7 +6619,7 @@ function getRunIdFromMetadata(event) {
|
|
|
6529
6619
|
return snakeCaseRunId;
|
|
6530
6620
|
}
|
|
6531
6621
|
const runContext = metadata["run_context"];
|
|
6532
|
-
if (
|
|
6622
|
+
if (isRecord19(runContext)) {
|
|
6533
6623
|
return readStringField(runContext, "runId") ?? readStringField(runContext, "run_id");
|
|
6534
6624
|
}
|
|
6535
6625
|
return void 0;
|
|
@@ -6747,7 +6837,7 @@ function runMetaFromRecord(record) {
|
|
|
6747
6837
|
}
|
|
6748
6838
|
|
|
6749
6839
|
// src/runtime-kernel/run-supervisor/runSupervisor.ts
|
|
6750
|
-
function
|
|
6840
|
+
function isRecord20(value) {
|
|
6751
6841
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6752
6842
|
}
|
|
6753
6843
|
function readStringField2(record, key) {
|
|
@@ -6764,7 +6854,7 @@ function readRunIdFromRuntimeEvent(event) {
|
|
|
6764
6854
|
return directRunId;
|
|
6765
6855
|
}
|
|
6766
6856
|
const runContext = metadata["run_context"];
|
|
6767
|
-
if (!
|
|
6857
|
+
if (!isRecord20(runContext)) {
|
|
6768
6858
|
return void 0;
|
|
6769
6859
|
}
|
|
6770
6860
|
return readStringField2(runContext, "runId") ?? readStringField2(runContext, "run_id");
|
|
@@ -6776,7 +6866,7 @@ function readAwaitingUserReason(event) {
|
|
|
6776
6866
|
if (typeof event.prompt === "string" && event.prompt.trim().length > 0) {
|
|
6777
6867
|
return event.prompt;
|
|
6778
6868
|
}
|
|
6779
|
-
if (
|
|
6869
|
+
if (isRecord20(event.form)) {
|
|
6780
6870
|
const prompt = readStringField2(event.form, "prompt");
|
|
6781
6871
|
if (prompt && prompt.trim().length > 0) {
|
|
6782
6872
|
return prompt;
|
|
@@ -7386,7 +7476,7 @@ function createQuickstartTelemetryPort(collector) {
|
|
|
7386
7476
|
}
|
|
7387
7477
|
|
|
7388
7478
|
// src/quickstart/runAgent.ts
|
|
7389
|
-
function
|
|
7479
|
+
function isRecord21(value) {
|
|
7390
7480
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7391
7481
|
}
|
|
7392
7482
|
function readString4(value) {
|
|
@@ -7400,7 +7490,7 @@ function resolveModelId(agent, options) {
|
|
|
7400
7490
|
return modelId;
|
|
7401
7491
|
}
|
|
7402
7492
|
function isQuickstartStreamChunkEvent(value) {
|
|
7403
|
-
return
|
|
7493
|
+
return isRecord21(value) && value.type === "stream_chunk" && typeof value.content === "string";
|
|
7404
7494
|
}
|
|
7405
7495
|
function createNoopObservationPreview() {
|
|
7406
7496
|
return {
|
|
@@ -7440,7 +7530,7 @@ function readFinalAnswer(events, checkpointLocal) {
|
|
|
7440
7530
|
if (chunks.length > 0) {
|
|
7441
7531
|
return chunks.join("");
|
|
7442
7532
|
}
|
|
7443
|
-
if (
|
|
7533
|
+
if (isRecord21(checkpointLocal)) {
|
|
7444
7534
|
const finalAnswer = checkpointLocal["finalAnswer"];
|
|
7445
7535
|
if (typeof finalAnswer === "string") {
|
|
7446
7536
|
return finalAnswer;
|
|
@@ -7449,7 +7539,7 @@ function readFinalAnswer(events, checkpointLocal) {
|
|
|
7449
7539
|
return "";
|
|
7450
7540
|
}
|
|
7451
7541
|
function readContextTrace(checkpointLocal) {
|
|
7452
|
-
if (!
|
|
7542
|
+
if (!isRecord21(checkpointLocal)) return void 0;
|
|
7453
7543
|
return checkpointLocal["contextTrace"];
|
|
7454
7544
|
}
|
|
7455
7545
|
async function emitRunEvent(event, sink) {
|
|
@@ -7573,11 +7663,11 @@ async function runAgent(agent, options) {
|
|
|
7573
7663
|
}
|
|
7574
7664
|
|
|
7575
7665
|
// src/cli/configLoader.ts
|
|
7576
|
-
function
|
|
7666
|
+
function isRecord22(value) {
|
|
7577
7667
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7578
7668
|
}
|
|
7579
7669
|
function readDefaultExport(moduleValue) {
|
|
7580
|
-
if (
|
|
7670
|
+
if (isRecord22(moduleValue) && "default" in moduleValue) {
|
|
7581
7671
|
return moduleValue.default;
|
|
7582
7672
|
}
|
|
7583
7673
|
return moduleValue;
|
|
@@ -7588,7 +7678,7 @@ async function loadConfig(configPath, cwd) {
|
|
|
7588
7678
|
moduleUrl.searchParams.set("t", String(Date.now()));
|
|
7589
7679
|
const loaded = await import(moduleUrl.href);
|
|
7590
7680
|
const config = readDefaultExport(loaded);
|
|
7591
|
-
if (!
|
|
7681
|
+
if (!isRecord22(config)) {
|
|
7592
7682
|
throw new Error(`[linnkit] config must export an object: ${absolutePath}`);
|
|
7593
7683
|
}
|
|
7594
7684
|
return defineConfig(config);
|