@reinsjs/agent 0.1.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 ADDED
@@ -0,0 +1,24 @@
1
+ # @reinsjs/agent
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - First public release.
8
+
9
+ - **Timeline first.** Append-only event log with `schemaVersion` on every event and fail-closed upcast on read; projection decides what the model sees each turn; replay and fork from the log alone.
10
+ - **A loop you can copy.** `runLoop` as an exported async generator: pause/resume is a return value with a small signed state, bounded retry on transient failures only while nothing has been written, tool pipeline `beforeTool → validate → approval → execute → afterTool`, sub-agent pause bubbling (`Interruption { kind: "subagent" }`).
11
+ - **Brain modules** (`@reinsjs/brain`): perception, compact (with folded-result manifest and `recall`; recommended on after measuring 100% recall at lower token cost on two model families), pins, spill, handoff, memory (`memory_20250818` shape, namespace isolation), approval (deny → ask → allow, fail-closed), budget (five limits, sub-agent usage included).
12
+ - **Lowering** (`@reinsjs/lowering-pi`): Anthropic Messages and OpenAI Responses via pi-ai with a declared loss matrix, trust markers on untrusted content, cache-breakpoint handling.
13
+ - **Serving** (`@reinsjs/server`, `@reinsjs/ui-agui`): Web-standard handler with SSE replay from `lastSeq`, per-session run lock, `authorizeSession` (only `true` allows), AG-UI as the first-class UI protocol.
14
+ - **Stores**: SQLite (`node:sqlite` / `bun:sqlite`) and Postgres (`pg` / PGlite), conformance-tested; configurable memory table for per-role isolation.
15
+ - **Tools**: MCP servers as one socket (`@reinsjs/tools-mcp`, tools bound per run, annotations as defaults not permissions); `asTool(agent)` in `reins`.
16
+ - **TanStack AI** middleware (`@reinsjs/adapter-tanstack-ai`): the brain on TanStack's loop with the log as the single source of truth; idempotent client-message import; runtime check for the approval interrupt.
17
+ - **Eval harness** (`@reinsjs/eval`): fixtures from real recordings, recorded tools, arm-vs-arm runner, metrics, the four-rule gate.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @reinsjs/core@0.1.0
23
+ - @reinsjs/server@0.1.0
24
+ - @reinsjs/ui-agui@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NewRate Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @reinsjs/agent
2
+
3
+ **Hand the reins to the model.** `reins` is the umbrella package: `createAgent()` plus everything from `@reinsjs/core`, `@reinsjs/server` and `@reinsjs/ui-agui` re-exported, so one install gives you an agent with a Web-standard handler and an AG-UI stream. Bring a lowering layer (`@reinsjs/lowering-pi`) and, optionally, the brain (`@reinsjs/brain`) and a store (`@reinsjs/store-sqlite`, `@reinsjs/store-pg`).
4
+
5
+ ```bash
6
+ pnpm add @reinsjs/agent @reinsjs/lowering-pi @reinsjs/brain
7
+ ```
8
+
9
+ ```ts
10
+ import { anthropic } from "@reinsjs/lowering-pi"
11
+ import { approval, compact, perception } from "@reinsjs/brain"
12
+ import { createAgent, defineTool, memoryStore } from "@reinsjs/agent"
13
+
14
+ const deploy = defineTool<{ env: "staging" | "prod" }>({
15
+ name: "deploy",
16
+ description: "Ship the current build",
17
+ inputSchema: { type: "object", properties: { env: { type: "string", enum: ["staging", "prod"] } }, required: ["env"] },
18
+ needsApproval: true,
19
+ execute: ({ env }) => `deployed to ${env}`,
20
+ })
21
+
22
+ export const agent = createAgent({
23
+ model: anthropic("claude-opus-5", { apiKey: process.env.ANTHROPIC_API_KEY! }),
24
+ tools: [deploy],
25
+ store: memoryStore(), // sqliteStores(...) / pgStores(...) for anything real
26
+ sockets: [perception(), compact(), approval()],
27
+ systemPrompt: "Be brief and direct.",
28
+ secret: process.env.REINS_SECRET, // signs the pause state so it can round-trip through a client
29
+ })
30
+
31
+ export const POST = agent.handler // (Request) => Promise<Response>, AG-UI over SSE
32
+ ```
33
+
34
+ Without HTTP:
35
+
36
+ ```ts
37
+ for await (const event of agent.run({ input: "Ship staging" })) console.log(event.type)
38
+ ```
39
+
40
+ `run()` yields every event as it is appended and returns `done | paused | handoff | error`. A `paused` result carries a small signed `state`; resume from any process with `agent.run({ sessionId, resume: state, decisions: [{ toolCallId, approved: true, by: "alice" }] })`.
41
+
42
+ ## Agents as tools
43
+
44
+ ```ts
45
+ import { asTool } from "@reinsjs/agent"
46
+
47
+ const lead = createAgent({
48
+ model, store,
49
+ tools: [
50
+ asTool(analyst, { name: "ask_analyst", description: "Ask the inventory analyst. Give a self-contained question." }),
51
+ asTool(writer, { name: "ask_writer", description: "Ask the copywriter for customer-facing copy.", abort: "detached" }),
52
+ ],
53
+ sockets: [approval(), budget({ limits: { totalTokens: 500_000 } })],
54
+ })
55
+ ```
56
+
57
+ Whether to call an expert, what to ask, and whether to trust the answer are the parent model's decisions; there is no orchestrator. `asTool` handles what a hand-rolled wrapper gets wrong: when the child pauses for approval the **parent pauses too** (`Interruption { kind: "subagent" }` carries the child's interruptions), the host answers with `decisions: [{ toolCallId, sessionId: childSessionId, approved, by }]`, and the child's tokens count against the parent's budget. `examples/team` in the repository is a complete three-role setup on Postgres.
58
+
59
+ ## What you get for free
60
+
61
+ - An append-only timeline you can replay, fork and audit — the model reads what actually happened, in order.
62
+ - A loop of a few hundred lines you can read and copy, with pause/resume as a return value.
63
+ - Brain modules that let the model tidy its own context, keep constraints, spill big results, remember, ask before dangerous actions, and stop at a budget — each optional, each measured before it became a default.
64
+ - Fail-closed defaults: unknown events are rejected, forged approval decisions cannot enter through the input, sessions are `404` to anyone not explicitly authorized.
65
+
66
+ ## Documentation
67
+
68
+ The design documents are in Chinese under `docs/` at the repository root: `docs/系统全景图.md` (overview), `docs/技术方案.md` (spec), `docs/DECISIONS.md` (why). Package READMEs are in English.
69
+
70
+ MIT © 2026 NewRate Limited.
@@ -0,0 +1,113 @@
1
+ import { ContentPart, EventDraft, SerializedRunState, ApprovalDecisionInput, Principal, LoweringDelta, Event, RunResult, BoundModel, Stores, ToolContext, Tool } from '@reinsjs/core';
2
+ export * from '@reinsjs/core';
3
+ import { AgentDefinition, AgentHandler, HandlerOptions } from '@reinsjs/server';
4
+ export * from '@reinsjs/server';
5
+ export * from '@reinsjs/ui-agui';
6
+
7
+ /**
8
+ * createAgent:把"模型 + 工具 + 存储 + 脑子"装成一个 agent 对象,给出两种用法 ——
9
+ * `handler`(Web 标准 (Request) => Response,缺省 AG-UI 编码)和 `run()`(不经 HTTP 直接跑,脚本 / 队列 / 测试用)。
10
+ *
11
+ * 它不引入新概念:AgentDefinition 就是 runLoop 的跨请求配置,handler 就是 createAgentHandler。
12
+ * 这里只负责把 BoundModel 拆成 model + lowering、把 Stores 拆成 log / blobs / memory。
13
+ */
14
+
15
+ interface CreateAgentOptions extends Omit<AgentDefinition, "model" | "lowering" | "log" | "blobs" | "memory"> {
16
+ /** `anthropic("claude-opus-5", { apiKey })` 之类工厂的返回值,或自己组的 { model, lowering } */
17
+ model: BoundModel;
18
+ /** `memoryStore()`、`sqliteStores(db)`、`await pgStores(client)`(B9)或自己实现的一套接口;只有 log 必需 */
19
+ store: Stores;
20
+ /** 传输层选项;缺省 AG-UI 编码(`encode: aguiEncoding()`),想推原始事件就传 `encode: () => rawEncoder` */
21
+ handler?: HandlerOptions;
22
+ }
23
+ /** `agent.run()` 的每次参数:与 HTTP 请求体是同一组字段,多了 signal / onDelta / principal 这些进程内才有的 */
24
+ interface RunOptions {
25
+ /** 缺省新建会话;结果的 sessionId 告诉你它是什么 */
26
+ sessionId?: string;
27
+ input?: string | ContentPart[] | EventDraft;
28
+ resume?: SerializedRunState;
29
+ decisions?: readonly ApprovalDecisionInput[];
30
+ principal?: Principal;
31
+ signal?: AbortSignal;
32
+ onDelta?: (delta: LoweringDelta) => void;
33
+ }
34
+ interface Agent {
35
+ /** 跨请求不变的循环配置;要自己起 runLoop 或接别的传输层时用它 */
36
+ readonly definition: AgentDefinition;
37
+ /** Web 标准 handler:POST 起 run、GET 补发,见 @reinsjs/server */
38
+ readonly handler: AgentHandler;
39
+ /** 不经 HTTP 直接跑一次:yield 每条刚 append 的事件,返回 RunResult 四态 */
40
+ run(options?: RunOptions): AsyncGenerator<Event, RunResult>;
41
+ }
42
+ declare function createAgent(options: CreateAgentOptions): Agent;
43
+
44
+ /**
45
+ * asTool:把一个 Agent 包成另一个 Agent 工具表上的 Tool —— 子代理即工具(技术方案 §10.1)。
46
+ *
47
+ * 要不要叫这个专家、叫它做什么、结果信不信,都是父模型在判断(宪法一);库不做编排器,只把"手写容易做错的两件事"做对:
48
+ * - **审批冒泡**:子 run 暂停(审批 / 预算 / 中止)时不把状态当 isError 交给父模型,而是返回 `subagentPause`——父 run 不落这条
49
+ * tool_result、整体 paused,宿主拿到 `Interruption(kind=subagent)`,处理子的审批后续跑父 run;父续跑补齐这条 pending 调用时,
50
+ * 本工具凭 childSessionId 续跑子 run。状态全部可序列化,换进程成立。
51
+ * - **预算合算**:子 run 的每条 budget_usage 经 `ctx.spend` 计入父 run 的 tokensSpent,父的 budget 模块按总账拦。
52
+ * 其余三件与手写范式(examples/team/subagent-tool.handwritten.ts)一致:中止传递由使用者自决(abort)、principal 原样下传、
53
+ * childSessionId 写进结果 JSON(模型可见,回放顺着它找子会话)。
54
+ *
55
+ * 子会话 id 缺省 `${父 sessionId}:${toolCallId}`:确定性派生,续跑时不靠内存、不靠宿主传回就能找回子会话。
56
+ * 子日志末条是 run_paused 即续跑(不带 input,宿主给子会话的结论从 `ctx.decisions` 原样下传,多层嵌套逐层传);
57
+ * 否则当这个专家的新一轮(带 input)——传自己的 `childSessionId` 就能跟同一个专家多轮对话。
58
+ * 深度守卫仍靠工具表:专家 agent 的工具表里不放 asTool 类工具。
59
+ */
60
+
61
+ interface SubagentTask {
62
+ task: string;
63
+ }
64
+ interface AsToolOptions {
65
+ /** 模型看到的工具名,如 ask_analyst */
66
+ name: string;
67
+ description: string;
68
+ /** 角色名,写进结果 JSON 方便模型与回放辨认 */
69
+ role?: string;
70
+ /**
71
+ * 中止传递:linked(缺省)把父的 signal 传给子 run,父停子停;detached 不传,父中止时子做完为止(接力语义)。
72
+ * 循环层保证正在执行的工具跑完、结果落进父日志,父才 paused(host)
73
+ */
74
+ abort?: "linked" | "detached";
75
+ /** 缺省 low(专家只有只读工具)。专家能改东西就提高,让父侧 approval 模块问人 */
76
+ risk?: "low" | "medium" | "high";
77
+ /** 子会话 id;缺省 `${父 sessionId}:${toolCallId}`。返回已有会话即跟同一个专家继续多轮 */
78
+ childSessionId?: (ctx: ToolContext, input: SubagentTask) => string;
79
+ }
80
+ /** 子 run 的用量汇总(从子会话时间线算,含续跑之前的部分) */
81
+ interface SubagentUsage {
82
+ requests: number;
83
+ input: number;
84
+ output: number;
85
+ cacheRead: number;
86
+ cacheWrite: number;
87
+ toolCalls: number;
88
+ }
89
+ /** 写进父 tool_result 的 JSON(模型可见) */
90
+ interface SubagentOutcome {
91
+ role?: string;
92
+ childSessionId: string;
93
+ /** 只会是 done / handoff / error:paused 不落结果而是冒泡 */
94
+ status: RunResult["status"];
95
+ /** 子 run 最后一轮的文本(最后一次工具调用之后的 model_text 拼接) */
96
+ answer?: string;
97
+ /** handoff 时的目标会话,error 时的错误 */
98
+ detail?: string;
99
+ usage: SubagentUsage;
100
+ }
101
+ declare const SUBAGENT_TASK_SCHEMA: Record<string, unknown>;
102
+ /** 缺省子会话 id:父会话 + 这次调用,确定性派生 */
103
+ declare function defaultChildSessionId(ctx: ToolContext): string;
104
+ declare function asTool(agent: Agent, opts: AsToolOptions): Tool;
105
+ /** 子会话时间线里的用量合计:每条 budget_usage 一次请求,tool_call 计工具次数 */
106
+ declare function usageOf(timeline: readonly Event[]): SubagentUsage;
107
+ /** 父时间线里,凡 name 在 toolNames 里的 tool_result,其 JSON 里的 childSessionId —— 回放 / 导出用 */
108
+ declare function subagentOutcomesOf(events: Iterable<{
109
+ type: string;
110
+ payload: unknown;
111
+ }>, toolNames: ReadonlySet<string>): SubagentOutcome[];
112
+
113
+ export { type Agent, type AsToolOptions, type CreateAgentOptions, type RunOptions, SUBAGENT_TASK_SCHEMA, type SubagentOutcome, type SubagentTask, type SubagentUsage, asTool, createAgent, defaultChildSessionId, subagentOutcomesOf, usageOf };
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ // src/index.ts
2
+ export * from "@reinsjs/core";
3
+ export * from "@reinsjs/server";
4
+ export * from "@reinsjs/ui-agui";
5
+
6
+ // src/as-tool.ts
7
+ import {
8
+ createCoreRegistry,
9
+ defineTool,
10
+ readTimeline,
11
+ subagentPause
12
+ } from "@reinsjs/core";
13
+ var SUBAGENT_TASK_SCHEMA = {
14
+ type: "object",
15
+ properties: {
16
+ task: {
17
+ type: "string",
18
+ description: "What you want this expert to do, self-contained: the expert sees none of your conversation."
19
+ }
20
+ },
21
+ required: ["task"],
22
+ additionalProperties: false
23
+ };
24
+ function defaultChildSessionId(ctx) {
25
+ return `${ctx.sessionId}:${ctx.toolCallId}`;
26
+ }
27
+ function asTool(agent, opts) {
28
+ const abort = opts.abort ?? "linked";
29
+ const registry = agent.definition.registry ?? createCoreRegistry();
30
+ const log = agent.definition.log;
31
+ return defineTool({
32
+ name: opts.name,
33
+ description: opts.description,
34
+ inputSchema: SUBAGENT_TASK_SCHEMA,
35
+ risk: opts.risk ?? "low",
36
+ validate(input) {
37
+ const task = input?.task;
38
+ if (typeof task !== "string" || task.trim() === "") throw new Error("task \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
39
+ return { task };
40
+ },
41
+ async execute(input, ctx) {
42
+ const childSessionId = (opts.childSessionId ?? defaultChildSessionId)(ctx, input);
43
+ const last = (await log.tail(childSessionId, 1))[0];
44
+ const resuming = last?.type === "core.run_paused";
45
+ const run = agent.run({
46
+ sessionId: childSessionId,
47
+ ...resuming ? {} : { input: input.task },
48
+ // 宿主给别的会话的结论原样下传:子循环把 sessionId 等于自己的拿去校验并记事件,其余再转发给它的工具(多层嵌套)
49
+ ...ctx.decisions && ctx.decisions.length > 0 ? { decisions: ctx.decisions } : {},
50
+ ...ctx.principal ? { principal: ctx.principal } : {},
51
+ ...abort === "linked" && ctx.signal ? { signal: ctx.signal } : {}
52
+ });
53
+ let answer = [];
54
+ let result;
55
+ while (true) {
56
+ const step = await run.next();
57
+ if (step.done) {
58
+ result = step.value;
59
+ break;
60
+ }
61
+ const e = step.value;
62
+ switch (e.type) {
63
+ case "core.budget_usage":
64
+ ctx.spend?.(e.payload.tokens);
65
+ break;
66
+ case "core.tool_call":
67
+ answer = [];
68
+ break;
69
+ case "core.model_text":
70
+ answer.push(e.payload.text);
71
+ break;
72
+ }
73
+ }
74
+ if (result.status === "paused") {
75
+ return subagentPause({
76
+ childSessionId,
77
+ reason: result.reason,
78
+ interruptions: result.interruptions,
79
+ state: result.state
80
+ });
81
+ }
82
+ const outcome = {
83
+ ...opts.role !== void 0 ? { role: opts.role } : {},
84
+ childSessionId,
85
+ status: result.status,
86
+ usage: usageOf(await readTimeline(log, childSessionId, { registry }))
87
+ };
88
+ let isError = false;
89
+ switch (result.status) {
90
+ case "done":
91
+ outcome.answer = answer.join("\n");
92
+ break;
93
+ case "handoff":
94
+ outcome.answer = answer.join("\n");
95
+ outcome.detail = `expert handed off to session ${result.toSessionId}; its work continues there`;
96
+ break;
97
+ case "error":
98
+ isError = true;
99
+ outcome.detail = `expert failed: ${result.error.payload.message ?? "unknown error"}`;
100
+ break;
101
+ }
102
+ const res = { content: [{ type: "text", text: JSON.stringify(outcome, null, 2) }], isError };
103
+ return res;
104
+ }
105
+ });
106
+ }
107
+ function usageOf(timeline) {
108
+ const usage = { requests: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, toolCalls: 0 };
109
+ for (const e of timeline) {
110
+ if (e.type === "core.budget_usage") {
111
+ const t = e.payload.tokens;
112
+ usage.requests += 1;
113
+ usage.input += t.input;
114
+ usage.output += t.output;
115
+ usage.cacheRead += t.cacheRead ?? 0;
116
+ usage.cacheWrite += t.cacheWrite ?? 0;
117
+ } else if (e.type === "core.tool_call") usage.toolCalls += 1;
118
+ }
119
+ return usage;
120
+ }
121
+ function subagentOutcomesOf(events, toolNames) {
122
+ const found = [];
123
+ for (const e of events) {
124
+ if (e.type !== "core.tool_result") continue;
125
+ const p = e.payload;
126
+ if (!toolNames.has(p.name)) continue;
127
+ const text = p.content.find((c) => c.type === "text")?.text;
128
+ if (!text) continue;
129
+ try {
130
+ const parsed = JSON.parse(text);
131
+ if (typeof parsed.childSessionId === "string") found.push(parsed);
132
+ } catch {
133
+ }
134
+ }
135
+ return found;
136
+ }
137
+
138
+ // src/create-agent.ts
139
+ import {
140
+ runLoop,
141
+ uuidv7
142
+ } from "@reinsjs/core";
143
+ import {
144
+ createAgentHandler
145
+ } from "@reinsjs/server";
146
+ import { aguiEncoding } from "@reinsjs/ui-agui";
147
+ function createAgent(options) {
148
+ const { model, store, handler: handlerOptions, ...rest } = options;
149
+ const definition = {
150
+ ...rest,
151
+ model: model.model,
152
+ lowering: model.lowering,
153
+ log: store.log,
154
+ ...store.blobs ? { blobs: store.blobs } : {},
155
+ ...store.memory ? { memory: store.memory } : {}
156
+ };
157
+ const handler = createAgentHandler(definition, { encode: aguiEncoding(), ...handlerOptions });
158
+ return {
159
+ definition,
160
+ handler,
161
+ run(runOptions = {}) {
162
+ const { sessionId, ...perRun } = runOptions;
163
+ return runLoop({ ...definition, ...stripUndefined(perRun), sessionId: sessionId ?? uuidv7() });
164
+ }
165
+ };
166
+ }
167
+ function stripUndefined(obj) {
168
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
169
+ }
170
+ export {
171
+ SUBAGENT_TASK_SCHEMA,
172
+ asTool,
173
+ createAgent,
174
+ defaultChildSessionId,
175
+ subagentOutcomesOf,
176
+ usageOf
177
+ };
178
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/as-tool.ts","../src/create-agent.ts"],"sourcesContent":["/**\n * reins —— 总包。装上、配自己的模型,就得到一辆有驾驭经验的车(PRD §5.1):\n *\n * import { createAgent, memoryStore } from \"@reinsjs/agent\"\n * import { anthropic } from \"@reinsjs/lowering-pi\"\n * const agent = createAgent({ model: anthropic(\"claude-opus-5\", { apiKey }), tools, store: memoryStore() })\n * export const POST = agent.handler\n *\n * 本包只做一件事:createAgent。其余全部原样再导出 @reinsjs/core、@reinsjs/server、@reinsjs/ui-agui,\n * 用户装一个包就够;降级层(@reinsjs/lowering-pi,带 pi-ai 依赖)单独装,换别的降级层实现不必带着它。\n */\nexport * from \"@reinsjs/core\"\nexport * from \"@reinsjs/server\"\nexport * from \"@reinsjs/ui-agui\"\nexport {\n type AsToolOptions,\n asTool,\n defaultChildSessionId,\n SUBAGENT_TASK_SCHEMA,\n type SubagentOutcome,\n type SubagentTask,\n type SubagentUsage,\n subagentOutcomesOf,\n usageOf,\n} from \"./as-tool.js\"\nexport { type Agent, type CreateAgentOptions, createAgent, type RunOptions } from \"./create-agent.js\"\n","/**\n * asTool:把一个 Agent 包成另一个 Agent 工具表上的 Tool —— 子代理即工具(技术方案 §10.1)。\n *\n * 要不要叫这个专家、叫它做什么、结果信不信,都是父模型在判断(宪法一);库不做编排器,只把\"手写容易做错的两件事\"做对:\n * - **审批冒泡**:子 run 暂停(审批 / 预算 / 中止)时不把状态当 isError 交给父模型,而是返回 `subagentPause`——父 run 不落这条\n * tool_result、整体 paused,宿主拿到 `Interruption(kind=subagent)`,处理子的审批后续跑父 run;父续跑补齐这条 pending 调用时,\n * 本工具凭 childSessionId 续跑子 run。状态全部可序列化,换进程成立。\n * - **预算合算**:子 run 的每条 budget_usage 经 `ctx.spend` 计入父 run 的 tokensSpent,父的 budget 模块按总账拦。\n * 其余三件与手写范式(examples/team/subagent-tool.handwritten.ts)一致:中止传递由使用者自决(abort)、principal 原样下传、\n * childSessionId 写进结果 JSON(模型可见,回放顺着它找子会话)。\n *\n * 子会话 id 缺省 `${父 sessionId}:${toolCallId}`:确定性派生,续跑时不靠内存、不靠宿主传回就能找回子会话。\n * 子日志末条是 run_paused 即续跑(不带 input,宿主给子会话的结论从 `ctx.decisions` 原样下传,多层嵌套逐层传);\n * 否则当这个专家的新一轮(带 input)——传自己的 `childSessionId` 就能跟同一个专家多轮对话。\n * 深度守卫仍靠工具表:专家 agent 的工具表里不放 asTool 类工具。\n */\nimport {\n type BudgetUsagePayload,\n createCoreRegistry,\n defineTool,\n type Event,\n type RunResult,\n readTimeline,\n subagentPause,\n type Tool,\n type ToolContext,\n type ToolResult,\n} from \"@reinsjs/core\"\nimport type { Agent } from \"./create-agent.js\"\n\nexport interface SubagentTask {\n task: string\n}\n\nexport interface AsToolOptions {\n /** 模型看到的工具名,如 ask_analyst */\n name: string\n description: string\n /** 角色名,写进结果 JSON 方便模型与回放辨认 */\n role?: string\n /**\n * 中止传递:linked(缺省)把父的 signal 传给子 run,父停子停;detached 不传,父中止时子做完为止(接力语义)。\n * 循环层保证正在执行的工具跑完、结果落进父日志,父才 paused(host)\n */\n abort?: \"linked\" | \"detached\"\n /** 缺省 low(专家只有只读工具)。专家能改东西就提高,让父侧 approval 模块问人 */\n risk?: \"low\" | \"medium\" | \"high\"\n /** 子会话 id;缺省 `${父 sessionId}:${toolCallId}`。返回已有会话即跟同一个专家继续多轮 */\n childSessionId?: (ctx: ToolContext, input: SubagentTask) => string\n}\n\n/** 子 run 的用量汇总(从子会话时间线算,含续跑之前的部分) */\nexport interface SubagentUsage {\n requests: number\n input: number\n output: number\n cacheRead: number\n cacheWrite: number\n toolCalls: number\n}\n\n/** 写进父 tool_result 的 JSON(模型可见) */\nexport interface SubagentOutcome {\n role?: string\n childSessionId: string\n /** 只会是 done / handoff / error:paused 不落结果而是冒泡 */\n status: RunResult[\"status\"]\n /** 子 run 最后一轮的文本(最后一次工具调用之后的 model_text 拼接) */\n answer?: string\n /** handoff 时的目标会话,error 时的错误 */\n detail?: string\n usage: SubagentUsage\n}\n\nexport const SUBAGENT_TASK_SCHEMA: Record<string, unknown> = {\n type: \"object\",\n properties: {\n task: {\n type: \"string\",\n description:\n \"What you want this expert to do, self-contained: the expert sees none of your conversation.\",\n },\n },\n required: [\"task\"],\n additionalProperties: false,\n}\n\n/** 缺省子会话 id:父会话 + 这次调用,确定性派生 */\nexport function defaultChildSessionId(ctx: ToolContext): string {\n return `${ctx.sessionId}:${ctx.toolCallId}`\n}\n\nexport function asTool(agent: Agent, opts: AsToolOptions): Tool {\n const abort = opts.abort ?? \"linked\"\n const registry = agent.definition.registry ?? createCoreRegistry()\n const log = agent.definition.log\n\n return defineTool<SubagentTask>({\n name: opts.name,\n description: opts.description,\n inputSchema: SUBAGENT_TASK_SCHEMA,\n risk: opts.risk ?? \"low\",\n validate(input) {\n const task = (input as { task?: unknown } | null)?.task\n if (typeof task !== \"string\" || task.trim() === \"\") throw new Error(\"task 必须是非空字符串\")\n return { task }\n },\n async execute(input, ctx) {\n const childSessionId = (opts.childSessionId ?? defaultChildSessionId)(ctx, input)\n // 子日志末条是 run_paused → 这是父续跑补齐 pending:续跑子 run,不再追加 input\n const last = (await log.tail(childSessionId, 1))[0]\n const resuming = last?.type === \"core.run_paused\"\n\n const run = agent.run({\n sessionId: childSessionId,\n ...(resuming ? {} : { input: input.task }),\n // 宿主给别的会话的结论原样下传:子循环把 sessionId 等于自己的拿去校验并记事件,其余再转发给它的工具(多层嵌套)\n ...(ctx.decisions && ctx.decisions.length > 0 ? { decisions: ctx.decisions } : {}),\n ...(ctx.principal ? { principal: ctx.principal } : {}),\n ...(abort === \"linked\" && ctx.signal ? { signal: ctx.signal } : {}),\n })\n\n let answer: string[] = []\n let result: RunResult\n while (true) {\n const step = await run.next()\n if (step.done) {\n result = step.value\n break\n }\n const e = step.value\n switch (e.type) {\n case \"core.budget_usage\":\n // 预算合算:子的每次模型请求都记到父 run 的账上\n ctx.spend?.((e.payload as BudgetUsagePayload).tokens)\n break\n case \"core.tool_call\":\n answer = [] // 答案 = 最后一次工具调用之后的文本\n break\n case \"core.model_text\":\n answer.push((e.payload as { text: string }).text)\n break\n }\n }\n\n if (result.status === \"paused\") {\n // 审批冒泡:不落结果,父 run 整体暂停;宿主处理子的中断后续跑父,父补齐这条 pending 时本工具再进来续跑子\n return subagentPause({\n childSessionId,\n reason: result.reason,\n interruptions: result.interruptions,\n state: result.state,\n })\n }\n\n const outcome: SubagentOutcome = {\n ...(opts.role !== undefined ? { role: opts.role } : {}),\n childSessionId,\n status: result.status,\n usage: usageOf(await readTimeline(log, childSessionId, { registry })),\n }\n let isError = false\n switch (result.status) {\n case \"done\":\n outcome.answer = answer.join(\"\\n\")\n break\n case \"handoff\":\n outcome.answer = answer.join(\"\\n\")\n outcome.detail = `expert handed off to session ${result.toSessionId}; its work continues there`\n break\n case \"error\":\n isError = true\n outcome.detail = `expert failed: ${(result.error.payload as { message?: string }).message ?? \"unknown error\"}`\n break\n }\n const res: ToolResult = { content: [{ type: \"text\", text: JSON.stringify(outcome, null, 2) }], isError }\n return res\n },\n })\n}\n\n/** 子会话时间线里的用量合计:每条 budget_usage 一次请求,tool_call 计工具次数 */\nexport function usageOf(timeline: readonly Event[]): SubagentUsage {\n const usage: SubagentUsage = { requests: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, toolCalls: 0 }\n for (const e of timeline) {\n if (e.type === \"core.budget_usage\") {\n const t = (e.payload as BudgetUsagePayload).tokens\n usage.requests += 1\n usage.input += t.input\n usage.output += t.output\n usage.cacheRead += t.cacheRead ?? 0\n usage.cacheWrite += t.cacheWrite ?? 0\n } else if (e.type === \"core.tool_call\") usage.toolCalls += 1\n }\n return usage\n}\n\n/** 父时间线里,凡 name 在 toolNames 里的 tool_result,其 JSON 里的 childSessionId —— 回放 / 导出用 */\nexport function subagentOutcomesOf(\n events: Iterable<{ type: string; payload: unknown }>,\n toolNames: ReadonlySet<string>,\n): SubagentOutcome[] {\n const found: SubagentOutcome[] = []\n for (const e of events) {\n if (e.type !== \"core.tool_result\") continue\n const p = e.payload as { name: string; content: { type: string; text?: string }[] }\n if (!toolNames.has(p.name)) continue\n const text = p.content.find((c) => c.type === \"text\")?.text\n if (!text) continue\n try {\n const parsed = JSON.parse(text) as Partial<SubagentOutcome>\n if (typeof parsed.childSessionId === \"string\") found.push(parsed as SubagentOutcome)\n } catch {\n // 不是 asTool 写的结果(如入参校验失败的 isError 文本),跳过\n }\n }\n return found\n}\n","/**\n * createAgent:把\"模型 + 工具 + 存储 + 脑子\"装成一个 agent 对象,给出两种用法 ——\n * `handler`(Web 标准 (Request) => Response,缺省 AG-UI 编码)和 `run()`(不经 HTTP 直接跑,脚本 / 队列 / 测试用)。\n *\n * 它不引入新概念:AgentDefinition 就是 runLoop 的跨请求配置,handler 就是 createAgentHandler。\n * 这里只负责把 BoundModel 拆成 model + lowering、把 Stores 拆成 log / blobs / memory。\n */\nimport {\n type ApprovalDecisionInput,\n type BoundModel,\n type ContentPart,\n type Event,\n type EventDraft,\n type LoweringDelta,\n type Principal,\n type RunResult,\n runLoop,\n type SerializedRunState,\n type Stores,\n uuidv7,\n} from \"@reinsjs/core\"\nimport {\n type AgentDefinition,\n type AgentHandler,\n createAgentHandler,\n type HandlerOptions,\n} from \"@reinsjs/server\"\nimport { aguiEncoding } from \"@reinsjs/ui-agui\"\n\nexport interface CreateAgentOptions\n extends Omit<AgentDefinition, \"model\" | \"lowering\" | \"log\" | \"blobs\" | \"memory\"> {\n /** `anthropic(\"claude-opus-5\", { apiKey })` 之类工厂的返回值,或自己组的 { model, lowering } */\n model: BoundModel\n /** `memoryStore()`、`sqliteStores(db)`、`await pgStores(client)`(B9)或自己实现的一套接口;只有 log 必需 */\n store: Stores\n /** 传输层选项;缺省 AG-UI 编码(`encode: aguiEncoding()`),想推原始事件就传 `encode: () => rawEncoder` */\n handler?: HandlerOptions\n}\n\n/** `agent.run()` 的每次参数:与 HTTP 请求体是同一组字段,多了 signal / onDelta / principal 这些进程内才有的 */\nexport interface RunOptions {\n /** 缺省新建会话;结果的 sessionId 告诉你它是什么 */\n sessionId?: string\n input?: string | ContentPart[] | EventDraft\n resume?: SerializedRunState\n decisions?: readonly ApprovalDecisionInput[]\n principal?: Principal\n signal?: AbortSignal\n onDelta?: (delta: LoweringDelta) => void\n}\n\nexport interface Agent {\n /** 跨请求不变的循环配置;要自己起 runLoop 或接别的传输层时用它 */\n readonly definition: AgentDefinition\n /** Web 标准 handler:POST 起 run、GET 补发,见 @reinsjs/server */\n readonly handler: AgentHandler\n /** 不经 HTTP 直接跑一次:yield 每条刚 append 的事件,返回 RunResult 四态 */\n run(options?: RunOptions): AsyncGenerator<Event, RunResult>\n}\n\nexport function createAgent(options: CreateAgentOptions): Agent {\n const { model, store, handler: handlerOptions, ...rest } = options\n const definition: AgentDefinition = {\n ...rest,\n model: model.model,\n lowering: model.lowering,\n log: store.log,\n ...(store.blobs ? { blobs: store.blobs } : {}),\n ...(store.memory ? { memory: store.memory } : {}),\n }\n const handler = createAgentHandler(definition, { encode: aguiEncoding(), ...handlerOptions })\n return {\n definition,\n handler,\n run(runOptions = {}) {\n const { sessionId, ...perRun } = runOptions\n return runLoop({ ...definition, ...stripUndefined(perRun), sessionId: sessionId ?? uuidv7() })\n },\n }\n}\n\n/** exactOptionalPropertyTypes 下不能把 undefined 传给可选字段,这里把没给的键去掉 */\nfunction stripUndefined<T extends object>(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@reinsjs/agent",
3
+ "version": "0.1.0",
4
+ "description": "reins — hand the reins to the model. createAgent() + re-exports of @reinsjs/core, @reinsjs/server and @reinsjs/ui-agui",
5
+ "keywords": [
6
+ "agent",
7
+ "llm",
8
+ "ai",
9
+ "event-sourcing",
10
+ "tool-use",
11
+ "mcp",
12
+ "ag-ui",
13
+ "anthropic",
14
+ "openai"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "NewRate Limited (https://www.adrate.io)",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/AdRate-io/reins.git",
21
+ "directory": "packages/agent"
22
+ },
23
+ "homepage": "https://github.com/AdRate-io/reins/tree/main/packages/agent#readme",
24
+ "bugs": "https://github.com/AdRate-io/reins/issues",
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "dependencies": {
38
+ "@reinsjs/core": "0.1.0",
39
+ "@reinsjs/server": "0.1.0",
40
+ "@reinsjs/ui-agui": "0.1.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup"
47
+ }
48
+ }