Align execution errors with the shared catalog and add optional logs.
Contract codes now use the agent.* values, and runtime can emit structured logs without changing results when no logger is configured. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
[submodule "deps/development-guidelines"]
|
||||
path = deps/development-guidelines
|
||||
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Development-Guidelines.git
|
||||
[submodule "deps/error-code-system"]
|
||||
path = deps/error-code-system
|
||||
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Error-Code-System.git
|
||||
[submodule "deps/logging-system"]
|
||||
path = deps/logging-system
|
||||
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Logging-System.git
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
| 逻辑 ID | 路径 | 角色 |
|
||||
|---|---|---|
|
||||
| development-guidelines | `deps/development-guidelines` | development-only,规范文本 |
|
||||
| error-code-system | `deps/error-code-system` | build-required(接入后) |
|
||||
| logging-system | `deps/logging-system` | build-required(接入后) |
|
||||
| error-code-system | `deps/error-code-system` | build-required,规范码目录 |
|
||||
| logging-system | `deps/logging-system` | build-required,可选日志器 |
|
||||
|
||||
克隆:
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
| 层 | 职责 | 依赖边界 |
|
||||
|---|---|---|
|
||||
| contract | 执行契约:Request/Result/Outcome/Error/Capability/Backend/Resolver | 不依赖内核、产品类型、数据库、配置 |
|
||||
| contract | 执行契约:Request/Result/Outcome/Error/Capability/Backend/Resolver | 不依赖内核、产品类型、数据库、配置,也不导入错误码库 |
|
||||
| exec | 执行端口:Backend/Request/Result/ProcessEvent、步骤重试与失败分类、交付物物化、Stub、并发限流 | 面向调用方的执行端口 |
|
||||
| runtime | 多轮循环、工具调度、模型调用、执行会话 | 实现 contract;不导入 Core/Squad |
|
||||
| runtime | 多轮循环、工具调度、模型调用、执行会话 | 不导入 Core/Squad;可选使用日志库,nil 日志器不改变结果 |
|
||||
| adapters | PI Runner transport / typed-result-v1;native:自研内核 runtime → exec.Backend(规范后端名 agent-native,含会话毒化轮转——resume-unsafe 失败后自动换新会话) | 显式映射外部差异,不伪造缺失能力 |
|
||||
| conformance | 所有 Backend 共用的行为验收 | fake 验证契约,真实 adapter 验证能力 |
|
||||
|
||||
@@ -37,6 +37,22 @@
|
||||
- 失败副作用显式分类(none/possible);possible 禁止无条件重放。
|
||||
- 重试单一责任层:步骤级归调用方,传输级有界归 adapter,turn 上限归 runtime。
|
||||
- Workspace 路径授权 ≠ OS 沙箱。
|
||||
- `contract.ErrorCode` 取值是 `agent.*` 规范码。`ErrorCode()` / `ErrorFields()` 供日志库识别,本包不导入错误码库。
|
||||
|
||||
## 依赖子模块
|
||||
|
||||
| 路径 | 作用 |
|
||||
|---|---|
|
||||
| `deps/development-guidelines` | 开发规范,不参与编译 |
|
||||
| `deps/error-code-system` | 规范码目录。`go.mod` 的 `replace` 指向这里 |
|
||||
| `deps/logging-system` | 结构化日志。runtime 与 native 适配器可选使用 |
|
||||
|
||||
版本以本仓 gitlink 为准。`scripts/submodule-consistency.sh` 核对 gitlink 与检出的 HEAD。
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-Native.git
|
||||
# 已克隆时:git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## CodeGraph
|
||||
|
||||
@@ -58,9 +74,11 @@ go test ./...
|
||||
```
|
||||
require agent-native v0.0.0
|
||||
|
||||
replace agent-native => ../Agent-Native
|
||||
replace agent-native => ./deps/agent-native
|
||||
```
|
||||
|
||||
本仓自己的 `go.mod` 则把错误码库和日志库替换到 `./deps/`。消费方若同时依赖这三仓,必须在消费方 `go.mod` 里写自己的 `replace`:依赖模块内部的 `replace` 不会在上层构建中生效。
|
||||
|
||||
远程:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-Native.git`
|
||||
|
||||
## CI
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"agent-native/runtime/llm"
|
||||
"agent-native/runtime/session"
|
||||
"agent-native/runtime/tools"
|
||||
|
||||
"logging-system/logsys"
|
||||
)
|
||||
|
||||
// Options 构造后端所需的模型接入与执行参数(由组合根注入,无产品依赖)。
|
||||
@@ -28,6 +30,10 @@ type Options struct {
|
||||
Timeout time.Duration
|
||||
// Stream 测试注入的流函数;缺省按 APIBase/APIKey 构造 llm.Client。
|
||||
Stream llm.StreamFunc
|
||||
// Log 可选。nil 时丢弃日志。
|
||||
Log *logsys.Logger
|
||||
// ExecutionID 写入日志,不参与执行控制。
|
||||
ExecutionID string
|
||||
}
|
||||
|
||||
// Backend 以自研内核实现 exec.Backend。
|
||||
@@ -35,7 +41,7 @@ type Backend struct {
|
||||
opts Options
|
||||
// lastFailure 记录各工作目录上次失败原因,用于会话毒化检测
|
||||
// (context_overflow 等错误续跑同一会话只会复现失败,须换新会话)。
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
lastFailure map[string]string
|
||||
}
|
||||
|
||||
@@ -135,6 +141,14 @@ func (b *Backend) Execute(ctx context.Context, req exec.Request) (*exec.Result,
|
||||
b.mu.Lock()
|
||||
b.lastFailure[workDir] = reason
|
||||
b.mu.Unlock()
|
||||
assessment := exec.AssessStepFailure(exec.StepFailureSignals{
|
||||
Err: runRes.Err,
|
||||
WorkDir: workDir,
|
||||
Result: &exec.Result{ErrorReason: reason, Retryable: runRes.Retryable, Logs: logs.String()},
|
||||
})
|
||||
b.logger().Error("native step failure classified", runRes.Err,
|
||||
"failure_class", assessment.Class.String(),
|
||||
"work_dir", workDir)
|
||||
res := &exec.Result{Success: false, ErrorReason: reason, Logs: logs.String(), Retryable: runRes.Retryable}
|
||||
if runRes.StopReason == llm.StopAborted {
|
||||
res.Retryable = true
|
||||
@@ -170,6 +184,8 @@ func (b *Backend) run(ctx context.Context, reg *tools.Registry, store *session.S
|
||||
Stream: stream,
|
||||
Session: store,
|
||||
MaxTurns: b.opts.MaxTurns,
|
||||
Log: b.logger(),
|
||||
ExecutionID: b.opts.ExecutionID,
|
||||
OnEvent: func(ev runtime.Event) {
|
||||
logs.WriteString(ev.Type)
|
||||
if ev.Tool != "" {
|
||||
@@ -224,10 +240,22 @@ func (b *Backend) rotateIfPoisoned(workDir string) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return
|
||||
}
|
||||
b.logger().Warn("rotating poisoned session", "work_dir", workDir)
|
||||
_ = os.Rename(path, filepath.Join(workDir, ".agentx",
|
||||
"transcript.poisoned."+time.Now().UTC().Format("20060102T150405.000000000")+".jsonl"))
|
||||
}
|
||||
|
||||
func (b *Backend) logger() *logsys.Logger {
|
||||
if b == nil || b.opts.Log == nil {
|
||||
return logsys.Nop()
|
||||
}
|
||||
log := b.opts.Log.Module("native")
|
||||
if b.opts.ExecutionID == "" {
|
||||
return log
|
||||
}
|
||||
return log.With("execution_id", b.opts.ExecutionID)
|
||||
}
|
||||
|
||||
// systemPrompt 组装系统提示:角色指令 + 交付物与工具档位约束。
|
||||
func systemPrompt(req exec.Request, profile string) string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type remoteRunWire struct {
|
||||
OK bool `json:"ok"`
|
||||
RunID string `json:"run_id"`
|
||||
FallbackReason string `json:"fallback_reason"`
|
||||
Retryable bool `json:"retryable"`
|
||||
RunnerProtocol string `json:"runner_protocol"`
|
||||
OK bool `json:"ok"`
|
||||
RunID string `json:"run_id"`
|
||||
FallbackReason string `json:"fallback_reason"`
|
||||
Retryable bool `json:"retryable"`
|
||||
RunnerProtocol string `json:"runner_protocol"`
|
||||
Capabilities *struct {
|
||||
TypedResult bool `json:"typed_result"`
|
||||
} `json:"capabilities"`
|
||||
@@ -23,7 +23,7 @@ type remoteRunWire struct {
|
||||
}
|
||||
|
||||
type decodedRemote struct {
|
||||
OK bool
|
||||
OK bool
|
||||
Retryable bool
|
||||
Reason string
|
||||
Incoming map[string][]byte
|
||||
@@ -91,17 +91,17 @@ func safeLegacyReason(reason string) string {
|
||||
case r == "":
|
||||
return "remote pi-agent reported task failure"
|
||||
case strings.EqualFold(r, "prompt_empty"):
|
||||
return "PROMPT_EMPTY"
|
||||
return "agent.prompt_empty"
|
||||
case strings.Contains(strings.ToLower(r), "unauthorized"):
|
||||
return "UNAUTHORIZED"
|
||||
return "agent.unauthorized"
|
||||
case strings.Contains(strings.ToLower(r), "timeout"):
|
||||
return "TIMEOUT"
|
||||
return "agent.timeout"
|
||||
default:
|
||||
if strings.ContainsAny(r, "/\\") || strings.Contains(strings.ToLower(r), "token") {
|
||||
return "RUN_FAILED"
|
||||
return "agent.run_failed"
|
||||
}
|
||||
if len(r) > 64 {
|
||||
return "RUN_FAILED"
|
||||
return "agent.run_failed"
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -20,20 +20,22 @@ const (
|
||||
|
||||
var ErrTypedResult = fmt.Errorf("pitransport: typed result_v1 rejected")
|
||||
|
||||
// 对外错误码与 Error-Code-System 的 agent 域一致。
|
||||
// CANCELLED 并入 agent.canceled,MISSING_TARGET 并入 agent.missing_deliverable。
|
||||
var allowedErrorCodes = map[string]struct{}{
|
||||
"PROMPT_EMPTY": {},
|
||||
"UNAUTHORIZED": {},
|
||||
"TIMEOUT": {},
|
||||
"CANCELLED": {},
|
||||
"TRANSIENT": {},
|
||||
"MISSING_TARGET": {},
|
||||
"RUN_FAILED": {},
|
||||
"agent.prompt_empty": {},
|
||||
"agent.unauthorized": {},
|
||||
"agent.timeout": {},
|
||||
"agent.canceled": {},
|
||||
"agent.transient": {},
|
||||
"agent.missing_deliverable": {},
|
||||
"agent.run_failed": {},
|
||||
}
|
||||
|
||||
var retryableErrorCodes = map[string]struct{}{
|
||||
"TIMEOUT": {},
|
||||
"CANCELLED": {},
|
||||
"TRANSIENT": {},
|
||||
"agent.timeout": {},
|
||||
"agent.canceled": {},
|
||||
"agent.transient": {},
|
||||
}
|
||||
|
||||
type ResultV1Member struct {
|
||||
|
||||
@@ -63,13 +63,13 @@ func TestResultV1RejectsMissingMemberBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTypedRetryableAllowlist(t *testing.T) {
|
||||
ok := BuildResultV1("r", "error", true, "TRANSIENT", nil)
|
||||
ok := BuildResultV1("r", "error", true, "agent.transient", nil)
|
||||
if !TypedRetryable(ok) {
|
||||
t.Fatal("TRANSIENT retryable")
|
||||
t.Fatal("agent.transient retryable")
|
||||
}
|
||||
denied := BuildResultV1("r", "error", true, "PROMPT_EMPTY", nil)
|
||||
denied := BuildResultV1("r", "error", true, "agent.prompt_empty", nil)
|
||||
if TypedRetryable(denied) {
|
||||
t.Fatal("PROMPT_EMPTY must not retry")
|
||||
t.Fatal("agent.prompt_empty must not retry")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -8,35 +8,35 @@ export const MAX_RESULT_WIRE_BYTES = 6 * 1024 * 1024;
|
||||
/** Tail window for execution.log patches; full log remains on shared volume. */
|
||||
export const EXECUTION_LOG_PATCH_BYTES = 128 * 1024;
|
||||
|
||||
const RETRYABLE_CODES = new Set(["TIMEOUT", "CANCELLED", "TRANSIENT"]);
|
||||
const RETRYABLE_CODES = new Set(["agent.timeout", "agent.canceled", "agent.transient"]);
|
||||
const ALLOWED_CODES = new Set([
|
||||
"PROMPT_EMPTY",
|
||||
"UNAUTHORIZED",
|
||||
"TIMEOUT",
|
||||
"CANCELLED",
|
||||
"TRANSIENT",
|
||||
"MISSING_TARGET",
|
||||
"RUN_FAILED",
|
||||
"agent.prompt_empty",
|
||||
"agent.unauthorized",
|
||||
"agent.timeout",
|
||||
"agent.canceled",
|
||||
"agent.transient",
|
||||
"agent.missing_deliverable",
|
||||
"agent.run_failed",
|
||||
]);
|
||||
|
||||
export function typedErrorCode(payload) {
|
||||
const raw = String(payload?.fallback_reason || payload?.error || "");
|
||||
const t = raw.toLowerCase();
|
||||
if (t.includes("prompt_empty")) return "PROMPT_EMPTY";
|
||||
if (t.includes("unauthorized")) return "UNAUTHORIZED";
|
||||
if (t.includes("timeout") || t.includes("timed out")) return "TIMEOUT";
|
||||
if (t.includes("cancel")) return "CANCELLED";
|
||||
if (t.includes("prompt_empty")) return "agent.prompt_empty";
|
||||
if (t.includes("unauthorized")) return "agent.unauthorized";
|
||||
if (t.includes("timeout") || t.includes("timed out")) return "agent.timeout";
|
||||
if (t.includes("cancel")) return "agent.canceled";
|
||||
if (
|
||||
t.includes("connection") ||
|
||||
t.includes("terminated") ||
|
||||
t.includes("econnreset") ||
|
||||
t.includes("socket hang")
|
||||
) {
|
||||
return "TRANSIENT";
|
||||
return "agent.transient";
|
||||
}
|
||||
if (t.includes("missing target")) return "MISSING_TARGET";
|
||||
if (payload?.retryable) return "TRANSIENT";
|
||||
if (!payload?.ok) return "RUN_FAILED";
|
||||
if (t.includes("missing target")) return "agent.missing_deliverable";
|
||||
if (payload?.retryable) return "agent.transient";
|
||||
if (!payload?.ok) return "agent.run_failed";
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export function withResultV1(payload) {
|
||||
run_id: String(out.run_id || "unknown").slice(0, 128),
|
||||
outcome: ok ? "success" : "error",
|
||||
retryable,
|
||||
error_code: ALLOWED_CODES.has(errorCode) ? errorCode : ok ? "" : "RUN_FAILED",
|
||||
error_code: ALLOWED_CODES.has(errorCode) ? errorCode : ok ? "" : "agent.run_failed",
|
||||
members,
|
||||
content_digest: membersDigest(members),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package catalogcheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"agent-native/contract"
|
||||
"error-code-system/errcode"
|
||||
)
|
||||
|
||||
// 常量取值必须与错误码库种子码相同。旧短字符串只作为错误码库里的别名存在。
|
||||
func TestContractCodesMatchCatalog(t *testing.T) {
|
||||
pairs := []struct {
|
||||
got contract.ErrorCode
|
||||
want errcode.Code
|
||||
}{
|
||||
{contract.ErrCanceled, errcode.AgentCanceled},
|
||||
{contract.ErrDeadlineExceeded, errcode.AgentDeadlineExceeded},
|
||||
{contract.ErrBudgetExceeded, errcode.AgentBudgetExceeded},
|
||||
{contract.ErrProviderUnavailable, errcode.AgentProviderUnavailable},
|
||||
{contract.ErrProviderRejected, errcode.AgentProviderRejected},
|
||||
{contract.ErrToolRejected, errcode.AgentToolRejected},
|
||||
{contract.ErrPathOutsideWorkspace, errcode.AgentPathOutsideWorkspace},
|
||||
{contract.ErrMissingCapability, errcode.AgentMissingCapability},
|
||||
{contract.ErrMissingDeliverable, errcode.AgentMissingDeliverable},
|
||||
{contract.ErrEmptyOutput, errcode.AgentEmptyOutput},
|
||||
{contract.ErrUnknownOutcome, errcode.AgentUnknownOutcome},
|
||||
{contract.ErrInternal, errcode.AgentInternal},
|
||||
}
|
||||
for _, pair := range pairs {
|
||||
if string(pair.got) != string(pair.want) {
|
||||
t.Errorf("contract %q != catalog %q", pair.got, pair.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-26
@@ -52,8 +52,8 @@ const (
|
||||
|
||||
// ToolProfile 是预定义工具策略名;与现有产品配置兼容。
|
||||
const (
|
||||
ToolProfileWorkspace = "workspace" // 默认:文档类工具受限集合
|
||||
ToolProfileReasoning = "reasoning" // 无工具,仅推理
|
||||
ToolProfileWorkspace = "workspace" // 默认:文档类工具受限集合
|
||||
ToolProfileReasoning = "reasoning" // 无工具,仅推理
|
||||
)
|
||||
|
||||
// ToolPolicy 描述执行允许的工具范围。实现不得自行扩展;未知工具一律拒绝。
|
||||
@@ -106,8 +106,8 @@ type Request struct {
|
||||
// SessionScope 为隔离身份;可为零值(一次性任务)。
|
||||
SessionScope SessionScope `json:"session_scope"`
|
||||
|
||||
Prompt string `json:"prompt"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
// Context 为可选历史消息;实现不得将其与 Prompt 之外的数据混合。
|
||||
Context []Message `json:"context,omitempty"`
|
||||
|
||||
@@ -143,12 +143,12 @@ const (
|
||||
type EventKind string
|
||||
|
||||
const (
|
||||
EventTextDelta EventKind = "text_delta"
|
||||
EventReasoningDelta EventKind = "reasoning_delta"
|
||||
EventToolCallDelta EventKind = "tool_call_delta"
|
||||
EventToolExecStart EventKind = "tool_execution_start"
|
||||
EventToolExecEnd EventKind = "tool_execution_end"
|
||||
EventMessageEnd EventKind = "message_end"
|
||||
EventTextDelta EventKind = "text_delta"
|
||||
EventReasoningDelta EventKind = "reasoning_delta"
|
||||
EventToolCallDelta EventKind = "tool_call_delta"
|
||||
EventToolExecStart EventKind = "tool_execution_start"
|
||||
EventToolExecEnd EventKind = "tool_execution_end"
|
||||
EventMessageEnd EventKind = "message_end"
|
||||
)
|
||||
|
||||
// Event 是过程事件帧;消费方(Core)负责映射为 SSE/持久化事件。
|
||||
@@ -174,30 +174,60 @@ const (
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrCanceled ErrorCode = "canceled"
|
||||
ErrDeadlineExceeded ErrorCode = "deadline_exceeded"
|
||||
ErrBudgetExceeded ErrorCode = "budget_exceeded"
|
||||
ErrProviderUnavailable ErrorCode = "provider_unavailable"
|
||||
ErrProviderRejected ErrorCode = "provider_rejected"
|
||||
ErrToolRejected ErrorCode = "tool_rejected"
|
||||
ErrPathOutsideWorkspace ErrorCode = "path_outside_workspace"
|
||||
ErrMissingCapability ErrorCode = "missing_capability"
|
||||
ErrMissingDeliverable ErrorCode = "missing_deliverable"
|
||||
ErrEmptyOutput ErrorCode = "empty_output"
|
||||
ErrUnknownOutcome ErrorCode = "unknown_outcome"
|
||||
ErrInternal ErrorCode = "internal"
|
||||
ErrCanceled ErrorCode = "agent.canceled"
|
||||
ErrDeadlineExceeded ErrorCode = "agent.deadline_exceeded"
|
||||
ErrBudgetExceeded ErrorCode = "agent.budget_exceeded"
|
||||
ErrProviderUnavailable ErrorCode = "agent.provider_unavailable"
|
||||
ErrProviderRejected ErrorCode = "agent.provider_rejected"
|
||||
ErrToolRejected ErrorCode = "agent.tool_rejected"
|
||||
ErrPathOutsideWorkspace ErrorCode = "agent.path_outside_workspace"
|
||||
ErrMissingCapability ErrorCode = "agent.missing_capability"
|
||||
ErrMissingDeliverable ErrorCode = "agent.missing_deliverable"
|
||||
ErrEmptyOutput ErrorCode = "agent.empty_output"
|
||||
ErrUnknownOutcome ErrorCode = "agent.unknown_outcome"
|
||||
ErrInternal ErrorCode = "agent.internal"
|
||||
)
|
||||
|
||||
// Error 是标准化执行错误。Retryable 仅表示传输级瞬时故障可由 adapter 有界
|
||||
// 重试或调用方作为新步骤重试;与 SideEffects 独立判定。
|
||||
type Error struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Retryable bool `json:"retryable,omitempty"`
|
||||
Code ErrorCode `json:"code"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Retryable bool `json:"retryable,omitempty"`
|
||||
SideEffects SideEffects `json:"side_effects"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string { return string(e.Code) + ": " + e.Reason }
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return string(e.Code) + ": " + e.Reason
|
||||
}
|
||||
|
||||
// ErrorCode 返回规范码字符串。日志库用这个方法识别错误,因此 contract 不导入错误码模块。
|
||||
func (e *Error) ErrorCode() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return string(e.Code)
|
||||
}
|
||||
|
||||
// ErrorFields 返回可写入日志的字段。执行标识由调用方随日志字段另传,不放进本结构。
|
||||
func (e *Error) ErrorFields() map[string]any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
fields := map[string]any{
|
||||
"retryable": e.Retryable,
|
||||
}
|
||||
if e.Reason != "" {
|
||||
fields["reason"] = e.Reason
|
||||
}
|
||||
if e.SideEffects != "" {
|
||||
fields["side_effects"] = string(e.SideEffects)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Usage 报告 token 用量;实现报告不了时保持零值,不猜测。
|
||||
type Usage struct {
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestFakeBackendSuite(t *testing.T) {
|
||||
func TestFakeCancelSemantics(t *testing.T) {
|
||||
block := make(chan struct{})
|
||||
b := &conformance.FakeBackend{
|
||||
Desc: contract.BackendDescriptor{Name: "fake", Capabilities: []contract.Capability{contract.CapCancel}},
|
||||
Desc: contract.BackendDescriptor{Name: "fake", Capabilities: []contract.Capability{contract.CapCancel}},
|
||||
BlockUntil: block,
|
||||
}
|
||||
conformance.Run(t, conformance.Case{
|
||||
@@ -41,3 +41,23 @@ func TestFakeCancelSemantics(t *testing.T) {
|
||||
t.Fatal("backend must observe ctx cancellation before returning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorDuckTypeForLogs(t *testing.T) {
|
||||
e := &contract.Error{
|
||||
Code: contract.ErrProviderUnavailable,
|
||||
Reason: "down",
|
||||
Retryable: true,
|
||||
SideEffects: contract.SideEffectNone,
|
||||
}
|
||||
if e.ErrorCode() != "agent.provider_unavailable" {
|
||||
t.Fatal(e.ErrorCode())
|
||||
}
|
||||
fields := e.ErrorFields()
|
||||
if fields["reason"] != "down" || fields["side_effects"] != "none" || fields["retryable"] != true {
|
||||
t.Fatalf("%v", fields)
|
||||
}
|
||||
var nilErr *contract.Error
|
||||
if nilErr.ErrorCode() != "" || nilErr.ErrorFields() != nil {
|
||||
t.Fatal("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
+1
Submodule deps/error-code-system added at b67696d393
+1
Submodule deps/logging-system added at 7f5972007b
@@ -1,3 +1,12 @@
|
||||
module agent-native
|
||||
|
||||
go 1.27
|
||||
|
||||
require (
|
||||
error-code-system v0.0.0
|
||||
logging-system v0.0.0
|
||||
)
|
||||
|
||||
replace error-code-system => ./deps/error-code-system
|
||||
|
||||
replace logging-system => ./deps/logging-system
|
||||
|
||||
+22
-1
@@ -9,6 +9,8 @@ import (
|
||||
"agent-native/runtime/llm"
|
||||
"agent-native/runtime/session"
|
||||
"agent-native/runtime/tools"
|
||||
|
||||
"logging-system/logsys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,6 +41,10 @@ type Config struct {
|
||||
GetSteering func() []llm.Message
|
||||
GetFollowUp func() []llm.Message
|
||||
OnEvent func(Event)
|
||||
// Log 可选。nil 时丢弃日志,不改变循环结果。
|
||||
Log *logsys.Logger
|
||||
// ExecutionID 只用于日志字段,不参与控制流。
|
||||
ExecutionID string
|
||||
}
|
||||
|
||||
// Result is the final assistant text and the transcript produced by one run.
|
||||
@@ -87,11 +93,13 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
var usage llm.Usage
|
||||
for turn := 0; turn < cfg.MaxTurns; turn++ {
|
||||
if ctx.Err() != nil {
|
||||
cfg.logger().Error("runtime turn aborted", ctx.Err())
|
||||
return Result{Text: last.Content, Messages: messages, StopReason: llm.StopAborted, Err: ctx.Err()}, nil
|
||||
}
|
||||
emit(cfg, Event{Type: "turn_start"})
|
||||
end, errEv := consume(ctx, cfg, messages)
|
||||
if errEv != nil {
|
||||
cfg.logger().Error("runtime turn failed", errEv, "retryable", end.Retryable)
|
||||
return Result{Text: end.Message.Content, Messages: messages, StopReason: end.Message.StopReason, Err: errEv, Retryable: end.Retryable}, nil
|
||||
}
|
||||
last = end.Message
|
||||
@@ -136,15 +144,28 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
emit(cfg, Event{Type: "agent_end", Message: last})
|
||||
return Result{Text: last.Content, Messages: messages, StopReason: last.StopReason, Usage: usage}, nil
|
||||
}
|
||||
errCap := errString("max turns exceeded")
|
||||
cfg.logger().Error("runtime turn cap hit", errCap)
|
||||
return Result{
|
||||
Text: last.Content,
|
||||
Messages: messages,
|
||||
StopReason: llm.StopError,
|
||||
Usage: usage,
|
||||
Err: errString("max turns exceeded"),
|
||||
Err: errCap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cfg Config) logger() *logsys.Logger {
|
||||
if cfg.Log == nil {
|
||||
return logsys.Nop()
|
||||
}
|
||||
log := cfg.Log.Module("runtime")
|
||||
if cfg.ExecutionID == "" {
|
||||
return log
|
||||
}
|
||||
return log.With("execution_id", cfg.ExecutionID)
|
||||
}
|
||||
|
||||
func systemText(cfg Config) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(cfg.SystemPrompt)
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# 核对本仓 gitlink 与已检出子模块 HEAD 是否一致。
|
||||
# 嵌套副本是否与消费方自己的 deps 相同,由消费方仓库的同名脚本比较。
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
fail=0
|
||||
for path in deps/development-guidelines deps/error-code-system deps/logging-system; do
|
||||
if [ ! -e "$path/.git" ]; then
|
||||
echo "missing submodule checkout: $path" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
recorded=$(git rev-parse ":$path")
|
||||
actual=$(git -C "$path" rev-parse HEAD)
|
||||
if [ "$recorded" != "$actual" ]; then
|
||||
echo "gitlink mismatch: $path recorded=$recorded HEAD=$actual" >&2
|
||||
fail=1
|
||||
else
|
||||
echo "ok $path $recorded"
|
||||
fi
|
||||
done
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user