diff --git a/.gitmodules b/.gitmodules index ef67b32..032d44a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/PROJECT.md b/PROJECT.md index 645963a..061827e 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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,可选日志器 | 克隆: diff --git a/README.md b/README.md index 627b415..c734c93 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/adapters/native/backend.go b/adapters/native/backend.go index e421940..9bccc40 100644 --- a/adapters/native/backend.go +++ b/adapters/native/backend.go @@ -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 diff --git a/adapters/pitransport/remote_result.go b/adapters/pitransport/remote_result.go index be48ce7..c939733 100644 --- a/adapters/pitransport/remote_result.go +++ b/adapters/pitransport/remote_result.go @@ -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 } diff --git a/adapters/pitransport/resultv1.go b/adapters/pitransport/resultv1.go index 3085e44..0ca2798 100644 --- a/adapters/pitransport/resultv1.go +++ b/adapters/pitransport/resultv1.go @@ -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 { diff --git a/adapters/pitransport/resultv1_test.go b/adapters/pitransport/resultv1_test.go index 8446f05..3b9fb51 100644 --- a/adapters/pitransport/resultv1_test.go +++ b/adapters/pitransport/resultv1_test.go @@ -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") } } diff --git a/adapters/pitransport/testdata/result-v1.mjs b/adapters/pitransport/testdata/result-v1.mjs index ac4f9fd..5c05737 100644 --- a/adapters/pitransport/testdata/result-v1.mjs +++ b/adapters/pitransport/testdata/result-v1.mjs @@ -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), }; diff --git a/catalogcheck/align_test.go b/catalogcheck/align_test.go new file mode 100644 index 0000000..8ffe78d --- /dev/null +++ b/catalogcheck/align_test.go @@ -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) + } + } +} diff --git a/contract/contract.go b/contract/contract.go index 450cb50..d22d727 100644 --- a/contract/contract.go +++ b/contract/contract.go @@ -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 { diff --git a/contract/contract_test.go b/contract/contract_test.go index 88087ee..9349258 100644 --- a/contract/contract_test.go +++ b/contract/contract_test.go @@ -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") + } +} diff --git a/deps/error-code-system b/deps/error-code-system new file mode 160000 index 0000000..b67696d --- /dev/null +++ b/deps/error-code-system @@ -0,0 +1 @@ +Subproject commit b67696d393e7abfbdd16fdde1064c07f5ecd2f72 diff --git a/deps/logging-system b/deps/logging-system new file mode 160000 index 0000000..7f59720 --- /dev/null +++ b/deps/logging-system @@ -0,0 +1 @@ +Subproject commit 7f5972007bdcb6047ce7f95ee891f53e0af9d136 diff --git a/go.mod b/go.mod index 4de126e..f46d604 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/runtime/loop.go b/runtime/loop.go index c125d9e..73291f7 100644 --- a/runtime/loop.go +++ b/runtime/loop.go @@ -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) diff --git a/scripts/submodule-consistency.sh b/scripts/submodule-consistency.sh new file mode 100755 index 0000000..76d427b --- /dev/null +++ b/scripts/submodule-consistency.sh @@ -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"