@haikeilabs/agentware 0.1.1 → 0.2.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/README.md CHANGED
@@ -43,3 +43,22 @@ Register only the wrapped handler with an agent framework or tool registry.
43
43
  Keep channel ingress, identity verification, delegated credentials, and
44
44
  domain-specific authorization in the host application. Agentware is not a
45
45
  policy engine; a policy gate can wrap this same boundary later.
46
+
47
+ ## Maintainer releases
48
+
49
+ Releases are deliberately manual and use npm staged publishing. Do not run
50
+ `npm publish` from a workstation or add a registry token to CI.
51
+
52
+ 1. Update `typescript/package.json` and `typescript/package-lock.json` to the
53
+ intended semantic version in a pull request, then merge it to `main`.
54
+ 2. In GitHub Actions, run **Stage Agentware npm package** from `main`, entering
55
+ that exact version (for example, `0.1.1`). The workflow verifies the version,
56
+ creates the immutable `agentware-v<version>` tag, runs checks, and submits
57
+ the package with `npm stage publish`.
58
+ 3. Approve the GitHub `release` environment when prompted.
59
+ 4. Review the staged package in npm and approve it. The approving maintainer
60
+ needs npm publish access and 2FA enabled for write actions; a WebAuthn
61
+ security key is supported.
62
+
63
+ The final npm approval is intentional: it is the human gate that makes the
64
+ staged version publicly available.
@@ -3,6 +3,7 @@ import type { ToolRegistry } from "../tools/index.js";
3
3
  import type { ResponseValidator } from "../middleware/guardrails/response_validator.js";
4
4
  import { ErrorTracker, ErrorCategory } from "../middleware/guardrails/error_tracker.js";
5
5
  import { StepEnforcer } from "../middleware/guardrails/step_enforcer.js";
