spineagent 0.0.1__py3-none-any.whl

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.
spineagent/__init__.py ADDED
@@ -0,0 +1,121 @@
1
+ """spineagent —— 通用多 agent 协作框架(ADR 0001 D1),依赖薄核 corespine。
2
+
3
+ agent / tool / 编排 + MCP / A2A 协议缝。复用 corespine 的缝元模式(Protocol + 离线确定性
4
+ 默认 + Registry 工厂 + 参数化 conformance)、隐私安全 observability 与 env 配置风格;核心
5
+ 默认路径【零网络、零重依赖、离线可跑】,真实协议 SDK 仅经可选 extra 延迟 import。
6
+
7
+ 运行时可把 ragspine 当作一个 Tool / MCP server 在【运行时】组合调用(ADR 0001 D4b),但本包
8
+ 【不】在包层面依赖 ragspine。详见 CLAUDE.md 宪章与家族 ADR 0001。
9
+ """
10
+
11
+ from spineagent.agent.agent import Agent, AgentResult, FunctionAgent, LlmAgent
12
+ from spineagent.agent.as_tool import AgentTool
13
+ from spineagent.agent.function_calling import FunctionCallingAgent
14
+ from spineagent.agent.policy import (
15
+ Action,
16
+ Finish,
17
+ Observation,
18
+ SyntaxToolPolicy,
19
+ ToolCall,
20
+ ToolPolicy,
21
+ tool_policies,
22
+ )
23
+ from spineagent.agent.tool_using import ToolUsingAgent
24
+ from spineagent.conformance import AGENT_INVARIANTS, POLICY_INVARIANTS, TOOL_INVARIANTS
25
+ from spineagent.llm.bedrock_provider import BedrockConverseProvider, load_boto3_sdk
26
+ from spineagent.llm.cohere_provider import CohereProvider, load_cohere_sdk
27
+ from spineagent.llm.gemini_provider import GeminiProvider, load_gemini_sdk
28
+ from spineagent.llm.provider import (
29
+ AnthropicProvider,
30
+ OpenAICompatProvider,
31
+ llm_providers,
32
+ load_anthropic_sdk,
33
+ load_openai_sdk,
34
+ )
35
+ from spineagent.orchestration.chain import ChainAgent
36
+ from spineagent.orchestration.coordinator import Coordinator
37
+ from spineagent.protocol.a2a.seam import (
38
+ A2AAgent,
39
+ A2AAgentAdapter,
40
+ A2AResult,
41
+ A2ATask,
42
+ OfflineA2AStub,
43
+ a2a_agents,
44
+ load_a2a_sdk,
45
+ )
46
+ from spineagent.protocol.mcp.seam import (
47
+ McpClient,
48
+ McpClientTool,
49
+ McpServer,
50
+ McpTool,
51
+ OfflineMcpStub,
52
+ load_mcp_sdk,
53
+ mcp_clients,
54
+ )
55
+ from spineagent.tools.function_tool import FunctionTool, function_tool
56
+ from spineagent.tools.tool import CalcTool, EchoTool, Tool, ToolResult, tool_registry
57
+
58
+ __version__ = "0.0.1"
59
+
60
+ __all__ = [
61
+ # agent
62
+ "Agent",
63
+ "AgentResult",
64
+ "LlmAgent",
65
+ "FunctionAgent",
66
+ "ToolUsingAgent",
67
+ "AgentTool",
68
+ "FunctionCallingAgent",
69
+ # tool-policy 缝(会用工具的 agent 的「大脑」)
70
+ "ToolPolicy",
71
+ "ToolCall",
72
+ "Finish",
73
+ "Action",
74
+ "Observation",
75
+ "SyntaxToolPolicy",
76
+ "tool_policies",
77
+ # tools
78
+ "Tool",
79
+ "ToolResult",
80
+ "EchoTool",
81
+ "CalcTool",
82
+ "tool_registry",
83
+ "FunctionTool",
84
+ "function_tool",
85
+ # orchestration
86
+ "Coordinator",
87
+ "ChainAgent",
88
+ # llm provider 适配器(挂在 corespine LLMProvider 缝后面;输出统一 OpenAI ChatCompletion)
89
+ "AnthropicProvider",
90
+ "OpenAICompatProvider",
91
+ "CohereProvider",
92
+ "GeminiProvider",
93
+ "BedrockConverseProvider",
94
+ "llm_providers",
95
+ "load_anthropic_sdk",
96
+ "load_openai_sdk",
97
+ "load_cohere_sdk",
98
+ "load_gemini_sdk",
99
+ "load_boto3_sdk",
100
+ # protocol: mcp
101
+ "McpClient",
102
+ "McpServer",
103
+ "McpTool",
104
+ "McpClientTool",
105
+ "OfflineMcpStub",
106
+ "mcp_clients",
107
+ "load_mcp_sdk",
108
+ # protocol: a2a
109
+ "A2AAgent",
110
+ "A2ATask",
111
+ "A2AResult",
112
+ "A2AAgentAdapter",
113
+ "OfflineA2AStub",
114
+ "a2a_agents",
115
+ "load_a2a_sdk",
116
+ # conformance (本包绑定的不变量)
117
+ "AGENT_INVARIANTS",
118
+ "TOOL_INVARIANTS",
119
+ "POLICY_INVARIANTS",
120
+ "__version__",
121
+ ]
@@ -0,0 +1 @@
1
+ """spineagent.agent —— Agent 协议 + 最小默认实现(LlmAgent / FunctionAgent)。"""
@@ -0,0 +1,122 @@
1
+ """agent 缝:Agent 协议 + 最小默认实现(单步执行)。
2
+
3
+ 家族缝的元模式:Protocol + 离线确定性默认 + 隐私安全 trace。Agent 是 spineagent 最小
4
+ 的执行单元——给一个任务,跑【一步】拿回结果。两个默认实现都离线可跑、零网络:
5
+
6
+ - LlmAgent —— 用一个 corespine LLMProvider 跑单步(离线用 MockProvider,确定性、可复现);
7
+ - FunctionAgent —— 把一个纯函数 (task->text) 包成 Agent(无需 LLM,做测试/编排的轻量节点)。
8
+
9
+ 隐私约定:step 可选接收一个 corespine TraceSink,实现只允许往里记【元数据】(agent 名、
10
+ 长度、token 数),【绝不】记任务/输出正文——由 InProcessPrivacyTraceSink「构造即保证」,
11
+ 本包再用 conformance 把这条不变量绑死(见 spineagent/conformance.py)。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass
18
+ from typing import Protocol, runtime_checkable
19
+
20
+ from corespine.llm.provider import LLMProvider
21
+ from corespine.observability.trace import TraceSink
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class AgentResult:
26
+ """一次 agent 步的结果:产出文本 + 来源 agent 名(provenance)+ 可选 token 用量 / 错误。
27
+
28
+ error 仅在【编排层弹性模式】下捕获 agent.step 异常时填充——归一为家族统一的可序列化错误
29
+ dict(corespine.errors.error_to_dict:含 code / retryable / context)。正常成功路径 error 为
30
+ None;agent.step 自身的契约仍是「成功产出非空、失败抛异常」,捕获与否是 Coordinator 的策略。
31
+ """
32
+
33
+ agent: str
34
+ output: str
35
+ usage: dict[str, int] | None = None
36
+ error: dict[str, object] | None = None
37
+
38
+ @property
39
+ def ok(self) -> bool:
40
+ """这步是否成功(未捕获到错误)。"""
41
+ return self.error is None
42
+
43
+
44
+ @runtime_checkable
45
+ class Agent(Protocol):
46
+ """agent 协议:有名字;给一个任务,跑【一步】拿回结果。
47
+
48
+ step 可选接收一个 TraceSink:实现只允许往里记元数据(code/计数/耗时),绝不记任务/
49
+ 输出正文——隐私 by construction,由 corespine 的 InProcessPrivacyTraceSink 兜底。
50
+ """
51
+
52
+ name: str
53
+
54
+ def step(self, task: str, *, trace: TraceSink | None = None) -> AgentResult: ...
55
+
56
+
57
+ class LlmAgent:
58
+ """最小默认 agent:用一个 corespine LLMProvider 跑单步(离线用 MockProvider)。"""
59
+
60
+ def __init__(self, name: str, provider: LLMProvider, *, system: str = "") -> None:
61
+ self._name = name
62
+ self._provider = provider
63
+ self._system = system
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return self._name
68
+
69
+ def step(self, task: str, *, trace: TraceSink | None = None) -> AgentResult:
70
+ messages: list[dict[str, str]] = [{"role": "user", "content": task}]
71
+ if self._system:
72
+ messages.insert(0, {"role": "system", "content": self._system})
73
+ completion = self._provider.chat(messages)
74
+ message = completion.choices[0].message
75
+ usage = (
76
+ {
77
+ "prompt_tokens": completion.usage.prompt_tokens,
78
+ "completion_tokens": completion.usage.completion_tokens,
79
+ "total_tokens": completion.usage.total_tokens,
80
+ }
81
+ if completion.usage is not None
82
+ else None
83
+ )
84
+ result = AgentResult(agent=self._name, output=message.content or "", usage=usage)
85
+ _emit_step(trace, self._name, task, result)
86
+ return result
87
+
88
+
89
+ class FunctionAgent:
90
+ """最小确定性 agent:把一个纯函数 (task->text) 包成 Agent(离线/测试/编排用,无需 LLM)。"""
91
+
92
+ def __init__(self, name: str, fn: Callable[[str], str]) -> None:
93
+ self._name = name
94
+ self._fn = fn
95
+
96
+ @property
97
+ def name(self) -> str:
98
+ return self._name
99
+
100
+ def step(self, task: str, *, trace: TraceSink | None = None) -> AgentResult:
101
+ result = AgentResult(agent=self._name, output=self._fn(task))
102
+ _emit_step(trace, self._name, task, result)
103
+ return result
104
+
105
+
106
+ def _emit_step(
107
+ trace: TraceSink | None, name: str, task: str, result: AgentResult
108
+ ) -> None:
109
+ """记一条隐私安全的步级 trace:只记 agent 名 + 长度 + token 数,绝不记正文。"""
110
+ if trace is None:
111
+ return
112
+ usage = result.usage or {}
113
+ # trace 字段名沿用 input/output_tokens(隐私元数据词表);取值兼容 OpenAI usage 的 prompt/
114
+ # completion_tokens 与旧式 input/output_tokens 两种键。
115
+ trace.emit(
116
+ "agent_step",
117
+ agent=name,
118
+ task_chars=len(task),
119
+ output_chars=len(result.output),
120
+ input_tokens=usage.get("prompt_tokens", usage.get("input_tokens", 0)),
121
+ output_tokens=usage.get("completion_tokens", usage.get("output_tokens", 0)),
122
+ )
@@ -0,0 +1,30 @@
1
+ """把一个 Agent 暴露成 Tool:让一个 agent 把另一个 agent 当工具调用(分层 / 督导式多 agent)。
2
+
3
+ AgentTool 是把现有原语组合出【分层多 agent】的关键一块:一个督导 agent(ToolUsingAgent)通过
4
+ 工具调用把子任务派给专精的子 agent——子 agent 也可以自己再用工具、再带子 agent,层层嵌套。
5
+ 它与 McpClientTool(外部 MCP 工具 → Tool)、A2AAgentAdapter(远端 A2A agent → Agent)构成完整
6
+ 的桥接三角:本地 agent → Tool。
7
+
8
+ run(arg) 即对子 agent 跑一步、取其输出包成带 provenance 的 ToolResult(tool = 工具名,默认取
9
+ 子 agent 名,可溯源到产出它的子 agent)。最薄桥接:只搬运文本(子 agent 的 usage / error 不
10
+ 透传);子 agent 抛异常照常上抛,与其它 Tool 一致——错误处理归编排层 / 调用方(见 Coordinator
11
+ 弹性容错)。Tool 协议的 run 无 trace 形参,故 AgentTool 自身不发 trace,外层 agent 循环会为这次
12
+ 工具调用发一条隐私安全的 tool_step(见 agent/tool_using.py)。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from spineagent.agent.agent import Agent
18
+ from spineagent.tools.tool import ToolResult
19
+
20
+
21
+ class AgentTool:
22
+ """跨原语适配器:把一个 Agent 桥成 Tool(实现 Tool 协议),用于分层 / 督导式多 agent。"""
23
+
24
+ def __init__(self, agent: Agent, *, name: str | None = None) -> None:
25
+ self._agent = agent
26
+ self.name = name or agent.name
27
+
28
+ def run(self, arg: str) -> ToolResult:
29
+ result = self._agent.step(arg)
30
+ return ToolResult(tool=self.name, output=result.output)
@@ -0,0 +1,129 @@
1
+ """真 function-calling 的 agent:用一个 chat 模型的【原生工具调用】跑多步循环。
2
+
3
+ 与离线确定性的 ToolUsingAgent(SyntaxToolPolicy 按语法路由)不同,FunctionCallingAgent 让【真 LLM】
4
+ 自己决定调哪个工具:把 FunctionTool 的 schema 喂给 model.chat(tools=...),模型回 tool_calls →
5
+ 按名执行工具 → 把结果以 OpenAI tool 角色消息喂回 → 再 chat,直到模型不再要工具(出文本)或触顶
6
+ max_steps。它实现 Agent 协议,故可直接进 Coordinator / 被 AgentTool 当工具 / 套进 ChainAgent。
7
+
8
+ 对外只认 corespine 的 OpenAI-canonical chat 缝(ChatCompletion + OpenAI message dicts),所以底层换
9
+ 任意 provider(OpenAI 兼容 / Anthropic / Gemini / Bedrock / Cohere)都不改这里一行——「统一 invoke」。
10
+ 离线默认 MockProvider 不回 tool_calls,故它直接出文本(诚实:离线不假装会 function-calling)。
11
+
12
+ 隐私:每步发 tool_step(agent / 步序 / 工具名 / 入参长度 / 输出长度)、收尾发 agent_finish、触顶发
13
+ agent_step_limit——只记 code / 计数,绝不记任务 / 参数 / 输出正文。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from collections.abc import Iterable
20
+ from typing import Any
21
+
22
+ from corespine.llm.provider import LLMProvider
23
+ from corespine.observability.trace import TraceSink
24
+
25
+ from spineagent.agent.agent import AgentResult
26
+ from spineagent.tools.function_tool import FunctionTool
27
+
28
+ # 触顶 max_steps 仍未出最终文本时的兜底文案(保证产出非空)。
29
+ _NO_OUTPUT = "(reached max_steps without a final answer)"
30
+
31
+
32
+ class FunctionCallingAgent:
33
+ """用真 LLM 的原生 function-calling 在单次 step() 内多步调用工具的 agent(实现 Agent 协议)。"""
34
+
35
+ def __init__(
36
+ self,
37
+ name: str,
38
+ model: LLMProvider,
39
+ tools: Iterable[FunctionTool],
40
+ *,
41
+ system: str = "",
42
+ max_steps: int = 8,
43
+ ) -> None:
44
+ self._name = name
45
+ self._model = model
46
+ self._tools = {tool.name: tool for tool in tools}
47
+ self._system = system
48
+ self._max_steps = max_steps
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ return self._name
53
+
54
+ def step(self, task: str, *, trace: TraceSink | None = None) -> AgentResult:
55
+ messages: list[dict[str, Any]] = [{"role": "user", "content": task}]
56
+ if self._system:
57
+ messages.insert(0, {"role": "system", "content": self._system})
58
+ schemas = [tool.schema() for tool in self._tools.values()] or None
59
+ last_usage: dict[str, int] | None = None
60
+ for index in range(self._max_steps):
61
+ result = self._model.chat(messages, tools=schemas)
62
+ last_usage = _usage_dict(result.usage)
63
+ message = result.choices[0].message
64
+ tool_calls = message.tool_calls or ()
65
+ if not tool_calls:
66
+ _emit_finish(trace, self._name, index, message.content or "")
67
+ return AgentResult(self._name, message.content or "", usage=last_usage)
68
+ # 把这一轮的 assistant(带 tool_calls)按 OpenAI 形状追加进对话历史。
69
+ messages.append(
70
+ {
71
+ "role": "assistant",
72
+ "content": message.content,
73
+ "tool_calls": [
74
+ {
75
+ "id": tc.id,
76
+ "type": "function",
77
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments},
78
+ }
79
+ for tc in tool_calls
80
+ ],
81
+ }
82
+ )
83
+ # 逐个执行工具,把结果以 tool 角色消息喂回(tool_call_id 对齐)。
84
+ for tc in tool_calls:
85
+ tool = self._tools.get(tc.function.name)
86
+ arguments = tc.function.arguments or "{}"
87
+ if tool is None:
88
+ output = f"error: unknown tool {tc.function.name!r}"
89
+ else:
90
+ output = tool.invoke(json.loads(arguments))
91
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": output})
92
+ _emit_tool_step(trace, self._name, index, tc.function.name, arguments, output)
93
+ # 触顶 max_steps 仍在要工具:强制收尾(兜底非空)。
94
+ _emit_step_limit(trace, self._name, self._max_steps)
95
+ _emit_finish(trace, self._name, self._max_steps, _NO_OUTPUT)
96
+ return AgentResult(self._name, _NO_OUTPUT, usage=last_usage)
97
+
98
+
99
+ def _usage_dict(usage: Any) -> dict[str, int] | None:
100
+ if usage is None:
101
+ return None
102
+ return {
103
+ "prompt_tokens": usage.prompt_tokens,
104
+ "completion_tokens": usage.completion_tokens,
105
+ "total_tokens": usage.total_tokens,
106
+ }
107
+
108
+
109
+ def _emit_tool_step(
110
+ trace: TraceSink | None, name: str, step: int, tool: str, arguments: str, output: str
111
+ ) -> None:
112
+ """隐私安全步级 trace:agent 名 / 步序 / 工具名 / 入参与输出长度,绝不记正文。"""
113
+ if trace is None:
114
+ return
115
+ trace.emit(
116
+ "tool_step", agent=name, step=step, tool=tool, arg_chars=len(arguments), output_chars=len(output)
117
+ )
118
+
119
+
120
+ def _emit_finish(trace: TraceSink | None, name: str, steps: int, answer: str) -> None:
121
+ if trace is None:
122
+ return
123
+ trace.emit("agent_finish", agent=name, steps=steps, answer_chars=len(answer))
124
+
125
+
126
+ def _emit_step_limit(trace: TraceSink | None, name: str, max_steps: int) -> None:
127
+ if trace is None:
128
+ return
129
+ trace.emit("agent_step_limit", agent=name, max_steps=max_steps)
@@ -0,0 +1,121 @@
1
+ """tool-policy 缝:ToolPolicy 协议 + 离线确定性默认(决定 agent 下一步调哪个工具)。
2
+
3
+ 家族缝的元模式(同 mcp / a2a 缝):Protocol + 离线确定性默认 + Registry 工厂 + 参数化
4
+ conformance。一个 ToolPolicy 是「会用工具的 agent」的大脑:给一个任务、当前可用的工具名
5
+ 集合、以及已执行步的观测历史,定下一个动作——【调某个工具】或【收尾给最终答案】。
6
+
7
+ 【离线默认为何不靠 LLM 推理(诚实性取舍)】corespine 的 MockProvider 只做「回声 + sha256
8
+ 指纹」,对任何 prompt 都吐 `[mock:<hex>] <prompt>`,绝不可能推理出 `{tool: calc, arg: 1+1}`。
9
+ 任何「解析 LLM 输出找 tool call」的默认实现,在离线下要么永远解析失败、空转到 max_steps,
10
+ 要么把 mock 输出硬塞进解析器自欺。所以离线默认【绝不依赖 LLM 选工具】——正如 MockProvider
11
+ 用回声诚实地「不假装生成内容」,SyntaxToolPolicy 把「工具调用意图」显式化为任务文本里的
12
+ 确定性语法,纯函数解析。它是这条缝的【确定性参照实现】;真实推理式 policy(走 corespine
13
+ 真 provider 解析 function-calling)应注册进 tool_policies 的 'llm' 位,与 mcp/a2a 的
14
+ offline/real 二分完全同构。
15
+
16
+ decide 是【无状态纯函数】:循环状态全由入参 history 携带,同一 (task, tools, history) 恒定
17
+ 产出同一 Action——可断言、可复现、零网络,也便于将来直接映射到一次 stateless completion。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from typing import Any, Protocol, TypeAlias, runtime_checkable
24
+
25
+ from corespine.errors import SeamError
26
+ from corespine.seam.registry import Registry
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ToolCall:
31
+ """决定:调一个工具(tool 名 + 传给它的参数)。arg 中字面量 $prev 由 agent 侧替换为上一步观测。"""
32
+
33
+ tool: str
34
+ arg: str
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Finish:
39
+ """决定:收尾,给出最终答案(保证非空)。"""
40
+
41
+ answer: str
42
+
43
+
44
+ # 一个决策动作:调工具,或收尾。isinstance 分发(PEP 604 联合)。
45
+ Action: TypeAlias = ToolCall | Finish
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Observation:
50
+ """一步执行的观测:产出它的工具名 + 实际入参 + 输出。喂回循环,供 $prev 链式消费与收尾拼接。"""
51
+
52
+ tool: str
53
+ arg: str
54
+ output: str
55
+
56
+
57
+ @runtime_checkable
58
+ class ToolPolicy(Protocol):
59
+ """tool-policy 协议:给 task + 可用工具名集 + 历史观测,决定下一个动作(调工具 / 收尾)。"""
60
+
61
+ def decide(
62
+ self, task: str, *, tools: tuple[str, ...], history: tuple[Observation, ...]
63
+ ) -> Action: ...
64
+
65
+
66
+ def _parse_instruction(line: str, tools: tuple[str, ...]) -> tuple[str, str] | None:
67
+ """把一行解析成 (工具名, 参数):形如 `<tool>: <arg>` 且工具名在 tools 内才算指令,否则视为正文。"""
68
+ if ":" not in line:
69
+ return None
70
+ name, _, arg = line.partition(":")
71
+ name = name.strip()
72
+ if name in tools:
73
+ return name, arg.strip()
74
+ return None
75
+
76
+
77
+ class SyntaxToolPolicy:
78
+ """离线确定性默认:按任务文本里的 `<tool>: <arg>` 显式语法 + 工具名集合,确定性路由工具调用。
79
+
80
+ 无状态纯函数:游标 = len(history) 表示「已执行到第几条工具指令」。第 cursor 条工具指令尚存
81
+ 则返回 ToolCall(该行工具名, 该行参数);工具指令耗尽则返回 Finish(把非指令正文行 + 最后一步
82
+ 观测按固定模板拼成最终答案,保证非空)。同一输入恒定同一输出。
83
+ """
84
+
85
+ def decide(
86
+ self, task: str, *, tools: tuple[str, ...], history: tuple[Observation, ...]
87
+ ) -> Action:
88
+ # 单遍把每行分流:能解析成工具指令的入 instructions,其余非空行入 prose 正文。
89
+ instructions: list[tuple[str, str]] = []
90
+ prose: list[str] = []
91
+ for line in task.splitlines():
92
+ parsed = _parse_instruction(line, tools)
93
+ if parsed is not None:
94
+ instructions.append(parsed)
95
+ elif stripped := line.strip():
96
+ prose.append(stripped)
97
+ cursor = len(history)
98
+ if cursor < len(instructions):
99
+ name, arg = instructions[cursor]
100
+ return ToolCall(tool=name, arg=arg)
101
+ # 工具指令耗尽 -> 收尾:非指令正文行 + 最后一步观测输出,拼成非空答案。
102
+ parts = prose + ([history[-1].output] if history else [])
103
+ answer = "\n".join(p for p in parts if p) or task.strip() or "(no output)"
104
+ return Finish(answer=answer)
105
+
106
+
107
+ def _make_llm_policy(**kwargs: Any) -> ToolPolicy:
108
+ # 真实推理式 policy 的占位(与 mcp/a2a 的 'real' 同构)。不同点:LLM policy 走 corespine
109
+ # LLMProvider 真后端,无网络 SDK 可延迟 import,故直接给「缝未接入」的明确报错。
110
+ # 用家族统一 SeamError(code="seam.unknown"):任何「缝槽存在但真实实现未接入」都同一形状。
111
+ raise SeamError(
112
+ "真实 LLM 推理式 ToolPolicy 留待接入:应走 corespine LLMProvider 真后端解析 "
113
+ "function-calling / 工具调用,并注册进 tool_policies 的 'llm' 位;"
114
+ "本壳只提供缝 + 离线确定性默认 SyntaxToolPolicy。"
115
+ )
116
+
117
+
118
+ # 缝注册表:一个 spec 选实现(默认 offline 离线确定性默认;llm 走真实推理式接入)。
119
+ tool_policies: Registry[ToolPolicy] = Registry("tool_policy")
120
+ tool_policies.register("offline", lambda **kw: SyntaxToolPolicy(**kw))
121
+ tool_policies.register("llm", _make_llm_policy)
@@ -0,0 +1,107 @@
1
+ """会用工具的多步 agent:在一次 step() 内跑「决策→调工具→喂回观测→再决策」的循环。
2
+
3
+ ToolUsingAgent 实现现有 Agent 协议(name + step),因此【零改 Coordinator】即可进顺序 / 并行
4
+ 编排。一次 step() 内:用一个 ToolPolicy 决定下一个动作——调某工具则按名取 Tool 执行、把观测
5
+ 追加进历史(供下一步 $prev 链式消费)、发一条隐私安全步级 trace;收尾则返回最终 AgentResult。
6
+
7
+ max_steps 守卫:其语义是【最多调用多少次工具】(收尾决策本身不占步预算)。已用满 max_steps
8
+ 次工具调用后,policy 若还想再调工具,则强制收尾——即便 policy 异常永不返回 Finish,也绝不
9
+ 死循环(history 每步单调增长,必在 max_steps 内触顶)。
10
+
11
+ $prev:工具参数里字面量 `$prev` 在【执行前】替换为上一步观测的输出(history 为空时替换为空
12
+ 串),让「把观测喂回循环」名副其实。替换只发生在内存里,trace 只记其长度,绝不写进正文。
13
+ 注意:若【首步】即引用 $prev(尚无上一步输出),替换为空串后,余下参数能否被工具处理由工具
14
+ 自身决定;工具若拒绝该参数(如 CalcTool 对空串抛错),异常照常上抛——错误处理 / 重试不在本
15
+ 增量范围(rule of three),调用方自行处理。
16
+
17
+ 隐私 trace:每步发 tool_step(agent / 步序 / 工具名 / 入参长度 / 输出长度),收尾发 agent_finish
18
+ (agent / 总步数 / 答案长度),触顶发 agent_step_limit——字段全为 code / 计数 / 序号,键名全程
19
+ 规避 corespine FORBIDDEN_KEYS,绝不携带 task / arg / output / answer 正文。
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Iterable
25
+
26
+ from corespine.observability.trace import TraceSink
27
+
28
+ from spineagent.agent.agent import AgentResult
29
+ from spineagent.agent.policy import Finish, Observation, ToolPolicy
30
+ from spineagent.tools.tool import Tool
31
+
32
+ # 触顶 max_steps 又无任何观测可作答时的固定兜底文案(保证产出非空)。
33
+ _NO_OUTPUT = "(reached max_steps without finishing)"
34
+
35
+
36
+ class ToolUsingAgent:
37
+ """用一个 ToolPolicy 驱动、在单次 step() 内多步调用工具的 agent(实现 Agent 协议)。"""
38
+
39
+ def __init__(
40
+ self,
41
+ name: str,
42
+ policy: ToolPolicy,
43
+ tools: Iterable[Tool],
44
+ *,
45
+ max_steps: int = 8,
46
+ ) -> None:
47
+ self._name = name
48
+ self._policy = policy
49
+ self._tools = {tool.name: tool for tool in tools}
50
+ # 工具名集合(传给 policy 用以避免幻觉一个不存在的工具);顺序 = 插入序。
51
+ self._tool_names = tuple(self._tools)
52
+ self._max_steps = max_steps
53
+
54
+ @property
55
+ def name(self) -> str:
56
+ return self._name
57
+
58
+ def step(self, task: str, *, trace: TraceSink | None = None) -> AgentResult:
59
+ history: list[Observation] = []
60
+ while True:
61
+ action = self._policy.decide(
62
+ task, tools=self._tool_names, history=tuple(history)
63
+ )
64
+ if isinstance(action, Finish):
65
+ _emit_finish(trace, self._name, len(history), action.answer)
66
+ return AgentResult(agent=self._name, output=action.answer)
67
+ # ToolCall:已用满 max_steps 次工具调用,policy 还想再调 -> 触顶强制收尾。
68
+ if len(history) >= self._max_steps:
69
+ answer = (history[-1].output if history else "") or _NO_OUTPUT
70
+ _emit_step_limit(trace, self._name, self._max_steps)
71
+ _emit_finish(trace, self._name, len(history), answer)
72
+ return AgentResult(agent=self._name, output=answer)
73
+ # 把 $prev 替换为上一步观测输出后执行该工具,观测追加进历史。
74
+ arg = action.arg.replace("$prev", history[-1].output if history else "")
75
+ result = self._tools[action.tool].run(arg)
76
+ history.append(Observation(tool=action.tool, arg=arg, output=result.output))
77
+ _emit_tool_step(trace, self._name, len(history) - 1, action.tool, arg, result.output)
78
+
79
+
80
+ def _emit_tool_step(
81
+ trace: TraceSink | None, name: str, step: int, tool: str, arg: str, output: str
82
+ ) -> None:
83
+ """记一条隐私安全的步级 trace:只记 agent 名 / 步序 / 工具名 / 入参与输出长度,绝不记正文。"""
84
+ if trace is None:
85
+ return
86
+ trace.emit(
87
+ "tool_step",
88
+ agent=name,
89
+ step=step,
90
+ tool=tool,
91
+ arg_chars=len(arg),
92
+ output_chars=len(output),
93
+ )
94
+
95
+
96
+ def _emit_finish(trace: TraceSink | None, name: str, steps: int, answer: str) -> None:
97
+ """记收尾 trace:agent 名 / 总步数 / 答案长度(answer_chars,绝不用 answer 键携带正文)。"""
98
+ if trace is None:
99
+ return
100
+ trace.emit("agent_finish", agent=name, steps=steps, answer_chars=len(answer))
101
+
102
+
103
+ def _emit_step_limit(trace: TraceSink | None, name: str, max_steps: int) -> None:
104
+ """记触顶 trace:便于排障「为何提前收尾」。"""
105
+ if trace is None:
106
+ return
107
+ trace.emit("agent_step_limit", agent=name, max_steps=max_steps)