@haikeilabs/agentware 0.1.0 → 0.1.1
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/executor/agent_loop.d.ts +41 -0
- package/dist/executor/agent_loop.d.ts.map +1 -0
- package/dist/executor/agent_loop.js +206 -0
- package/dist/executor/index.d.ts +2 -1
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/llm/backend.d.ts +7 -0
- package/dist/llm/backend.d.ts.map +1 -1
- package/dist/llm/index.d.ts +2 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -0
- package/dist/llm/openai_backend.d.ts +51 -0
- package/dist/llm/openai_backend.d.ts.map +1 -0
- package/dist/llm/openai_backend.js +203 -0
- package/dist/llm/response.d.ts +5 -0
- package/dist/llm/response.d.ts.map +1 -1
- package/dist/middleware/guardrails/response_validator.d.ts +1 -1
- package/dist/middleware/guardrails/response_validator.d.ts.map +1 -1
- package/dist/middleware/guardrails/response_validator.js +1 -1
- package/dist/middleware/index.d.ts +5 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/inference.d.ts +8 -0
- package/dist/middleware/inference.d.ts.map +1 -1
- package/dist/middleware/inference.js +73 -31
- package/dist/reasoning/adapter.d.ts +33 -0
- package/dist/reasoning/adapter.d.ts.map +1 -0
- package/dist/reasoning/adapter.js +365 -0
- package/dist/reasoning/errors.d.ts +28 -0
- package/dist/reasoning/errors.d.ts.map +1 -0
- package/dist/reasoning/errors.js +57 -0
- package/dist/reasoning/index.d.ts +6 -0
- package/dist/reasoning/index.d.ts.map +1 -0
- package/dist/reasoning/index.js +4 -0
- package/dist/reasoning/options.d.ts +18 -0
- package/dist/reasoning/options.d.ts.map +1 -0
- package/dist/reasoning/options.js +18 -0
- package/dist/reasoning/types.d.ts +44 -0
- package/dist/reasoning/types.d.ts.map +1 -0
- package/dist/reasoning/types.js +32 -0
- package/dist/toolformat/formatter.d.ts +3 -3
- package/dist/toolformat/formatter.d.ts.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/registry.d.ts +4 -4
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/tool.d.ts +8 -1
- package/dist/tools/tool.d.ts.map +1 -1
- package/dist/tools/tool.js +7 -0
- package/package.json +10 -1
|
@@ -5,6 +5,7 @@ import type { ContextWindowManager } from "../llmcontext/context_window.js";
|
|
|
5
5
|
import type { ResponseValidator } from "./guardrails/response_validator.js";
|
|
6
6
|
import { ErrorTracker } from "./guardrails/error_tracker.js";
|
|
7
7
|
import { StepEnforcer } from "./guardrails/step_enforcer.js";
|
|
8
|
+
import { ReasoningAdapter, type ContextTree } from "../reasoning/index.js";
|
|
8
9
|
export declare class RetriesExhaustedError extends Error {
|
|
9
10
|
constructor(message: string);
|
|
10
11
|
}
|
|
@@ -13,6 +14,10 @@ export interface InferenceResult {
|
|
|
13
14
|
newMessages: Message[];
|
|
14
15
|
toolCallCounter: number;
|
|
15
16
|
attempts: number;
|
|
17
|
+
/** Normalized context tree for the final turn's reasoning (native
|
|
18
|
+
* reasoning_content and/or thinking tags). Null when the turn carried no
|
|
19
|
+
* reasoning. Holds bounded summaries only; raw reasoning is never attached. */
|
|
20
|
+
reasoningTree: ContextTree | null;
|
|
16
21
|
}
|
|
17
22
|
export interface InferenceConfig {
|
|
18
23
|
client: Backend;
|
|
@@ -23,6 +28,9 @@ export interface InferenceConfig {
|
|
|
23
28
|
toolSpecs: ToolDefinition[];
|
|
24
29
|
maxAttempts: number;
|
|
25
30
|
stepIndex?: number;
|
|
31
|
+
/** Optional reasoning adapter override for limits and registered model
|
|
32
|
+
* fields. */
|
|
33
|
+
reasoning?: ReasoningAdapter;
|
|
26
34
|
}
|
|
27
35
|
export declare function runInference(messages: Message[], cfg: InferenceConfig, sessionId?: string): Promise<InferenceResult | null>;
|
|
28
36
|
//# sourceMappingURL=inference.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inference.d.ts","sourceRoot":"","sources":["../../src/middleware/inference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAA2B,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAG5E,OAAO,KAAK,EAAE,iBAAiB,EAA8B,MAAM,oCAAoC,CAAC;AACxG,OAAO,EAAE,YAAY,EAAiB,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"inference.d.ts","sourceRoot":"","sources":["../../src/middleware/inference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAA2B,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAG5E,OAAO,KAAK,EAAE,iBAAiB,EAA8B,MAAM,oCAAoC,CAAC;AACxG,OAAO,EAAE,YAAY,EAAiB,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAkB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAE3F,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,OAAO,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB;;mFAE+E;IAC/E,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;iBACa;IACb,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAKD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,eAAe,EACpB,SAAS,GAAE,MAAW,GACrB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA8LjC"}
|
|
@@ -2,12 +2,15 @@ import { Role } from "../llm/request.js";
|
|
|
2
2
|
import { MessageType } from "./types.js";
|
|
3
3
|
import { ErrorCategory } from "./guardrails/error_tracker.js";
|
|
4
4
|
import { stepNudge } from "./guardrails/nudge.js";
|
|
5
|
+
import { ReasoningAdapter, ReasoningError } from "../reasoning/index.js";
|
|
5
6
|
export class RetriesExhaustedError extends Error {
|
|
6
7
|
constructor(message) {
|
|
7
8
|
super(message);
|
|
8
9
|
this.name = "RetriesExhaustedError";
|
|
9
10
|
}
|
|
10
11
|
}
|
|
12
|
+
// Default adapter strips and normalizes reasoning in the inference loop.
|
|
13
|
+
const defaultAdapter = new ReasoningAdapter();
|
|
11
14
|
export async function runInference(messages, cfg, sessionId = "") {
|
|
12
15
|
let maxAttempts = cfg.maxAttempts;
|
|
13
16
|
if (maxAttempts <= 0) {
|
|
@@ -49,43 +52,81 @@ export async function runInference(messages, cfg, sessionId = "") {
|
|
|
49
52
|
cfg.contextManager.updateTokenCount(resp.usage_tokens.total_tokens);
|
|
50
53
|
}
|
|
51
54
|
let validationResult = null;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// AR-1: normalize reasoning and strip it before ordinary tool-call
|
|
56
|
+
// parsing. Reasoning can arrive as a native reasoning_content field or as
|
|
57
|
+
// embedded thinking tags in content; both are combined into a synthetic
|
|
58
|
+
// structured input so the adapter sees the full picture. Malformed or
|
|
59
|
+
// unbounded reasoning fails closed: this turn is treated as invalid and
|
|
60
|
+
// retried rather than parsed partially.
|
|
61
|
+
let reasoningTree = null;
|
|
62
|
+
let cleanContent = resp.content;
|
|
63
|
+
let reasoningFailed = false;
|
|
64
|
+
if (resp.content || resp.reasoning) {
|
|
65
|
+
const reasoningInput = { content: resp.content };
|
|
66
|
+
if (resp.reasoning) {
|
|
67
|
+
reasoningInput["reasoning_content"] = resp.reasoning;
|
|
59
68
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
needsRetry: false,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
else if (resp.content) {
|
|
69
|
-
if (cfg.validator) {
|
|
70
|
-
validationResult = cfg.validator.validateTextResponse(resp.content);
|
|
69
|
+
const adapter = cfg.reasoning ?? defaultAdapter;
|
|
70
|
+
try {
|
|
71
|
+
reasoningTree = adapter.extract(reasoningInput, cfg.client.modelName(), "llm-backend");
|
|
72
|
+
cleanContent = adapter.strip(reasoningInput);
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (e instanceof ReasoningError) {
|
|
76
|
+
reasoningFailed = true;
|
|
77
|
+
// Fail closed: never echo content that may carry reasoning we could
|
|
78
|
+
// not parse.
|
|
79
|
+
cleanContent = "";
|
|
80
|
+
validationResult = {
|
|
81
|
+
toolCalls: [],
|
|
82
|
+
nudge: null,
|
|
83
|
+
needsRetry: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
74
89
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
}
|
|
91
|
+
if (!reasoningFailed) {
|
|
92
|
+
if (resp.tool_calls && resp.tool_calls.length > 0) {
|
|
93
|
+
const guardrailsToolCalls = resp.tool_calls.map((tc) => ({
|
|
94
|
+
tool: tc.name,
|
|
95
|
+
args: tc.arguments,
|
|
80
96
|
}));
|
|
97
|
+
if (cfg.validator) {
|
|
98
|
+
validationResult = cfg.validator.validateToolCalls(guardrailsToolCalls);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
validationResult = {
|
|
102
|
+
toolCalls: guardrailsToolCalls,
|
|
103
|
+
nudge: null,
|
|
104
|
+
needsRetry: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
else if (cleanContent) {
|
|
109
|
+
if (cfg.validator) {
|
|
110
|
+
validationResult = cfg.validator.validateTextResponse(cleanContent);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
validationResult = { toolCalls: [], nudge: null, needsRetry: false };
|
|
114
|
+
}
|
|
115
|
+
if (!validationResult.needsRetry && validationResult.toolCalls.length > 0) {
|
|
116
|
+
resp.tool_calls = validationResult.toolCalls.map((tc) => ({
|
|
117
|
+
id: "",
|
|
118
|
+
name: tc.tool,
|
|
119
|
+
arguments: tc.args,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
86
122
|
}
|
|
87
123
|
else {
|
|
88
|
-
|
|
124
|
+
if (cfg.validator) {
|
|
125
|
+
validationResult = cfg.validator.validateTextResponse("");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
validationResult = { toolCalls: [], nudge: null, needsRetry: true };
|
|
129
|
+
}
|
|
89
130
|
}
|
|
90
131
|
}
|
|
91
132
|
lastResponse = resp;
|
|
@@ -114,6 +155,7 @@ export async function runInference(messages, cfg, sessionId = "") {
|
|
|
114
155
|
newMessages: currentMessages,
|
|
115
156
|
toolCallCounter,
|
|
116
157
|
attempts,
|
|
158
|
+
reasoningTree,
|
|
117
159
|
};
|
|
118
160
|
}
|
|
119
161
|
if (cfg.errorTracker) {
|
|
@@ -132,7 +174,7 @@ export async function runInference(messages, cfg, sessionId = "") {
|
|
|
132
174
|
}
|
|
133
175
|
const failedMsg = {
|
|
134
176
|
role: Role.ASSISTANT,
|
|
135
|
-
content:
|
|
177
|
+
content: cleanContent,
|
|
136
178
|
meta: { type: MessageType.TEXT_RESPONSE },
|
|
137
179
|
};
|
|
138
180
|
currentMessages.push(failedMsg);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ReasoningError } from "./errors.js";
|
|
2
|
+
import { ReasoningOptions } from "./options.js";
|
|
3
|
+
import { ContextTree } from "./types.js";
|
|
4
|
+
export { ReasoningError };
|
|
5
|
+
export declare class ReasoningAdapter {
|
|
6
|
+
private readonly opts;
|
|
7
|
+
private readonly modelFields;
|
|
8
|
+
constructor(options?: ReasoningOptions);
|
|
9
|
+
get options(): Required<ReasoningOptions>;
|
|
10
|
+
/** Map a model name to a model-specific reasoning field name. Fields
|
|
11
|
+
* registered here are consulted before the known field set. */
|
|
12
|
+
registerModelField(model: string, fieldName: string): void;
|
|
13
|
+
/** Remove reasoning from raw and return the clean content that ordinary
|
|
14
|
+
* tool-call parsers should see. Fails closed. */
|
|
15
|
+
strip(raw: unknown): string;
|
|
16
|
+
/** Normalize reasoning found in raw into a versioned context tree. Raw
|
|
17
|
+
* reasoning is never stored on the tree: nodes carry bounded summaries. */
|
|
18
|
+
extract(raw: unknown, model?: string, backend?: string): ContextTree;
|
|
19
|
+
/** Strip and extract in one call: return the clean content and tree. */
|
|
20
|
+
parse(raw: unknown, model?: string, backend?: string): {
|
|
21
|
+
clean: string;
|
|
22
|
+
tree: ContextTree;
|
|
23
|
+
};
|
|
24
|
+
private analyze;
|
|
25
|
+
private analyzeString;
|
|
26
|
+
private analyzeBytes;
|
|
27
|
+
private analyzeStructured;
|
|
28
|
+
private extractReasoningField;
|
|
29
|
+
private analyzeText;
|
|
30
|
+
private hasRecognizedKey;
|
|
31
|
+
private parseNodes;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/reasoning/adapter.ts"],"names":[],"mappings":"AAcA,OAAO,EAKL,cAAc,EAKf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAoB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAoB,WAAW,EAAsC,MAAM,YAAY,CAAC;AAE/F,OAAO,EAAE,cAAc,EAAE,CAAC;AAmE1B,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;gBAEtC,OAAO,CAAC,EAAE,gBAAgB;IAKtC,IAAI,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,CAExC;IAED;mEAC+D;IAC/D,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1D;qDACiD;IACjD,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM;IAI3B;+EAC2E;IAC3E,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAK,EAAE,OAAO,SAAK,GAAG,WAAW;IA0B5D,wEAAwE;IACxE,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAK,EAAE,OAAO,SAAK,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,WAAW,CAAA;KAAE;IAKnF,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,UAAU;CA2DnB"}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// Model-agnostic reasoning adapter (mirrors go/reasoning/adapter.go).
|
|
2
|
+
//
|
|
3
|
+
// The adapter recognizes three shapes of model output:
|
|
4
|
+
// - native reasoning_content fields (DeepSeek/Qwen/Kimi/GLM-style JSON)
|
|
5
|
+
// - generic thinking tags (<thinking>...</thinking>, [THINK]...[/THINK])
|
|
6
|
+
// - model-specific reasoning fields (registered per model name)
|
|
7
|
+
//
|
|
8
|
+
// Reasoning is stripped before ordinary tool-call parsing, and the extracted
|
|
9
|
+
// reasoning text is normalized into a versioned context tree. Raw reasoning is
|
|
10
|
+
// never exposed in normal replies or default audits: tree nodes carry only
|
|
11
|
+
// bounded summaries. The adapter fails closed on malformed or unbounded input.
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { MalformedContentError, MalformedInputError, MalformedJSONError, MalformedReasoningFieldError, ReasoningError, ReasoningTooLargeError, TooDeepError, TooManyNodesError, UnbalancedThinkingError, } from "./errors.js";
|
|
14
|
+
import { normalizeOptions } from "./options.js";
|
|
15
|
+
import { CONTRACT_VERSION, Format, NodeKind, NodeStatus } from "./types.js";
|
|
16
|
+
export { ReasoningError };
|
|
17
|
+
// Known reasoning field names on a structured response, in priority order.
|
|
18
|
+
// reasoning_content is the de-facto native field (DeepSeek, Qwen/QwQ, Kimi,
|
|
19
|
+
// GLM); the others cover common model-specific names.
|
|
20
|
+
const KNOWN_REASONING_FIELDS = ["reasoning_content", "thinking", "thinking_content", "reasoning"];
|
|
21
|
+
// Keys that make a structured response worth entering the structured path. A
|
|
22
|
+
// mapping without any of these is treated as opaque text and returned
|
|
23
|
+
// unchanged.
|
|
24
|
+
const RECOGNIZED_KEYS = new Set([
|
|
25
|
+
"message",
|
|
26
|
+
"content",
|
|
27
|
+
"reasoning_content",
|
|
28
|
+
"thinking",
|
|
29
|
+
"thinking_content",
|
|
30
|
+
"reasoning",
|
|
31
|
+
]);
|
|
32
|
+
const THINKING_OPEN = /<thinking>|\[THINK\]/i;
|
|
33
|
+
const THINKING_CLOSE = /<\/thinking>|\[\/THINK\]/i;
|
|
34
|
+
const PARAGRAPH_SEP = /\n[ \t]*\n/;
|
|
35
|
+
const LIST_ORNAMENT = /^\s*(?:[-*+•]|\d+[.)])\s+/;
|
|
36
|
+
const KIND_PREFIX = /^\s*(goal|observation|decision|tool-call|tool_call|tool-result|tool_result|tool|conclusion|blocker)\s*[:=]\s*/i;
|
|
37
|
+
const STATUS_MARKER = /\s*\[(blocked|pending|active|superseded|complete)\]\s*$/i;
|
|
38
|
+
const EVIDENCE_PATTERNS = [
|
|
39
|
+
/\[E\d+\]/g,
|
|
40
|
+
/\bref\s*[=:]\s*(\S+)/g,
|
|
41
|
+
/\bevidence\s*[=:]\s*(\S+)/g,
|
|
42
|
+
];
|
|
43
|
+
const TOOL_REF = /\b(?:tool|call)\s*[=:]\s*([a-zA-Z_][a-zA-Z0-9_.-]*)/;
|
|
44
|
+
const LEADING_IDENTIFIER = /^([a-zA-Z_][a-zA-Z0-9_.-]*)\s*[:=]/;
|
|
45
|
+
const BARE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_.-]*$/;
|
|
46
|
+
const KIND_BY_LABEL = {
|
|
47
|
+
goal: NodeKind.GOAL,
|
|
48
|
+
observation: NodeKind.OBSERVATION,
|
|
49
|
+
decision: NodeKind.DECISION,
|
|
50
|
+
"tool-call": NodeKind.TOOL_CALL,
|
|
51
|
+
tool_call: NodeKind.TOOL_CALL,
|
|
52
|
+
tool: NodeKind.TOOL_CALL,
|
|
53
|
+
"tool-result": NodeKind.TOOL_RESULT,
|
|
54
|
+
tool_result: NodeKind.TOOL_RESULT,
|
|
55
|
+
conclusion: NodeKind.CONCLUSION,
|
|
56
|
+
blocker: NodeKind.BLOCKER,
|
|
57
|
+
};
|
|
58
|
+
const STATUS_BY_LABEL = {
|
|
59
|
+
blocked: NodeStatus.BLOCKED,
|
|
60
|
+
pending: NodeStatus.ACTIVE,
|
|
61
|
+
active: NodeStatus.ACTIVE,
|
|
62
|
+
superseded: NodeStatus.SUPERSEDED,
|
|
63
|
+
complete: NodeStatus.COMPLETE,
|
|
64
|
+
};
|
|
65
|
+
function isRecord(v) {
|
|
66
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
67
|
+
}
|
|
68
|
+
export class ReasoningAdapter {
|
|
69
|
+
opts;
|
|
70
|
+
modelFields;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.opts = normalizeOptions(options);
|
|
73
|
+
this.modelFields = new Map();
|
|
74
|
+
}
|
|
75
|
+
get options() {
|
|
76
|
+
return this.opts;
|
|
77
|
+
}
|
|
78
|
+
/** Map a model name to a model-specific reasoning field name. Fields
|
|
79
|
+
* registered here are consulted before the known field set. */
|
|
80
|
+
registerModelField(model, fieldName) {
|
|
81
|
+
if (model && fieldName) {
|
|
82
|
+
this.modelFields.set(model.toLowerCase(), fieldName);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Remove reasoning from raw and return the clean content that ordinary
|
|
86
|
+
* tool-call parsers should see. Fails closed. */
|
|
87
|
+
strip(raw) {
|
|
88
|
+
return this.analyze(raw, "").clean;
|
|
89
|
+
}
|
|
90
|
+
/** Normalize reasoning found in raw into a versioned context tree. Raw
|
|
91
|
+
* reasoning is never stored on the tree: nodes carry bounded summaries. */
|
|
92
|
+
extract(raw, model = "", backend = "") {
|
|
93
|
+
const analysis = this.analyze(raw, model);
|
|
94
|
+
const now = this.opts.clock();
|
|
95
|
+
const tree = {
|
|
96
|
+
version: CONTRACT_VERSION,
|
|
97
|
+
format: analysis.format,
|
|
98
|
+
model,
|
|
99
|
+
backend,
|
|
100
|
+
created_at: now,
|
|
101
|
+
root_id: "",
|
|
102
|
+
nodes: [],
|
|
103
|
+
};
|
|
104
|
+
if (analysis.reasoning.trim() === "") {
|
|
105
|
+
return tree;
|
|
106
|
+
}
|
|
107
|
+
const nodes = this.parseNodes(analysis.reasoning, now);
|
|
108
|
+
tree.nodes = nodes;
|
|
109
|
+
if (nodes.length > 0) {
|
|
110
|
+
tree.root_id = nodes[0].id;
|
|
111
|
+
}
|
|
112
|
+
return tree;
|
|
113
|
+
}
|
|
114
|
+
/** Strip and extract in one call: return the clean content and tree. */
|
|
115
|
+
parse(raw, model = "", backend = "") {
|
|
116
|
+
const analysis = this.analyze(raw, model);
|
|
117
|
+
return { clean: analysis.clean, tree: this.extract(raw, model, backend) };
|
|
118
|
+
}
|
|
119
|
+
analyze(raw, model) {
|
|
120
|
+
if (typeof raw === "string") {
|
|
121
|
+
return this.analyzeString(raw, model);
|
|
122
|
+
}
|
|
123
|
+
if (raw instanceof Uint8Array) {
|
|
124
|
+
return this.analyzeBytes(raw, model);
|
|
125
|
+
}
|
|
126
|
+
if (isRecord(raw)) {
|
|
127
|
+
return this.analyzeStructured(raw, model);
|
|
128
|
+
}
|
|
129
|
+
throw new MalformedInputError(`unsupported raw type ${typeof raw}`);
|
|
130
|
+
}
|
|
131
|
+
analyzeString(raw, model) {
|
|
132
|
+
const trimmed = raw.trim();
|
|
133
|
+
if (trimmed.startsWith("{")) {
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(trimmed);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
throw new MalformedJSONError(e instanceof Error ? e.message : String(e));
|
|
140
|
+
}
|
|
141
|
+
if (isRecord(parsed) && this.hasRecognizedKey(parsed)) {
|
|
142
|
+
return this.analyzeStructured(parsed, model);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return this.analyzeText(raw);
|
|
146
|
+
}
|
|
147
|
+
analyzeBytes(raw, model) {
|
|
148
|
+
const trimmed = raw.length > 0 ? new TextDecoder().decode(raw).trim() : "";
|
|
149
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(trimmed);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
throw new MalformedJSONError(e instanceof Error ? e.message : String(e));
|
|
156
|
+
}
|
|
157
|
+
if (isRecord(parsed) && this.hasRecognizedKey(parsed)) {
|
|
158
|
+
return this.analyzeStructured(parsed, model);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return this.analyzeText(trimmed);
|
|
162
|
+
}
|
|
163
|
+
analyzeStructured(m, model) {
|
|
164
|
+
const message = m["message"];
|
|
165
|
+
if (isRecord(message)) {
|
|
166
|
+
return this.analyzeStructured(message, model);
|
|
167
|
+
}
|
|
168
|
+
let content = "";
|
|
169
|
+
if ("content" in m) {
|
|
170
|
+
const value = m["content"];
|
|
171
|
+
if (value === null || value === undefined) {
|
|
172
|
+
content = "";
|
|
173
|
+
}
|
|
174
|
+
else if (typeof value === "string") {
|
|
175
|
+
content = value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new MalformedContentError(`content field is ${typeof value}, not a string`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const [reasoning, format] = this.extractReasoningField(m, model);
|
|
182
|
+
if (reasoning !== "") {
|
|
183
|
+
return { reasoning, clean: splitThinkingTags(content).clean, format };
|
|
184
|
+
}
|
|
185
|
+
const result = splitThinkingTags(content);
|
|
186
|
+
if (result.reasoning !== "") {
|
|
187
|
+
return { reasoning: result.reasoning, clean: result.clean, format: Format.THINKING };
|
|
188
|
+
}
|
|
189
|
+
return { reasoning: "", clean: content, format: Format.NONE };
|
|
190
|
+
}
|
|
191
|
+
extractReasoningField(m, model) {
|
|
192
|
+
if (model) {
|
|
193
|
+
const fieldName = this.modelFields.get(model.toLowerCase());
|
|
194
|
+
if (fieldName) {
|
|
195
|
+
const value = m[fieldName];
|
|
196
|
+
if (value !== null && value !== undefined) {
|
|
197
|
+
if (typeof value !== "string") {
|
|
198
|
+
throw new MalformedReasoningFieldError(`field "${fieldName}" is ${typeof value}, not a string`);
|
|
199
|
+
}
|
|
200
|
+
if (value !== "") {
|
|
201
|
+
return [value, Format.MODEL_SPECIFIC];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const fieldName of KNOWN_REASONING_FIELDS) {
|
|
207
|
+
const value = m[fieldName];
|
|
208
|
+
if (value === null || value === undefined) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typeof value !== "string") {
|
|
212
|
+
throw new MalformedReasoningFieldError(`field "${fieldName}" is ${typeof value}, not a string`);
|
|
213
|
+
}
|
|
214
|
+
if (value !== "") {
|
|
215
|
+
return [value, Format.NATIVE];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return ["", Format.NONE];
|
|
219
|
+
}
|
|
220
|
+
analyzeText(raw) {
|
|
221
|
+
const result = splitThinkingTags(raw);
|
|
222
|
+
if (result.reasoning !== "") {
|
|
223
|
+
return { reasoning: result.reasoning, clean: result.clean, format: Format.THINKING };
|
|
224
|
+
}
|
|
225
|
+
return { reasoning: "", clean: raw, format: Format.NONE };
|
|
226
|
+
}
|
|
227
|
+
hasRecognizedKey(m) {
|
|
228
|
+
return Object.keys(m).some((k) => RECOGNIZED_KEYS.has(k));
|
|
229
|
+
}
|
|
230
|
+
parseNodes(reasoning, now) {
|
|
231
|
+
if (Buffer.byteLength(reasoning, "utf8") > this.opts.max_reasoning_bytes) {
|
|
232
|
+
throw new ReasoningTooLargeError();
|
|
233
|
+
}
|
|
234
|
+
const paragraphs = splitParagraphs(reasoning);
|
|
235
|
+
if (paragraphs.length > this.opts.max_nodes) {
|
|
236
|
+
throw new TooManyNodesError();
|
|
237
|
+
}
|
|
238
|
+
const nodes = [];
|
|
239
|
+
let parentId = "";
|
|
240
|
+
let depth = 0;
|
|
241
|
+
for (const para of paragraphs) {
|
|
242
|
+
depth += 1;
|
|
243
|
+
if (depth > this.opts.max_depth) {
|
|
244
|
+
throw new TooDeepError();
|
|
245
|
+
}
|
|
246
|
+
let kind = NodeKind.OBSERVATION;
|
|
247
|
+
let summary = para.replace(LIST_ORNAMENT, "");
|
|
248
|
+
const kindMatch = KIND_PREFIX.exec(summary);
|
|
249
|
+
if (kindMatch) {
|
|
250
|
+
kind = KIND_BY_LABEL[kindMatch[1].toLowerCase()] ?? NodeKind.OBSERVATION;
|
|
251
|
+
summary = summary.slice(kindMatch[0].length).trim();
|
|
252
|
+
}
|
|
253
|
+
let status = NodeStatus.COMPLETE;
|
|
254
|
+
const statusMatch = STATUS_MARKER.exec(summary);
|
|
255
|
+
if (statusMatch) {
|
|
256
|
+
status = STATUS_BY_LABEL[statusMatch[1].toLowerCase()] ?? NodeStatus.COMPLETE;
|
|
257
|
+
summary = summary.slice(0, statusMatch.index).trim();
|
|
258
|
+
}
|
|
259
|
+
else if (kind === NodeKind.BLOCKER) {
|
|
260
|
+
status = NodeStatus.BLOCKED;
|
|
261
|
+
}
|
|
262
|
+
summary = truncateCodePoints(summary, this.opts.max_summary_chars);
|
|
263
|
+
if (summary === "") {
|
|
264
|
+
depth -= 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const id = nodeId(kind, parentId, summary);
|
|
268
|
+
nodes.push({
|
|
269
|
+
id,
|
|
270
|
+
parent_id: parentId,
|
|
271
|
+
kind,
|
|
272
|
+
status,
|
|
273
|
+
summary,
|
|
274
|
+
evidence_refs: extractEvidence(summary),
|
|
275
|
+
tool_call_refs: extractToolRefs(kind, summary),
|
|
276
|
+
created_at: now,
|
|
277
|
+
updated_at: now,
|
|
278
|
+
});
|
|
279
|
+
parentId = id;
|
|
280
|
+
}
|
|
281
|
+
return nodes;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function splitThinkingTags(text) {
|
|
285
|
+
if (!THINKING_OPEN.test(text) && !THINKING_CLOSE.test(text)) {
|
|
286
|
+
return { clean: text, reasoning: "" };
|
|
287
|
+
}
|
|
288
|
+
const parts = [];
|
|
289
|
+
const reasons = [];
|
|
290
|
+
let rest = text;
|
|
291
|
+
for (;;) {
|
|
292
|
+
const open = THINKING_OPEN.exec(rest);
|
|
293
|
+
if (open === null) {
|
|
294
|
+
if (THINKING_CLOSE.test(rest)) {
|
|
295
|
+
throw new UnbalancedThinkingError();
|
|
296
|
+
}
|
|
297
|
+
parts.push(rest);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
parts.push(rest.slice(0, open.index));
|
|
301
|
+
const inner = rest.slice(open.index + open[0].length);
|
|
302
|
+
const close = THINKING_CLOSE.exec(inner);
|
|
303
|
+
if (close === null) {
|
|
304
|
+
throw new UnbalancedThinkingError();
|
|
305
|
+
}
|
|
306
|
+
reasons.push(inner.slice(0, close.index));
|
|
307
|
+
rest = inner.slice(close.index + close[0].length);
|
|
308
|
+
}
|
|
309
|
+
return { clean: parts.join(""), reasoning: reasons.join("\n").trim() };
|
|
310
|
+
}
|
|
311
|
+
function splitParagraphs(text) {
|
|
312
|
+
return text
|
|
313
|
+
.split(PARAGRAPH_SEP)
|
|
314
|
+
.map((p) => p.trim())
|
|
315
|
+
.filter((p) => p !== "");
|
|
316
|
+
}
|
|
317
|
+
function extractEvidence(summary) {
|
|
318
|
+
const out = [];
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
for (const pattern of EVIDENCE_PATTERNS) {
|
|
321
|
+
pattern.lastIndex = 0;
|
|
322
|
+
let match;
|
|
323
|
+
while ((match = pattern.exec(summary)) !== null) {
|
|
324
|
+
const ref = (match[1] ?? match[0]).replace(/^\[|\]$/g, "");
|
|
325
|
+
if (ref !== "" && !seen.has(ref)) {
|
|
326
|
+
seen.add(ref);
|
|
327
|
+
out.push(ref);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
function extractToolRefs(kind, summary) {
|
|
334
|
+
if (kind !== NodeKind.TOOL_CALL && kind !== NodeKind.TOOL_RESULT) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const leading = LEADING_IDENTIFIER.exec(summary);
|
|
338
|
+
if (leading) {
|
|
339
|
+
return [leading[1]];
|
|
340
|
+
}
|
|
341
|
+
const ref = TOOL_REF.exec(summary);
|
|
342
|
+
if (ref) {
|
|
343
|
+
return [ref[1]];
|
|
344
|
+
}
|
|
345
|
+
if (BARE_IDENTIFIER.test(summary)) {
|
|
346
|
+
return [summary];
|
|
347
|
+
}
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
function truncateCodePoints(s, n) {
|
|
351
|
+
if (n <= 0) {
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
const points = Array.from(s);
|
|
355
|
+
if (points.length <= n) {
|
|
356
|
+
return s;
|
|
357
|
+
}
|
|
358
|
+
return points.slice(0, n).join("");
|
|
359
|
+
}
|
|
360
|
+
function nodeId(kind, parentId, summary) {
|
|
361
|
+
return createHash("sha256")
|
|
362
|
+
.update(`${kind}\0${parentId}\0${summary}`)
|
|
363
|
+
.digest("hex")
|
|
364
|
+
.slice(0, 16);
|
|
365
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare class ReasoningError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class MalformedInputError extends ReasoningError {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class MalformedJSONError extends ReasoningError {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class MalformedReasoningFieldError extends ReasoningError {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class MalformedContentError extends ReasoningError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class UnbalancedThinkingError extends ReasoningError {
|
|
17
|
+
constructor(message?: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class ReasoningTooLargeError extends ReasoningError {
|
|
20
|
+
constructor(message?: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class TooManyNodesError extends ReasoningError {
|
|
23
|
+
constructor(message?: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class TooDeepError extends ReasoningError {
|
|
26
|
+
constructor(message?: string);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/reasoning/errors.ts"],"names":[],"mappings":"AAIA,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,mBAAoB,SAAQ,cAAc;gBACzC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,kBAAmB,SAAQ,cAAc;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,4BAA6B,SAAQ,cAAc;gBAClD,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,cAAc;gBAC3C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,uBAAwB,SAAQ,cAAc;gBAC7C,OAAO,SAAuC;CAI3D;AAED,qBAAa,sBAAuB,SAAQ,cAAc;gBAC5C,OAAO,SAA8C;CAIlE;AAED,qBAAa,iBAAkB,SAAQ,cAAc;gBACvC,OAAO,SAAwC;CAI5D;AAED,qBAAa,YAAa,SAAQ,cAAc;gBAClC,OAAO,SAAuC;CAI3D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Fail-closed errors raised by the reasoning adapter.
|
|
2
|
+
// Every error represents a fail-closed outcome: malformed or unbounded input
|
|
3
|
+
// never produces a partial tree or partially-stripped content.
|
|
4
|
+
export class ReasoningError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ReasoningError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class MalformedInputError extends ReasoningError {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "MalformedInputError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class MalformedJSONError extends ReasoningError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "MalformedJSONError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class MalformedReasoningFieldError extends ReasoningError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "MalformedReasoningFieldError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class MalformedContentError extends ReasoningError {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "MalformedContentError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class UnbalancedThinkingError extends ReasoningError {
|
|
35
|
+
constructor(message = "reasoning: unbalanced thinking tag") {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "UnbalancedThinkingError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class ReasoningTooLargeError extends ReasoningError {
|
|
41
|
+
constructor(message = "reasoning: reasoning exceeds maximum size") {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ReasoningTooLargeError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class TooManyNodesError extends ReasoningError {
|
|
47
|
+
constructor(message = "reasoning: too many reasoning nodes") {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "TooManyNodesError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class TooDeepError extends ReasoningError {
|
|
53
|
+
constructor(message = "reasoning: reasoning tree too deep") {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "TooDeepError";
|
|
56
|
+
}
|
|
57
|
+
}
|