6
+ import { ReasoningAdapter, type ContextTree } from "../reasoning/index.js";
6
7
  export declare enum AgentTerminationReason {
7
8
  COMPLETE = "complete",
8
9
  MAX_ITERATIONS = "max_iterations",
@@ -18,6 +19,9 @@ export interface AgentLoopConfig {
18
19
  max_iterations?: number;
19
20
  max_nudges?: number;
20
21
  require_tool_call?: boolean;
22
+ /** Optional reasoning adapter override for limits and registered model
23
+ * fields. */
24
+ reasoning?: ReasoningAdapter;
21
25
  }
22
26
  export interface AgentResult {
23
27
  final_response: string;
@@ -26,6 +30,11 @@ export interface AgentResult {
26
30
  nudges: number;
27
31
  termination_reason: AgentTerminationReason;
28
32
  conversation: Message[];
33
+ /** Normalized context tree for the last turn that carried reasoning
34
+ * (native reasoning_content and/or thinking tags). Null when no turn
35
+ * carried reasoning. Holds bounded summaries only; raw reasoning is never
36
+ * attached. */
37
+ reasoning_tree: ContextTree | null;
29
38
  }
30
39
  export declare function registryToolDefinitions(registry: ToolRegistry): ToolDefinition[];
31
40
  export declare function categorizeError(message: string): ErrorCategory;
@@ -1 +1 @@
1
- {"version":3,"file":"agent_loop.d.ts","sourceRoot":"","sources":["../../src/executor/agent_loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,gDAAgD,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACd,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAQzE,oBAAY,sBAAsB;IAChC,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,gBAAgB,qBAAqB;IACrC,KAAK,UAAU;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,sBAAsB,CAAC;IAC3C,YAAY,EAAE,OAAO,EAAE,CAAC;CACzB;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,YAAY,GACrB,cAAc,EAAE,CASlB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CA0B9D;AAiBD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,eAAe;IAU7B,GAAG,CACP,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAO,EAAO,EACvB,UAAU,GAAE,MAAW,GACtB,OAAO,CAAC,WAAW,CAAC;IAsLvB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,MAAM;CAiBf"}
1
+ {"version":3,"file":"agent_loop.d.ts","sourceRoot":"","sources":["../../src/executor/agent_loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,gDAAgD,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACd,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAOzE,OAAO,EACL,gBAAgB,EAGhB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,oBAAY,sBAAsB;IAChC,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,gBAAgB,qBAAqB;IACrC,KAAK,UAAU;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;iBACa;IACb,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,sBAAsB,CAAC;IAC3C,YAAY,EAAE,OAAO,EAAE,CAAC;IACxB;;;mBAGe;IACf,cAAc,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAKD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,YAAY,GACrB,cAAc,EAAE,CASlB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CA0B9D;AAiBD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,eAAe;IAU7B,GAAG,CACP,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAO,EAAO,EACvB,UAAU,GAAE,MAAW,GACtB,OAAO,CAAC,WAAW,CAAC;IA2OvB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,MAAM;CAmBf"}
@@ -3,6 +3,7 @@ import { executeTool, Result } from "../tools/index.js";
3
3
  import { ErrorCategory, } from "../middleware/guardrails/error_tracker.js";
4
4
  import { NudgeKind, stepNudge, } from "../middleware/guardrails/nudge.js";
5
5
  import { MessageType } from "../middleware/types.js";
6
+ import { ReasoningAdapter, ReasoningError, isContextTreeEmpty, } from "../reasoning/index.js";
6
7
  export var AgentTerminationReason;
7
8
  (function (AgentTerminationReason) {
8
9
  AgentTerminationReason["COMPLETE"] = "complete";
@@ -10,6 +11,8 @@ export var AgentTerminationReason;
10
11
  AgentTerminationReason["NUDGES_EXHAUSTED"] = "nudges_exhausted";
11
12
  AgentTerminationReason["ERROR"] = "error";
12
13
  })(AgentTerminationReason || (AgentTerminationReason = {}));
14
+ // Default adapter strips and normalizes reasoning in the agent loop.
15
+ const defaultAdapter = new ReasoningAdapter();
13
16
  export function registryToolDefinitions(registry) {
14
17
  return registry.all().map((tool) => ({
15
18
  name: tool.name,
@@ -69,14 +72,19 @@ export class AgentLoop {
69
72
  config.max_nudges && config.max_nudges > 0 ? config.max_nudges : 3;
70
73
  }
71
74
  async run(system_prompt, user_message, history = [], session_id = "") {
72
- const conversation = [...history];
73
- conversation.push({ role: Role.SYSTEM, content: system_prompt });
75
+ // Cross-language contract: the system prompt leads the conversation,
76
+ // ahead of any caller-supplied history (mirrors Go buildConversation).
77
+ const conversation = [
78
+ { role: Role.SYSTEM, content: system_prompt },
79
+ ...history,
80
+ ];
74
81
  conversation.push({ role: Role.USER, content: user_message });
75
82
  const toolDefs = registryToolDefinitions(this.config.registry);
76
83
  let iterations = 0;
77
84
  let toolCallsMade = 0;
78
85
  let nudges = 0;
79
86
  let finalResponse = "";
87
+ let reasoningTree = null;
80
88
  while (iterations < this.maxIterations) {
81
89
  iterations++;
82
90
  let resp;
@@ -85,17 +93,57 @@ export class AgentLoop {
85
93
  }
86
94
  catch (err) {
87
95
  this.config.error_tracker?.recordError(session_id, "", {}, toError(err), ErrorCategory.UNKNOWN);
88
- return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.ERROR, conversation);
96
+ return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.ERROR, conversation, reasoningTree);
89
97
  }
90
- const validation = this.validate(resp);
98
+ // AR-1 parity: normalize reasoning and strip it before validation.
99
+ // Reasoning can arrive as a native reasoning_content field or as
100
+ // embedded thinking tags in content; both are combined into a synthetic
101
+ // structured input so the adapter sees the full picture. Malformed or
102
+ // unbounded reasoning fails closed: this turn is treated as invalid and
103
+ // retried rather than parsed partially.
104
+ let turnTree = null;
105
+ let cleanContent = resp.content;
106
+ let reasoningFailed = false;
107
+ if (resp.content || resp.reasoning) {
108
+ const reasoningInput = {
109
+ content: resp.content,
110
+ };
111
+ if (resp.reasoning) {
112
+ reasoningInput["reasoning_content"] = resp.reasoning;
113
+ }
114
+ const adapter = this.config.reasoning ?? defaultAdapter;
115
+ try {
116
+ turnTree = adapter.extract(reasoningInput, this.config.backend.modelName(), "agent-loop");
117
+ cleanContent = adapter.strip(reasoningInput);
118
+ }
119
+ catch (e) {
120
+ if (e instanceof ReasoningError) {
121
+ reasoningFailed = true;
122
+ // Fail closed: never echo content that may carry reasoning we
123
+ // could not parse.
124
+ cleanContent = "";
125
+ }
126
+ else {
127
+ throw e;
128
+ }
129
+ }
130
+ }
131
+ // Keep the last turn that actually carried reasoning; a later plain
132
+ // turn must not blank out the tree the consumer is after.
133
+ if (!isContextTreeEmpty(turnTree)) {
134
+ reasoningTree = turnTree;
135
+ }
136
+ const validation = reasoningFailed
137
+ ? { toolCalls: [], nudge: null, needsRetry: true }
138
+ : this.validate(resp, cleanContent);
91
139
  if (validation.needsRetry) {
92
140
  if (nudges >= this.maxNudges) {
93
- return this.finish(resp.content, iterations, toolCallsMade, nudges, AgentTerminationReason.NUDGES_EXHAUSTED, conversation);
141
+ return this.finish(cleanContent, iterations, toolCallsMade, nudges, AgentTerminationReason.NUDGES_EXHAUSTED, conversation, reasoningTree);
94
142
  }
95
143
  nudges++;
96
144
  conversation.push({
97
145
  role: Role.ASSISTANT,
98
- content: resp.content,
146
+ content: cleanContent,
99
147
  meta: { type: MessageType.TEXT_RESPONSE },
100
148
  });
101
149
  if (validation.nudge) {
@@ -108,8 +156,8 @@ export class AgentLoop {
108
156
  continue;
109
157
  }
110
158
  if (validation.toolCalls.length === 0) {
111
- finalResponse = resp.content;
112
- return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.COMPLETE, conversation);
159
+ finalResponse = cleanContent;
160
+ return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.COMPLETE, conversation, reasoningTree);
113
161
  }
114
162
  let stepNudged = false;
115
163
  for (const call of validation.toolCalls) {
@@ -117,7 +165,7 @@ export class AgentLoop {
117
165
  const [allowed, missing] = this.config.step_enforcer.canExecute(session_id, call.tool);
118
166
  if (!allowed) {
119
167
  if (nudges >= this.maxNudges) {
120
- return this.finish(resp.content, iterations, toolCallsMade, nudges, AgentTerminationReason.NUDGES_EXHAUSTED, conversation);
168
+ return this.finish(cleanContent, iterations, toolCallsMade, nudges, AgentTerminationReason.NUDGES_EXHAUSTED, conversation, reasoningTree);
121
169
  }
122
170
  nudges++;
123
171
  stepNudged = true;
@@ -178,13 +226,13 @@ export class AgentLoop {
178
226
  continue;
179
227
  }
180
228
  }
181
- return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.MAX_ITERATIONS, conversation);
229
+ return this.finish(finalResponse, iterations, toolCallsMade, nudges, AgentTerminationReason.MAX_ITERATIONS, conversation, reasoningTree);
182
230
  }
183
- validate(resp) {
231
+ validate(resp, cleanContent) {
184
232
  if (resp.tool_calls.length > 0) {
185
233
  return this.config.validator.validateToolCalls(resp.tool_calls.map((tc) => ({ tool: tc.name, args: tc.arguments })));
186
234
  }
187
- const textValidation = this.config.validator.validateTextResponse(resp.content);
235
+ const textValidation = this.config.validator.validateTextResponse(cleanContent);
188
236
  if (textValidation.toolCalls.length > 0) {
189
237
  return textValidation;
190
238
  }
@@ -193,7 +241,7 @@ export class AgentLoop {
193
241
  }
194
242
  return { toolCalls: [], nudge: null, needsRetry: false };
195
243
  }
196
- finish(finalResponse, iterations, toolCallsMade, nudges, termination, conversation) {
244
+ finish(finalResponse, iterations, toolCallsMade, nudges, termination, conversation, reasoningTree) {
197
245
  return {
198
246
  final_response: finalResponse,
199
247
  iterations,
@@ -201,6 +249,7 @@ export class AgentLoop {
201
249
  nudges,
202
250
  termination_reason: termination,
203
251
  conversation,
252
+ reasoning_tree: reasoningTree,
204
253
  };
205
254
  }
206
255
  }
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/executor/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAIxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,oBAAY,iBAAiB;IAC3B,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,KAAK,UAAU;IACf,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,YAAY,EAAE,OAAO,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,aAAa,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,aAAa,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,OAAO,CAAC,MAAM,CAA0B;gBAE5B,MAAM,EAAE,uBAAuB;IAQ3C,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,aAAa;CA2D5C"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/executor/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAIxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,oBAAY,iBAAiB;IAC3B,QAAQ,aAAa;IACrB,cAAc,mBAAmB;IACjC,KAAK,UAAU;IACf,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,YAAY,EAAE,OAAO,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,aAAa,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,aAAa,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,OAAO,CAAC,MAAM,CAA0B;gBAE5B,MAAM,EAAE,uBAAuB;IAQ3C,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,aAAa;CA+D5C"}
@@ -16,8 +16,12 @@ export class InferenceExecutor {
16
16
  };
17
17
  }
18
18
  execute(req) {
19
- const conversation = [...req.history];
20
- conversation.push({ role: Role.SYSTEM, content: req.system_prompt });
19
+ // Cross-language contract: the system prompt leads the conversation,
20
+ // ahead of any caller-supplied history (mirrors Go buildConversation).
21
+ const conversation = [
22
+ { role: Role.SYSTEM, content: req.system_prompt },
23
+ ...req.history,
24
+ ];
21
25
  conversation.push({ role: Role.USER, content: req.user_message });
22
26
  let iterations = 0;
23
27
  let tool_calls_made = 0;
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export * from "./prompts/index.js";
8
8
  export * from "./toolformat/index.js";
9
9
  export * from "./memory/index.js";
10
10
  export * from "./reasoning/index.js";
11
+ export * from "./kei/index.js";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export * from "./prompts/index.js";
8
8
  export * from "./toolformat/index.js";
9
9
  export * from "./memory/index.js";
10
10
  export * from "./reasoning/index.js";
11
+ export * from "./kei/index.js";
@@ -0,0 +1,2 @@
1
+ export * from "./runtimeLink.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/kei/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from "./runtimeLink.js";
@@ -0,0 +1,281 @@
1
+ /**
2
+ * RuntimeLink contract: the harness → Agentware → kei-connector-runtime heartbeat.
3
+ *
4
+ * TypeScript mirror of the Go reference `go/kei/runtimelink` for
5
+ * `docs/specs/runtime-heartbeat-liveness.md` (HAI-141). Contract types only:
6
+ * configuration, status, identity, failure classes, the child's JSONL wire
7
+ * events, and redacted lifecycle events. It never spawns a process, opens a
8
+ * network connection, or reads a credential value. The only heartbeat path is
9
+ * harness → RuntimeLink → kei-connector-runtime; nothing here talks to the
10
+ * catalog.
11
+ *
12
+ * All three SDK languages share `testing/contracts/runtime-link`. Durations
13
+ * are milliseconds here (`intervalMs`), seconds in Python, and
14
+ * `time.Duration` in Go.
15
+ */
16
+ /** The child stdout event version ("v") this SDK accepts. */
17
+ export declare const RUNTIME_LINK_CONTRACT_VERSION = 1;
18
+ /** Bound on one child stdout line, excluding the newline. */
19
+ export declare const MAX_CHILD_LINE_BYTES = 4096;
20
+ /** Capacity of the RuntimeLink event ring. */
21
+ export declare const RUNTIME_LINK_EVENT_BUFFER_SIZE = 64;
22
+ export declare const RUNTIME_LINK_SDK_LANG = "typescript";
23
+ export declare const HARNESS_KINDS: readonly ["assistant", "pde", "chat-discord", "chat-slack", "chat-teams", "cli"];
24
+ export type HarnessKind = (typeof HARNESS_KINDS)[number];
25
+ export declare const LINK_STATES: readonly ["disabled", "starting", "connected", "degraded", "reconnecting", "terminal", "stopped"];
26
+ export type LinkState = (typeof LINK_STATES)[number];
27
+ /**
28
+ * Closed failure taxonomy (spec §6.3). Separates harness↔runtime failures
29
+ * from runtime↔catalog failures so alerts route to the right owner.
30
+ */
31
+ export declare const FAILURE_CLASSES: readonly ["config_invalid", "runtime_unavailable", "runtime_crashloop", "runtime_unresponsive", "contract_mismatch", "catalog_unreachable", "catalog_timeout", "catalog_error", "catalog_backpressure", "installation_unauthorized", "installation_stale", "installation_offline", "audit_backlog", "legacy_runtime_unverified"];
32
+ export type FailureClass = (typeof FAILURE_CLASSES)[number];
33
+ export declare const BEAT_OUTCOMES: readonly ["ok", "catalog_unreachable", "catalog_timeout", "catalog_error", "catalog_backpressure", "catalog_rejected", "unauthorized"];
34
+ export type BeatOutcome = (typeof BEAT_OUTCOMES)[number];
35
+ export declare const LIFECYCLE_EVENT_NAMES: readonly ["runtime.link.started", "runtime.link.connected", "runtime.link.degraded", "runtime.link.reconnecting", "runtime.link.terminal", "runtime.link.stopped"];
36
+ export type LifecycleEventName = (typeof LIFECYCLE_EVENT_NAMES)[number];
37
+ export declare const SDK_LANGS: readonly ["go", "python", "typescript"];
38
+ export type SdkLang = (typeof SDK_LANGS)[number];
39
+ /** The exact environment a runtime child may receive; never `process.env`. */
40
+ export declare const CHILD_ENV_ALLOWLIST: readonly ["KEI_RUNTIME_TOKEN", "KEI_RUNTIME_CONTROL_PLANE_URL", "KEI_HARNESS_KIND", "KEI_HARNESS_VERSION", "KEI_DEPLOYMENT_ENV", "KEI_AGENTWARE_SDK_LANG", "KEI_AGENTWARE_SDK_VERSION", "KEI_HEARTBEAT_RUN_ID", "PATH", "HOME", "TZ"];
41
+ /** Environment variables. KEI_PROXY_* keep their legacy names (runtime contract). */
42
+ export declare const RUNTIME_LINK_ENV: {
43
+ readonly enabled: "KEI_RUNTIME_ENABLED";
44
+ readonly token: "KEI_RUNTIME_TOKEN";
45
+ readonly legacyToken: "KEI_HARNESS_TOKEN";
46
+ readonly controlPlaneUrl: "KEI_RUNTIME_CONTROL_PLANE_URL";
47
+ readonly proxyPath: "KEI_PROXY_PATH";
48
+ readonly proxySha: "KEI_PROXY_SHA";
49
+ readonly interval: "KEI_HEARTBEAT_INTERVAL";
50
+ readonly timeout: "KEI_HEARTBEAT_TIMEOUT";
51
+ readonly restartMin: "KEI_HEARTBEAT_RESTART_MIN";
52
+ readonly restartMax: "KEI_HEARTBEAT_RESTART_MAX";
53
+ readonly stableSeconds: "KEI_HEARTBEAT_STABLE_SECONDS";
54
+ readonly logCount: "KEI_HEARTBEAT_LOG_COUNT";
55
+ readonly harnessKind: "KEI_HARNESS_KIND";
56
+ readonly harnessVersion: "KEI_HARNESS_VERSION";
57
+ readonly deploymentEnv: "KEI_DEPLOYMENT_ENV";
58
+ };
59
+ /** The distribution keeps the kei-proxy name; the repo is kei-connector-runtime. */
60
+ export declare const DEFAULT_RUNTIME_BINARY_PATH = "kei-proxy";
61
+ /**
62
+ * Bounds. Interval is clamped; everything else is rejected when out of range
63
+ * so a typo fails closed instead of silently changing behavior.
64
+ */
65
+ export declare const RUNTIME_LINK_BOUNDS: {
66
+ readonly minIntervalMs: 15000;
67
+ readonly maxIntervalMs: 300000;
68
+ readonly maxGraceMs: 300000;
69
+ readonly minRestartMs: 1000;
70
+ readonly maxRestartMs: 3600000;
71
+ readonly minStableResetMs: 1000;
72
+ readonly maxStableResetMs: 3600000;
73
+ readonly minStopTimeoutMs: 1000;
74
+ readonly maxStopTimeoutMs: 60000;
75
+ readonly maxLogCount: 100;
76
+ };
77
+ export declare const RUNTIME_LINK_CONFIG_ERROR_CODES: readonly ["invalid_value", "out_of_range", "beat_timeout", "harness_kind", "envelope", "legacy_token", "token_missing", "control_plane_url", "binary"];
78
+ export type RuntimeLinkConfigErrorCode = (typeof RUNTIME_LINK_CONFIG_ERROR_CODES)[number];
79
+ /**
80
+ * Invalid RuntimeLink configuration. Carries the offending field and a stable
81
+ * code, never the offending value, so a misplaced secret cannot leak.
82
+ */
83
+ export declare class RuntimeLinkConfigError extends Error {
84
+ readonly code: RuntimeLinkConfigErrorCode;
85
+ readonly field: string;
86
+ constructor(code: RuntimeLinkConfigErrorCode, field: string);
87
+ }
88
+ /** The runtime binary and its expected SHA-256 (lowercase hex). */
89
+ export interface BinaryRef {
90
+ path: string;
91
+ sha256: string;
92
+ }
93
+ /** Where the runtime token lives: the env var name only, never the value. */
94
+ export interface SecretSource {
95
+ env: string;
96
+ }
97
+ /** Child restart backoff with full jitter. */
98
+ export interface Backoff {
99
+ minMs: number;
100
+ maxMs: number;
101
+ stableResetMs: number;
102
+ }
103
+ /** Declared, non-authoritative harness metadata. Never used for authorization. */
104
+ export interface HarnessEnvelope {
105
+ kind: string;
106
+ version: string;
107
+ deploymentEnv: string;
108
+ }
109
+ export interface RuntimeLinkConfig {
110
+ enabled: boolean;
111
+ binary: BinaryRef;
112
+ controlPlaneUrl: string;
113
+ token: SecretSource;
114
+ intervalMs: number;
115
+ beatTimeoutMs: number;
116
+ graceMs: number;
117
+ restart: Backoff;
118
+ stopTimeoutMs: number;
119
+ logCount: number;
120
+ harness: HarnessEnvelope;
121
+ }
122
+ /** The spec defaults with the link disabled. */
123
+ export declare function defaultRuntimeLinkConfig(): RuntimeLinkConfig;
124
+ /**
125
+ * Clamp `intervalMs` to [15s, 300s] and validate every other field, throwing
126
+ * RuntimeLinkConfigError on the first violation. A disabled link (local-only
127
+ * mode) is valid, but its declared envelope and timings are still checked so
128
+ * typos fail loudly.
129
+ */
130
+ export declare function normalizeRuntimeLinkConfig(cfg: RuntimeLinkConfig): RuntimeLinkConfig;
131
+ export type EnvSource = Readonly<Record<string, string | undefined>>;
132
+ /**
133
+ * Build a normalized config from the spec §3.2 variables. Non-empty fields of
134
+ * `harness` override the KEI_HARNESS_* variables. The runtime token is only
135
+ * checked for presence; its value is never retained.
136
+ */
137
+ export declare function runtimeLinkConfigFromEnv(env?: EnvSource, harness?: Partial<HarnessEnvelope>): RuntimeLinkConfig;
138
+ /**
139
+ * Full-jitter restart delay for attempt n (0-based):
140
+ * `floor(r × min(maxMs, minMs × 2^n))`. `r` is clamped to [0, 1); negative
141
+ * attempts count as 0.
142
+ */
143
+ export declare function backoffDelayMs(backoff: Pick<Backoff, "minMs" | "maxMs">, attempt: number, r: number): number;
144
+ export interface WaitBackoffOptions {
145
+ signal?: AbortSignal;
146
+ random?: () => number;
147
+ }
148
+ /**
149
+ * Sleep for the jittered delay, or reject with the signal's abort reason as
150
+ * soon as `signal` aborts, so a stop during backoff never waits out the delay.
151
+ */
152
+ export declare function waitBackoff(backoff: Pick<Backoff, "minMs" | "maxMs">, attempt: number, options?: WaitBackoffOptions): Promise<void>;
153
+ /** Point-in-time snapshot. Diagnostic only: readiness must never depend on it. */
154
+ export interface LinkStatus {
155
+ state: LinkState;
156
+ /** Absent when connected. */
157
+ reason?: FailureClass;
158
+ lastBeatAt?: Date;
159
+ lastCatalogOkAt?: Date;
160
+ consecutiveFails: number;
161
+ runId: string;
162
+ restarts: number;
163
+ }
164
+ /** Authoritative installation identity from catalog whoami, via the runtime. */
165
+ export interface RuntimeIdentity {
166
+ runId: string;
167
+ installationId: string;
168
+ orgId: string;
169
+ workspaceId: string;
170
+ platform: string;
171
+ status: string;
172
+ bindingStatus: string;
173
+ runtimeVersion: string;
174
+ }
175
+ export type ChildLineErrorKind = "line_too_long" | "malformed" | "contract_mismatch";
176
+ /** A child line the SDK must drop and count. */
177
+ export declare class ChildLineError extends Error {
178
+ readonly kind: ChildLineErrorKind;
179
+ constructor(kind: ChildLineErrorKind, detail: string);
180
+ }
181
+ export interface BeatEvent {
182
+ runId: string;
183
+ /** 0 when absent: the runtime omits seq on beats sent before identity. */
184
+ seq: number;
185
+ at: string;
186
+ outcome: BeatOutcome;
187
+ httpStatus: number;
188
+ latencyMs: number;
189
+ nextInMs: number;
190
+ }
191
+ export interface TerminalEvent {
192
+ runId: string;
193
+ reason: string;
194
+ }
195
+ export type ChildEvent = {
196
+ kind: "identity";
197
+ identity: RuntimeIdentity;
198
+ } | {
199
+ kind: "beat";
200
+ beat: BeatEvent;
201
+ } | {
202
+ kind: "terminal";
203
+ terminal: TerminalEvent;
204
+ } | {
205
+ kind: "ignored";
206
+ };
207
+ /**
208
+ * Parse one line of child stdout (a trailing "\n" / "\r\n" is ignored). Only
209
+ * allowlisted fields survive; payloads, results, reasoning, tokens, or any
210
+ * other key the runtime might emit are dropped. Throws ChildLineError for
211
+ * lines the SDK must drop and count.
212
+ */
213
+ export declare function parseChildLine(line: string | Uint8Array): ChildEvent;
214
+ /** A lifecycle event has a value outside its closed set or bounds. */
215
+ export declare class InvalidLinkEventError extends Error {
216
+ constructor(detail: string);
217
+ }
218
+ export interface EventHarness {
219
+ kind: HarnessKind;
220
+ version: string;
221
+ deploymentEnv: string;
222
+ sdkLang: SdkLang;
223
+ sdkVersion: string;
224
+ }
225
+ /**
226
+ * A redacted lifecycle record. Redacted by construction: the type has no
227
+ * field that could carry a token, argv, env, child stderr, provider payload
228
+ * or result, reasoning, customer content, or a subject.
229
+ */
230
+ export interface LinkEvent {
231
+ name: LifecycleEventName;
232
+ at: Date;
233
+ state: LinkState;
234
+ /** Empty when there is no failure. */
235
+ reason: FailureClass | "";
236
+ runId: string;
237
+ seq: number;
238
+ sdkInstanceId: string;
239
+ harness: EventHarness;
240
+ restarts: number;
241
+ consecutiveFails: number;
242
+ }
243
+ /** The serialized form: exactly these keys, in this order. */
244
+ export interface LinkEventWire {
245
+ name: LifecycleEventName;
246
+ at: string;
247
+ state: LinkState;
248
+ reason: FailureClass | "";
249
+ run_id: string;
250
+ seq: number;
251
+ sdk_instance_id: string;
252
+ harness: {
253
+ kind: HarnessKind;
254
+ version: string;
255
+ deployment_env: string;
256
+ sdk_lang: SdkLang;
257
+ sdk_version: string;
258
+ };
259
+ restarts: number;
260
+ consecutive_fails: number;
261
+ }
262
+ /** Throws InvalidLinkEventError unless every closed set and bound holds. */
263
+ export declare function validateLinkEvent(ev: LinkEvent): LinkEvent;
264
+ /** Build an event from allowlisted wire keys only; everything else is dropped. */
265
+ export declare function linkEventFromWire(raw: unknown): LinkEvent;
266
+ /** Validate and emit exactly the allowlisted keys; `at` is UTC with millisecond precision. */
267
+ export declare function linkEventToWire(ev: LinkEvent): LinkEventWire;
268
+ /**
269
+ * Supervises the harness→runtime heartbeat. `start` returns promptly and
270
+ * rejects only for invalid config, never for network or runtime state.
271
+ * Aborting the signal passed to `start`, or calling `stop`, ends the link;
272
+ * `stop` is idempotent and bounded by `stopTimeoutMs`.
273
+ */
274
+ export interface RuntimeLink {
275
+ start(signal?: AbortSignal): Promise<void>;
276
+ stop(): Promise<void>;
277
+ status(): LinkStatus;
278
+ identity(): RuntimeIdentity | undefined;
279
+ events(): AsyncIterable<LinkEvent>;
280
+ }
281
+ //# sourceMappingURL=runtimeLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtimeLink.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,591 @@
1
+ /**
2
+ * RuntimeLink contract: the harness → Agentware → kei-connector-runtime heartbeat.
3
+ *
4
+ * TypeScript mirror of the Go reference `go/kei/runtimelink` for
5
+ * `docs/specs/runtime-heartbeat-liveness.md` (HAI-141). Contract types only:
6
+ * configuration, status, identity, failure classes, the child's JSONL wire
7
+ * events, and redacted lifecycle events. It never spawns a process, opens a
8
+ * network connection, or reads a credential value. The only heartbeat path is
9
+ * harness → RuntimeLink → kei-connector-runtime; nothing here talks to the
10
+ * catalog.
11
+ *
12
+ * All three SDK languages share `testing/contracts/runtime-link`. Durations
13
+ * are milliseconds here (`intervalMs`), seconds in Python, and
14
+ * `time.Duration` in Go.
15
+ */
16
+ /** The child stdout event version ("v") this SDK accepts. */
17
+ export const RUNTIME_LINK_CONTRACT_VERSION = 1;
18
+ /** Bound on one child stdout line, excluding the newline. */
19
+ export const MAX_CHILD_LINE_BYTES = 4096;
20
+ /** Capacity of the RuntimeLink event ring. */
21
+ export const RUNTIME_LINK_EVENT_BUFFER_SIZE = 64;
22
+ export const RUNTIME_LINK_SDK_LANG = "typescript";
23
+ export const HARNESS_KINDS = [
24
+ "assistant",
25
+ "pde",
26
+ "chat-discord",
27
+ "chat-slack",
28
+ "chat-teams",
29
+ "cli",
30
+ ];
31
+ export const LINK_STATES = [
32
+ "disabled",
33
+ "starting",
34
+ "connected",
35
+ "degraded",
36
+ "reconnecting",
37
+ "terminal",
38
+ "stopped",
39
+ ];
40
+ /**
41
+ * Closed failure taxonomy (spec §6.3). Separates harness↔runtime failures
42
+ * from runtime↔catalog failures so alerts route to the right owner.
43
+ */
44
+ export const FAILURE_CLASSES = [
45
+ "config_invalid",
46
+ "runtime_unavailable",
47
+ "runtime_crashloop",
48
+ "runtime_unresponsive",
49
+ "contract_mismatch",
50
+ "catalog_unreachable",
51
+ "catalog_timeout",
52
+ "catalog_error",
53
+ "catalog_backpressure",
54
+ "installation_unauthorized",
55
+ "installation_stale",
56
+ "installation_offline",
57
+ "audit_backlog",
58
+ "legacy_runtime_unverified",
59
+ ];
60
+ export const BEAT_OUTCOMES = [
61
+ "ok",
62
+ "catalog_unreachable",
63
+ "catalog_timeout",
64
+ "catalog_error",
65
+ "catalog_backpressure",
66
+ "catalog_rejected",
67
+ "unauthorized",
68
+ ];
69
+ export const LIFECYCLE_EVENT_NAMES = [
70
+ "runtime.link.started",
71
+ "runtime.link.connected",
72
+ "runtime.link.degraded",
73
+ "runtime.link.reconnecting",
74
+ "runtime.link.terminal",
75
+ "runtime.link.stopped",
76
+ ];
77
+ export const SDK_LANGS = ["go", "python", "typescript"];
78
+ /** The exact environment a runtime child may receive; never `process.env`. */
79
+ export const CHILD_ENV_ALLOWLIST = [
80
+ "KEI_RUNTIME_TOKEN",
81
+ "KEI_RUNTIME_CONTROL_PLANE_URL",
82
+ "KEI_HARNESS_KIND",
83
+ "KEI_HARNESS_VERSION",
84
+ "KEI_DEPLOYMENT_ENV",
85
+ "KEI_AGENTWARE_SDK_LANG",
86
+ "KEI_AGENTWARE_SDK_VERSION",
87
+ "KEI_HEARTBEAT_RUN_ID",
88
+ "PATH",
89
+ "HOME",
90
+ "TZ",
91
+ ];
92
+ function isOneOf(set, value) {
93
+ return (typeof value === "string" && set.includes(value));
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Configuration (spec §3.1, §3.2)
97
+ // ---------------------------------------------------------------------------
98
+ /** Environment variables. KEI_PROXY_* keep their legacy names (runtime contract). */
99
+ export const RUNTIME_LINK_ENV = {
100
+ enabled: "KEI_RUNTIME_ENABLED",
101
+ token: "KEI_RUNTIME_TOKEN",
102
+ legacyToken: "KEI_HARNESS_TOKEN",
103
+ controlPlaneUrl: "KEI_RUNTIME_CONTROL_PLANE_URL",
104
+ proxyPath: "KEI_PROXY_PATH",
105
+ proxySha: "KEI_PROXY_SHA",
106
+ interval: "KEI_HEARTBEAT_INTERVAL",
107
+ timeout: "KEI_HEARTBEAT_TIMEOUT",
108
+ restartMin: "KEI_HEARTBEAT_RESTART_MIN",
109
+ restartMax: "KEI_HEARTBEAT_RESTART_MAX",
110
+ stableSeconds: "KEI_HEARTBEAT_STABLE_SECONDS",
111
+ logCount: "KEI_HEARTBEAT_LOG_COUNT",
112
+ harnessKind: "KEI_HARNESS_KIND",
113
+ harnessVersion: "KEI_HARNESS_VERSION",
114
+ deploymentEnv: "KEI_DEPLOYMENT_ENV",
115
+ };
116
+ /** The distribution keeps the kei-proxy name; the repo is kei-connector-runtime. */
117
+ export const DEFAULT_RUNTIME_BINARY_PATH = "kei-proxy";
118
+ /**
119
+ * Bounds. Interval is clamped; everything else is rejected when out of range
120
+ * so a typo fails closed instead of silently changing behavior.
121
+ */
122
+ export const RUNTIME_LINK_BOUNDS = {
123
+ minIntervalMs: 15_000,
124
+ maxIntervalMs: 300_000,
125
+ maxGraceMs: 300_000,
126
+ minRestartMs: 1_000,
127
+ maxRestartMs: 3_600_000,
128
+ minStableResetMs: 1_000,
129
+ maxStableResetMs: 3_600_000,
130
+ minStopTimeoutMs: 1_000,
131
+ maxStopTimeoutMs: 60_000,
132
+ maxLogCount: 100,
133
+ };
134
+ export const RUNTIME_LINK_CONFIG_ERROR_CODES = [
135
+ "invalid_value",
136
+ "out_of_range",
137
+ "beat_timeout",
138
+ "harness_kind",
139
+ "envelope",
140
+ "legacy_token",
141
+ "token_missing",
142
+ "control_plane_url",
143
+ "binary",
144
+ ];
145
+ /**
146
+ * Invalid RuntimeLink configuration. Carries the offending field and a stable
147
+ * code, never the offending value, so a misplaced secret cannot leak.
148
+ */
149
+ export class RuntimeLinkConfigError extends Error {
150
+ code;
151
+ field;
152
+ constructor(code, field) {
153
+ super(`runtimeLink: invalid config: ${field}: ${code}`);
154
+ this.code = code;
155
+ this.field = field;
156
+ this.name = "RuntimeLinkConfigError";
157
+ }
158
+ }
159
+ /** The spec defaults with the link disabled. */
160
+ export function defaultRuntimeLinkConfig() {
161
+ return {
162
+ enabled: false,
163
+ binary: { path: DEFAULT_RUNTIME_BINARY_PATH, sha256: "" },
164
+ controlPlaneUrl: "",
165
+ token: { env: RUNTIME_LINK_ENV.token },
166
+ intervalMs: 60_000,
167
+ beatTimeoutMs: 10_000,
168
+ graceMs: 15_000,
169
+ restart: { minMs: 1_000, maxMs: 300_000, stableResetMs: 300_000 },
170
+ stopTimeoutMs: 10_000,
171
+ logCount: 3,
172
+ harness: { kind: "", version: "", deploymentEnv: "" },
173
+ };
174
+ }
175
+ const VERSION_RE = /^[A-Za-z0-9._+-]{1,64}$/;
176
+ const ENV_NAME_RE = /^[A-Za-z0-9._-]{1,32}$/;
177
+ const SHA256_RE = /^[0-9a-f]{64}$/;
178
+ const ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
179
+ const REASON_RE = /^[a-z0-9_]{1,64}$/;
180
+ const TIMESTAMP_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
181
+ const ENV_INT_RE = /^[0-9]{1,9}$/;
182
+ /**
183
+ * Clamp `intervalMs` to [15s, 300s] and validate every other field, throwing
184
+ * RuntimeLinkConfigError on the first violation. A disabled link (local-only
185
+ * mode) is valid, but its declared envelope and timings are still checked so
186
+ * typos fail loudly.
187
+ */
188
+ export function normalizeRuntimeLinkConfig(cfg) {
189
+ const b = RUNTIME_LINK_BOUNDS;
190
+ const h = cfg.harness;
191
+ if ((h.kind && !isOneOf(HARNESS_KINDS, h.kind)) || (cfg.enabled && !h.kind)) {
192
+ throw new RuntimeLinkConfigError("harness_kind", "harness.kind");
193
+ }
194
+ if (h.version && !VERSION_RE.test(h.version)) {
195
+ throw new RuntimeLinkConfigError("envelope", "harness.version");
196
+ }
197
+ if (h.deploymentEnv && !ENV_NAME_RE.test(h.deploymentEnv)) {
198
+ throw new RuntimeLinkConfigError("envelope", "harness.deploymentEnv");
199
+ }
200
+ if (cfg.enabled) {
201
+ if (cfg.token.env === RUNTIME_LINK_ENV.legacyToken) {
202
+ throw new RuntimeLinkConfigError("legacy_token", "token");
203
+ }
204
+ if (cfg.token.env !== RUNTIME_LINK_ENV.token) {
205
+ throw new RuntimeLinkConfigError("invalid_value", "token");
206
+ }
207
+ validateControlPlaneUrl(cfg.controlPlaneUrl);
208
+ if (!cfg.binary.path || cfg.binary.path.includes("\0")) {
209
+ throw new RuntimeLinkConfigError("binary", "binary.path");
210
+ }
211
+ if (!cfg.binary.sha256 && h.deploymentEnv === "prod") {
212
+ throw new RuntimeLinkConfigError("binary", "binary.sha256");
213
+ }
214
+ }
215
+ if (cfg.binary.sha256 && !SHA256_RE.test(cfg.binary.sha256)) {
216
+ throw new RuntimeLinkConfigError("binary", "binary.sha256");
217
+ }
218
+ const intervalMs = Math.min(Math.max(cfg.intervalMs, b.minIntervalMs), b.maxIntervalMs);
219
+ if (!(cfg.beatTimeoutMs > 0) || 2 * cfg.beatTimeoutMs >= intervalMs) {
220
+ throw new RuntimeLinkConfigError("beat_timeout", "beatTimeoutMs");
221
+ }
222
+ if (!inRange(cfg.graceMs, 0, b.maxGraceMs)) {
223
+ throw new RuntimeLinkConfigError("out_of_range", "graceMs");
224
+ }
225
+ const r = cfg.restart;
226
+ if (!(r.minMs >= b.minRestartMs) ||
227
+ !(r.maxMs <= b.maxRestartMs) ||
228
+ r.minMs > r.maxMs) {
229
+ throw new RuntimeLinkConfigError("out_of_range", "restart");
230
+ }
231
+ if (!inRange(r.stableResetMs, b.minStableResetMs, b.maxStableResetMs)) {
232
+ throw new RuntimeLinkConfigError("out_of_range", "restart.stableResetMs");
233
+ }
234
+ if (!inRange(cfg.stopTimeoutMs, b.minStopTimeoutMs, b.maxStopTimeoutMs)) {
235
+ throw new RuntimeLinkConfigError("out_of_range", "stopTimeoutMs");
236
+ }
237
+ if (!Number.isInteger(cfg.logCount) ||
238
+ !inRange(cfg.logCount, 0, b.maxLogCount)) {
239
+ throw new RuntimeLinkConfigError("out_of_range", "logCount");
240
+ }
241
+ return { ...cfg, intervalMs };
242
+ }
243
+ function inRange(v, lo, hi) {
244
+ return v >= lo && v <= hi;
245
+ }
246
+ function validateControlPlaneUrl(raw) {
247
+ let ok = false;
248
+ try {
249
+ const u = new URL(raw);
250
+ ok =
251
+ (u.protocol === "http:" || u.protocol === "https:") &&
252
+ u.hostname !== "" &&
253
+ u.username === "" &&
254
+ u.password === "";
255
+ }
256
+ catch {
257
+ ok = false;
258
+ }
259
+ if (!ok)
260
+ throw new RuntimeLinkConfigError("control_plane_url", "controlPlaneUrl");
261
+ }
262
+ /**
263
+ * Build a normalized config from the spec §3.2 variables. Non-empty fields of
264
+ * `harness` override the KEI_HARNESS_* variables. The runtime token is only
265
+ * checked for presence; its value is never retained.
266
+ */
267
+ export function runtimeLinkConfigFromEnv(env = process.env, harness = {}) {
268
+ const E = RUNTIME_LINK_ENV;
269
+ const get = (key) => (env[key] ?? "").trim();
270
+ const seconds = (key) => {
271
+ const raw = get(key);
272
+ if (raw === "")
273
+ return undefined;
274
+ if (!ENV_INT_RE.test(raw))
275
+ throw new RuntimeLinkConfigError("invalid_value", key);
276
+ return Number(raw) * 1000;
277
+ };
278
+ const cfg = defaultRuntimeLinkConfig();
279
+ cfg.intervalMs = seconds(E.interval) ?? cfg.intervalMs;
280
+ cfg.beatTimeoutMs = seconds(E.timeout) ?? cfg.beatTimeoutMs;
281
+ cfg.restart.minMs = seconds(E.restartMin) ?? cfg.restart.minMs;
282
+ cfg.restart.maxMs = seconds(E.restartMax) ?? cfg.restart.maxMs;
283
+ cfg.restart.stableResetMs =
284
+ seconds(E.stableSeconds) ?? cfg.restart.stableResetMs;
285
+ const logCount = get(E.logCount);
286
+ if (logCount !== "") {
287
+ if (!ENV_INT_RE.test(logCount))
288
+ throw new RuntimeLinkConfigError("invalid_value", E.logCount);
289
+ cfg.logCount = Number(logCount);
290
+ }
291
+ const hasToken = get(E.token) !== "";
292
+ if (!hasToken && get(E.legacyToken) !== "") {
293
+ throw new RuntimeLinkConfigError("legacy_token", E.legacyToken);
294
+ }
295
+ cfg.enabled = hasToken;
296
+ const enabled = get(E.enabled).toLowerCase();
297
+ if (enabled !== "") {
298
+ if (enabled === "true" || enabled === "1") {
299
+ if (!hasToken)
300
+ throw new RuntimeLinkConfigError("token_missing", E.token);
301
+ cfg.enabled = true;
302
+ }
303
+ else if (enabled === "false" || enabled === "0") {
304
+ cfg.enabled = false;
305
+ }
306
+ else {
307
+ throw new RuntimeLinkConfigError("invalid_value", E.enabled);
308
+ }
309
+ }
310
+ cfg.controlPlaneUrl = get(E.controlPlaneUrl);
311
+ cfg.binary = {
312
+ path: get(E.proxyPath) || DEFAULT_RUNTIME_BINARY_PATH,
313
+ sha256: get(E.proxySha),
314
+ };
315
+ cfg.harness = {
316
+ kind: harness.kind || get(E.harnessKind),
317
+ version: harness.version || get(E.harnessVersion),
318
+ deploymentEnv: harness.deploymentEnv || get(E.deploymentEnv),
319
+ };
320
+ return normalizeRuntimeLinkConfig(cfg);
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Backoff and cancellation
324
+ // ---------------------------------------------------------------------------
325
+ const MAX_JITTER_SAMPLE = 1 - Number.EPSILON / 2;
326
+ /**
327
+ * Full-jitter restart delay for attempt n (0-based):
328
+ * `floor(r × min(maxMs, minMs × 2^n))`. `r` is clamped to [0, 1); negative
329
+ * attempts count as 0.
330
+ */
331
+ export function backoffDelayMs(backoff, attempt, r) {
332
+ const n = Math.min(Math.max(Math.trunc(attempt), 0), 30);
333
+ const capMs = Math.min(backoff.maxMs, backoff.minMs * 2 ** n);
334
+ const sample = Math.min(Math.max(r, 0), MAX_JITTER_SAMPLE);
335
+ return Math.floor(sample * capMs);
336
+ }
337
+ /**
338
+ * Sleep for the jittered delay, or reject with the signal's abort reason as
339
+ * soon as `signal` aborts, so a stop during backoff never waits out the delay.
340
+ */
341
+ export function waitBackoff(backoff, attempt, options = {}) {
342
+ const { signal, random = Math.random } = options;
343
+ return new Promise((resolve, reject) => {
344
+ const abortReason = () => signal?.reason ?? new Error("aborted");
345
+ if (signal?.aborted) {
346
+ reject(abortReason());
347
+ return;
348
+ }
349
+ const onAbort = () => {
350
+ clearTimeout(timer);
351
+ reject(abortReason());
352
+ };
353
+ const timer = setTimeout(() => {
354
+ signal?.removeEventListener("abort", onAbort);
355
+ resolve();
356
+ }, backoffDelayMs(backoff, attempt, random()));
357
+ signal?.addEventListener("abort", onAbort, { once: true });
358
+ });
359
+ }
360
+ /** A child line the SDK must drop and count. */
361
+ export class ChildLineError extends Error {
362
+ kind;
363
+ constructor(kind, detail) {
364
+ super(`runtimeLink: child line ${kind}: ${detail}`);
365
+ this.kind = kind;
366
+ this.name = "ChildLineError";
367
+ }
368
+ }
369
+ const MAX_SEQ = Number.MAX_SAFE_INTEGER;
370
+ const MAX_LATENCY_MS = 3_600_000;
371
+ const MAX_NEXT_IN_MS = 3_600_000;
372
+ class FieldReader {
373
+ raw;
374
+ constructor(raw) {
375
+ this.raw = raw;
376
+ }
377
+ text(key, required) {
378
+ if (!Object.hasOwn(this.raw, key)) {
379
+ if (required)
380
+ throw new ChildLineError("malformed", key);
381
+ return "";
382
+ }
383
+ const v = this.raw[key];
384
+ if (typeof v !== "string")
385
+ throw new ChildLineError("malformed", key);
386
+ return v;
387
+ }
388
+ id(key, required) {
389
+ const v = this.text(key, required);
390
+ if ((v !== "" || required) && !ID_RE.test(v))
391
+ throw new ChildLineError("malformed", key);
392
+ return v;
393
+ }
394
+ timestamp(key) {
395
+ const v = this.text(key, true);
396
+ if (!TIMESTAMP_RE.test(v))
397
+ throw new ChildLineError("malformed", key);
398
+ return v;
399
+ }
400
+ integer(key, required, lo, hi) {
401
+ if (!Object.hasOwn(this.raw, key)) {
402
+ if (required)
403
+ throw new ChildLineError("malformed", key);
404
+ return 0;
405
+ }
406
+ const v = this.raw[key];
407
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < lo || v > hi) {
408
+ throw new ChildLineError("malformed", key);
409
+ }
410
+ return v;
411
+ }
412
+ }
413
+ const encoder = new TextEncoder();
414
+ const decoder = new TextDecoder();
415
+ /**
416
+ * Parse one line of child stdout (a trailing "\n" / "\r\n" is ignored). Only
417
+ * allowlisted fields survive; payloads, results, reasoning, tokens, or any
418
+ * other key the runtime might emit are dropped. Throws ChildLineError for
419
+ * lines the SDK must drop and count.
420
+ */
421
+ export function parseChildLine(line) {
422
+ let text = typeof line === "string" ? line : decoder.decode(line);
423
+ if (text.endsWith("\n"))
424
+ text = text.slice(0, -1);
425
+ if (text.endsWith("\r"))
426
+ text = text.slice(0, -1);
427
+ if (encoder.encode(text).length > MAX_CHILD_LINE_BYTES) {
428
+ throw new ChildLineError("line_too_long", "line");
429
+ }
430
+ let raw;
431
+ try {
432
+ raw = JSON.parse(text);
433
+ }
434
+ catch {
435
+ throw new ChildLineError("malformed", "not json");
436
+ }
437
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
438
+ throw new ChildLineError("malformed", "not an object");
439
+ }
440
+ const obj = raw;
441
+ if (obj.v !== RUNTIME_LINK_CONTRACT_VERSION) {
442
+ throw new ChildLineError("contract_mismatch", "v");
443
+ }
444
+ if (typeof obj.event !== "string")
445
+ throw new ChildLineError("malformed", "event");
446
+ const f = new FieldReader(obj);
447
+ switch (obj.event) {
448
+ case "identity":
449
+ return {
450
+ kind: "identity",
451
+ identity: {
452
+ runId: f.id("run_id", true),
453
+ installationId: f.id("installation_id", true),
454
+ orgId: f.id("org_id", true),
455
+ workspaceId: f.id("workspace_id", false),
456
+ platform: f.id("platform", false),
457
+ status: f.id("status", false),
458
+ bindingStatus: f.id("binding_status", false),
459
+ runtimeVersion: f.id("runtime_version", false),
460
+ },
461
+ };
462
+ case "beat": {
463
+ const runId = f.id("run_id", true);
464
+ const seq = f.integer("seq", false, 0, MAX_SEQ);
465
+ const at = f.timestamp("at");
466
+ const outcome = f.text("outcome", true);
467
+ const latencyMs = f.integer("latency_ms", false, 0, MAX_LATENCY_MS);
468
+ const nextInMs = f.integer("next_in_ms", false, 0, MAX_NEXT_IN_MS);
469
+ const httpStatus = f.integer("http_status", false, 0, 599);
470
+ if (httpStatus > 0 && httpStatus < 100)
471
+ throw new ChildLineError("malformed", "http_status");
472
+ if (!isOneOf(BEAT_OUTCOMES, outcome))
473
+ throw new ChildLineError("contract_mismatch", "outcome");
474
+ return {
475
+ kind: "beat",
476
+ beat: { runId, seq, at, outcome, httpStatus, latencyMs, nextInMs },
477
+ };
478
+ }
479
+ case "terminal": {
480
+ const runId = f.id("run_id", true);
481
+ const reason = f.text("reason", true);
482
+ if (!REASON_RE.test(reason))
483
+ throw new ChildLineError("malformed", "reason");
484
+ return { kind: "terminal", terminal: { runId, reason } };
485
+ }
486
+ default:
487
+ return { kind: "ignored" };
488
+ }
489
+ }
490
+ // ---------------------------------------------------------------------------
491
+ // Redacted lifecycle events (spec §5.3)
492
+ // ---------------------------------------------------------------------------
493
+ /** A lifecycle event has a value outside its closed set or bounds. */
494
+ export class InvalidLinkEventError extends Error {
495
+ constructor(detail) {
496
+ super(`runtimeLink: invalid lifecycle event: ${detail}`);
497
+ this.name = "InvalidLinkEventError";
498
+ }
499
+ }
500
+ function isCount(v) {
501
+ return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
502
+ }
503
+ /** Throws InvalidLinkEventError unless every closed set and bound holds. */
504
+ export function validateLinkEvent(ev) {
505
+ const h = ev.harness;
506
+ const ok = isOneOf(LIFECYCLE_EVENT_NAMES, ev.name) &&
507
+ isOneOf(LINK_STATES, ev.state) &&
508
+ (ev.reason === "" || isOneOf(FAILURE_CLASSES, ev.reason)) &&
509
+ ev.at instanceof Date &&
510
+ !Number.isNaN(ev.at.getTime()) &&
511
+ (ev.runId === "" || ID_RE.test(ev.runId)) &&
512
+ ID_RE.test(ev.sdkInstanceId) &&
513
+ isOneOf(HARNESS_KINDS, h.kind) &&
514
+ isOneOf(SDK_LANGS, h.sdkLang) &&
515
+ (h.version === "" || VERSION_RE.test(h.version)) &&
516
+ (h.deploymentEnv === "" || ENV_NAME_RE.test(h.deploymentEnv)) &&
517
+ (h.sdkVersion === "" || VERSION_RE.test(h.sdkVersion)) &&
518
+ isCount(ev.seq) &&
519
+ isCount(ev.restarts) &&
520
+ isCount(ev.consecutiveFails);
521
+ if (!ok)
522
+ throw new InvalidLinkEventError("value outside contract");
523
+ return ev;
524
+ }
525
+ function str(v, fallback = "") {
526
+ if (v === undefined)
527
+ return fallback;
528
+ if (typeof v !== "string")
529
+ throw new InvalidLinkEventError("expected string");
530
+ return v;
531
+ }
532
+ function parseEventTimestamp(v) {
533
+ const m = typeof v === "string" ? TIMESTAMP_RE.exec(v) : null;
534
+ if (!m)
535
+ throw new InvalidLinkEventError("at");
536
+ const fraction = (m[2] ?? ".000").slice(0, 4);
537
+ return new Date(`${m[1]}${fraction}${m[3]}`);
538
+ }
539
+ /** Build an event from allowlisted wire keys only; everything else is dropped. */
540
+ export function linkEventFromWire(raw) {
541
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
542
+ throw new InvalidLinkEventError("not an object");
543
+ }
544
+ const o = raw;
545
+ const hRaw = o.harness ?? {};
546
+ if (hRaw === null || typeof hRaw !== "object" || Array.isArray(hRaw)) {
547
+ throw new InvalidLinkEventError("harness");
548
+ }
549
+ const h = hRaw;
550
+ return validateLinkEvent({
551
+ name: str(o.name),
552
+ at: parseEventTimestamp(o.at),
553
+ state: str(o.state),
554
+ reason: str(o.reason),
555
+ runId: str(o.run_id),
556
+ seq: (o.seq ?? 0),
557
+ sdkInstanceId: str(o.sdk_instance_id),
558
+ harness: {
559
+ kind: str(h.kind),
560
+ version: str(h.version),
561
+ deploymentEnv: str(h.deployment_env),
562
+ sdkLang: str(h.sdk_lang),
563
+ sdkVersion: str(h.sdk_version),
564
+ },
565
+ restarts: (o.restarts ?? 0),
566
+ consecutiveFails: (o.consecutive_fails ?? 0),
567
+ });
568
+ }
569
+ /** Validate and emit exactly the allowlisted keys; `at` is UTC with millisecond precision. */
570
+ export function linkEventToWire(ev) {
571
+ validateLinkEvent(ev);
572
+ const h = ev.harness;
573
+ return {
574
+ name: ev.name,
575
+ at: ev.at.toISOString(),
576
+ state: ev.state,
577
+ reason: ev.reason,
578
+ run_id: ev.runId,
579
+ seq: ev.seq,
580
+ sdk_instance_id: ev.sdkInstanceId,
581
+ harness: {
582
+ kind: h.kind,
583
+ version: h.version,
584
+ deployment_env: h.deploymentEnv,
585
+ sdk_lang: h.sdkLang,
586
+ sdk_version: h.sdkVersion,
587
+ },
588
+ restarts: ev.restarts,
589
+ consecutive_fails: ev.consecutiveFails,
590
+ };
591
+ }
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "@haikeilabs/agentware",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Agent middleware and tooling for LLM tool calling",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/HaikeiLabs/Agentware.git",
7
+ "url": "git+https://github.com/HaikeiLabs/agentware.git",
8
8
  "directory": "typescript"
9
9
  },
10
- "homepage": "https://github.com/HaikeiLabs/Agentware#readme",
10
+ "homepage": "https://github.com/HaikeiLabs/agentware#readme",
11
11
  "bugs": {
12
- "url": "https://github.com/HaikeiLabs/Agentware/issues"
12
+ "url": "https://github.com/HaikeiLabs/agentware/issues"
13
13
  },
14
14
  "main": "dist/index.js",
15
15
  "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
16
22
  "type": "module",
17
23
  "files": [
18
24
  "dist",
@@ -21,6 +27,7 @@
21
27
  "scripts": {
22
28
  "prepack": "npm run build",
23
29
  "build": "tsc",
30
+ "check:package": "node scripts/check-package.mjs",
24
31
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
25
32
  "lint": "eslint src --ext .ts",
26
33
  "format": "prettier --write \"src/**/*.ts\""