@jangts/agent-core 0.2.5 → 0.3.6

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +386 -50
  3. package/development/docs/api/README.en.md +4 -0
  4. package/development/docs/api/README.md +4 -0
  5. package/development/docs/api/decorators.en.md +83 -0
  6. package/development/docs/api/decorators.md +108 -0
  7. package/development/docs/api/observability.en.md +37 -11
  8. package/development/docs/api/observability.md +64 -11
  9. package/development/docs/api/reference.en.md +469 -348
  10. package/development/docs/api/reference.md +225 -137
  11. package/development/docs/api/types.en.md +1 -0
  12. package/development/docs/api/types.md +1 -0
  13. package/development/docs/api/workflow-views.en.md +64 -0
  14. package/development/docs/api/workflow-views.md +78 -0
  15. package/development/docs/api/workflow.en.md +6 -2
  16. package/development/docs/api/workflow.md +6 -2
  17. package/development/skills/agent-core-app/SKILL.md +16 -0
  18. package/dist/cjs/capabilities/invocation.js +21 -1
  19. package/dist/cjs/capabilities/tool-runtime.js +8 -1
  20. package/dist/cjs/capabilities/tools.js +12 -17
  21. package/dist/cjs/diagnostics/index.js +51 -3
  22. package/dist/cjs/diagnostics/native-sub-flow.js +106 -0
  23. package/dist/cjs/diagnostics/sub-flow.js +278 -32
  24. package/dist/cjs/execution/engine.js +3 -2
  25. package/dist/cjs/execution/events.js +5 -5
  26. package/dist/cjs/execution/reporting.js +18 -28
  27. package/dist/cjs/execution/workflow-chain.js +211 -0
  28. package/dist/cjs/execution/workflow-graph.js +72 -0
  29. package/dist/cjs/execution/workflow.js +34 -6
  30. package/dist/cjs/kits/aggregation.js +17 -14
  31. package/dist/cjs/kits/answering/checks.js +2 -1
  32. package/dist/cjs/kits/answering/flow.js +24 -21
  33. package/dist/cjs/kits/answering/tasks.js +25 -20
  34. package/dist/cjs/kits/delivery.js +36 -24
  35. package/dist/cjs/kits/knowledge/catalog.js +33 -30
  36. package/dist/cjs/kits/policies.js +3 -2
  37. package/dist/cjs/kits/presentation/outputs.js +2 -1
  38. package/dist/cjs/kits/retrieval.js +7 -1
  39. package/dist/cjs/kits/tools.js +1 -0
  40. package/dist/cjs/model/decision-retry.js +2 -1
  41. package/dist/cjs/model/runner.js +20 -5
  42. package/dist/cjs/structs/execution/workflow-views.js +2 -0
  43. package/dist/cjs/support/context/nodes.js +48 -45
  44. package/dist/cjs/support/context/processing.js +6 -3
  45. package/dist/cjs/support/decorators/definition.js +42 -0
  46. package/dist/cjs/support/decorators/index.js +63 -0
  47. package/dist/cjs/support/decorators/pool.js +87 -0
  48. package/dist/cjs/support/decorators/types.js +2 -0
  49. package/dist/cjs/utils/invocation.js +1 -0
  50. package/dist/esm/capabilities/invocation.js +21 -1
  51. package/dist/esm/capabilities/tool-runtime.js +8 -1
  52. package/dist/esm/capabilities/tools.js +12 -17
  53. package/dist/esm/diagnostics/index.d.ts +1 -0
  54. package/dist/esm/diagnostics/index.js +48 -2
  55. package/dist/esm/diagnostics/native-sub-flow.d.ts +94 -0
  56. package/dist/esm/diagnostics/native-sub-flow.js +101 -0
  57. package/dist/esm/diagnostics/sub-flow.d.ts +56 -4
  58. package/dist/esm/diagnostics/sub-flow.js +271 -32
  59. package/dist/esm/execution/engine.d.ts +1 -0
  60. package/dist/esm/execution/engine.js +3 -2
  61. package/dist/esm/execution/events.js +5 -5
  62. package/dist/esm/execution/reporting.d.ts +6 -12
  63. package/dist/esm/execution/reporting.js +19 -29
  64. package/dist/esm/execution/workflow-chain.d.ts +3 -0
  65. package/dist/esm/execution/workflow-chain.js +208 -0
  66. package/dist/esm/execution/workflow-graph.d.ts +7 -0
  67. package/dist/esm/execution/workflow-graph.js +68 -0
  68. package/dist/esm/execution/workflow.d.ts +6 -0
  69. package/dist/esm/execution/workflow.js +34 -6
  70. package/dist/esm/kits/aggregation.d.ts +1 -1
  71. package/dist/esm/kits/aggregation.js +17 -14
  72. package/dist/esm/kits/answering/checks.js +2 -1
  73. package/dist/esm/kits/answering/flow.js +24 -21
  74. package/dist/esm/kits/answering/tasks.js +25 -20
  75. package/dist/esm/kits/delivery.d.ts +0 -9
  76. package/dist/esm/kits/delivery.js +36 -24
  77. package/dist/esm/kits/knowledge/catalog.js +33 -30
  78. package/dist/esm/kits/policies.d.ts +1 -1
  79. package/dist/esm/kits/policies.js +3 -2
  80. package/dist/esm/kits/presentation/outputs.js +2 -1
  81. package/dist/esm/kits/retrieval.js +7 -1
  82. package/dist/esm/kits/tools.js +1 -0
  83. package/dist/esm/model/decision-retry.d.ts +2 -0
  84. package/dist/esm/model/decision-retry.js +2 -1
  85. package/dist/esm/model/runner.d.ts +2 -0
  86. package/dist/esm/model/runner.js +20 -5
  87. package/dist/esm/structs/capabilities/invocation.d.ts +2 -0
  88. package/dist/esm/structs/capabilities/tools.d.ts +7 -3
  89. package/dist/esm/structs/common.d.ts +2 -0
  90. package/dist/esm/structs/debug.d.ts +36 -20
  91. package/dist/esm/structs/execution/workflow-views.d.ts +123 -0
  92. package/dist/esm/structs/execution/workflow-views.js +1 -0
  93. package/dist/esm/structs/execution/workflow.d.ts +4 -3
  94. package/dist/esm/structs/index.d.ts +1 -0
  95. package/dist/esm/structs/model/runtime.d.ts +2 -0
  96. package/dist/esm/support/context/nodes.js +48 -45
  97. package/dist/esm/support/context/processing.js +6 -3
  98. package/dist/esm/support/decorators/definition.d.ts +19 -0
  99. package/dist/esm/support/decorators/definition.js +38 -0
  100. package/dist/esm/support/decorators/index.d.ts +39 -0
  101. package/dist/esm/support/decorators/index.js +48 -0
  102. package/dist/esm/support/decorators/pool.d.ts +97 -0
  103. package/dist/esm/support/decorators/pool.js +83 -0
  104. package/dist/esm/support/decorators/types.d.ts +22 -0
  105. package/dist/esm/support/decorators/types.js +1 -0
  106. package/dist/esm/utils/invocation.d.ts +1 -0
  107. package/dist/esm/utils/invocation.js +1 -0
  108. package/package.json +10 -2
  109. package/dist/cjs/utils/debug.js +0 -30
  110. package/dist/esm/utils/debug.d.ts +0 -27
  111. package/dist/esm/utils/debug.js +0 -27
@@ -0,0 +1,83 @@
1
+ # Decorators and definition pools
2
+
3
+ Import decorators and readers from `@jangts/agent-core/support/decorators`. Only standard TypeScript decorators are supported; NestJS compilation units using experimentalDecorators cannot use this syntax directly. Original object, define, constructor and bind paths remain available. No runtime dependency, reflection metadata or automatic DI is added.
4
+
5
+ Class decorators construct a definition once after static initialization and supply id. Classes own their original fields, handlers, private helpers and getters; do not repeat complete options in decorator metadata. Classes require zero-argument construction. Constructors initialize definitions without business operations. No Engine, Runner, request budget or model call is created.
6
+
7
+ ## Named class contracts
8
+
9
+ Import Node, Tool, Fragment, Workflow, Agent and Model from the decorator subpath. These contracts omit the decorator-owned id. Object-shaped contracts support implements; Agent preserves the original union and is checked by its decorator rather than requiring a class to implement a union. Getters, private fields and prototype methods retain their original receiver.
10
+
11
+ ```ts
12
+ import { AgentCoreEngine, defineWorkflow } from '@jangts/agent-core';
13
+ import { node, nodePool } from '@jangts/agent-core/support/decorators';
14
+ import type { Node } from '@jangts/agent-core/support/decorators';
15
+ type State = { text: string; answer?: string };
16
+ @node<State, string>('greet', { ns: 'chat' })
17
+ class Greeting implements Node<State, string> {
18
+ readonly description = 'Greeting';
19
+ readonly routes = [{ complete: (state: Readonly<State>) => state.answer! }];
20
+ #prefix = 'Hello, ';
21
+ run(state: Readonly<State>) { return { answer: this._format(state.text) }; }
22
+ private _format(text: string) { return this.#prefix + text.trim(); }
23
+ }
24
+ const workflow = defineWorkflow<State, string>({ id: 'greeting', nodes: nodePool.getAll('chat') });
25
+ console.log(await new AgentCoreEngine({ workflow }).run({ text: 'visitor' }));
26
+ ```
27
+
28
+ ## Namespaces and reads
29
+
30
+ Each kind has one shared reader: nodePool, toolPool, fragmentPool, workflowPool, agentPool and modelPool. Consumers do not create pools or register definitions manually. Every decorator accepts optional `{ ns: string | readonly string[] }`, defaulting to `'default'`.
31
+
32
+ ```ts
33
+ nodePool.get('greet', 'chat');
34
+ nodePool.getAll('chat');
35
+ nodePool.pick(['greet', 'answer'], 'chat');
36
+ nodePool.get('greet'); // default namespace
37
+ nodePool.getAll(); // only default, not every namespace
38
+ nodePool.pick(['greet']);
39
+ ```
40
+
41
+ Ids are unique within one kind and namespace. Other namespaces and kinds may reuse an id. Names match exactly, without hierarchy, wildcard or fallback; blank names and empty namespace arrays reject. Repeated namespaces are deduplicated. An array registers the same definition in multiple namespaces without copying or reconstructing it. All destinations are checked before writing; a conflict leaves no partial registration.
42
+
43
+ get returns undefined on a miss. getAll returns a frozen registration-order snapshot, or an empty array for an unknown namespace. pick takes an id array, preserves caller order and rejects missing or repeated ids; an empty id array returns an empty snapshot. Reads accept original-definition generics, for example `nodePool.get<WorkflowNode<State, string>>('greet', 'chat')`. The generic describes the caller's expected contract, not a runtime type proof.
44
+
45
+ ns is external registration metadata, excluded from Core options. Returned ids remain unchanged. Combining same-id definitions from different namespaces in one Workflow or ToolRegistry still fails original uniqueness checks. App owns explicit node ordering and routes. Readers expose no constructors, registration, reset, sealing, tags or owner lookups.
46
+
47
+ ## Existing instance methods
48
+
49
+ ```ts
50
+ class GreetingService {
51
+ constructor(private readonly prefix: string) {}
52
+ @node<State, string>('reply', {
53
+ ns: 'chat-service', description: 'Use an existing service',
54
+ routes: [{ complete: (state: Readonly<State>) => state.answer! }],
55
+ })
56
+ answer(state: Readonly<State>) { return { answer: this.prefix + state.text }; }
57
+ }
58
+ const service = new GreetingService('Hello, ');
59
+ const definition = nodePool.get('reply', 'chat-service');
60
+ ```
61
+
62
+ Standard instance initializers automatically bind and register methods. No registerFrom, manual bind, renamed handler or extra class decorator is needed. Initializers run before instance fields and constructor bodies, so they bind without invoking handlers or reading business fields. Read and execute only after successful host construction. Constructor failures do not roll back completed initializer registrations and must be treated as wiring failures. Pools retain host references; use long-lived services, not request-scoped hosts. A second instance with the same kind, namespace and id rejects without replacing the original binding.
63
+
64
+ Direct metadata callbacks also bind the host. Nested schema/routes retain their original receiver, and arrow functions retain lexical this. Only public instance methods are supported; static/private methods, top-level function decorator syntax and implicit inheritance merging are unsupported. A class-owned id rejects because its decorator supplies id.
65
+
66
+ ## Original consumers
67
+
68
+ | Decorator | Class contract | Adapted method | Reader | Original consumer |
69
+ | --- | --- | --- | --- | --- |
70
+ | node | Node<State, Result, App> | run | nodePool | defineWorkflow |
71
+ | tool | Tool<Input, Output, App> | execute | toolPool | defineTool / defineToolCatalog |
72
+ | fragment | Fragment<Input> | format | fragmentPool | defineContextFragments |
73
+ | workflow | Workflow<State, Result, App> | None | workflowPool | defineWorkflow |
74
+ | agent | Agent<State, Result, App> | None | agentPool | Engine constructor |
75
+ | model | Model | None | modelPool | ModelRunner constructor |
76
+
77
+ agent/model ids are registration keys, not Core branch, model or transport identities. Original schema, graph, trust, authorization, result, budget and cancellation boundaries remain in force. Nodes are validated in the final graph. No public metadata-plus-free-function adapter is provided; top-level functions belong directly in original options.
78
+
79
+ ## Getter-defined routes
80
+
81
+ A routes getter may return `when: state => this._ready(state)` and `complete: state => this._reply(state)`. Workflow assembly reads and snapshots the table, potentially reading the getter multiple times. Keep getters pure and independent of request state. After state patches commit, route callbacks receive the latest state and can call private helpers. The getter is not re-evaluated each run; do not store request state on shared nodes.
82
+
83
+ ESM/CJS module copies share roots within one JavaScript realm. Processes, workers and separate realms do not share roots. Independently evaluated classes registering the same kind, namespace and id reject rather than merge. Explicitly retain definition modules in the bundle: discarded side-effect-only imports cannot guarantee registration. Shared definitions must not retain request identity, signal or budget. Reading is neither execution nor authorization.
@@ -0,0 +1,108 @@
1
+ # 装饰器与定义池
2
+
3
+ 装饰器与读取池从 `@jangts/agent-core/support/decorators` 导入,仅支持 TypeScript 标准装饰器语义。启用了 experimentalDecorators 的 NestJS 编译单元不能直接使用。普通对象、define、构造与 bind 入口保持可用。无新增运行时依赖,不使用 reflect-metadata 或自动 DI。
4
+
5
+ 类装饰器代办定义实例化与 id 注入;实例方法装饰器将原方法绑定为 handler,保留原对象及 this。产物仍是原合同的定义或配置,不创建 Engine/ModelRunner,不改变预算、取消、授权、状态提交与工具回执。
6
+
7
+ ## 类:处理单元与内部协作
8
+
9
+ 从同一子路径导入 Node、Tool、Fragment、Workflow、Agent、Model 类型,描述类应提供的原字段与方法;id 由装饰器补齐,无需重复写 Omit。Agent 保留原配置的联合类型分支,由装饰器检查,不要求 class implements 联合类型。
10
+
11
+ ```ts
12
+ import { AgentCoreEngine, defineWorkflow } from '@jangts/agent-core';
13
+ import { node, nodePool } from '@jangts/agent-core/support/decorators';
14
+ import type { Node } from '@jangts/agent-core/support/decorators';
15
+
16
+ type State = { text: string; answer?: string };
17
+
18
+ @node<State, string>('greet', { ns: 'chat' })
19
+ class Greeting implements Node<State, string> {
20
+ readonly description = '问候';
21
+ readonly routes = [{ complete: (state: Readonly<State>) => state.answer! }];
22
+ #prefix = '你好,';
23
+ run(state: Readonly<State>) {
24
+ return { answer: this._format(state.text) };
25
+ }
26
+ private _format(text: string) { return this.#prefix + text.trim(); }
27
+ }
28
+
29
+ const workflow = defineWorkflow<State, string>({
30
+ id: 'greeting', nodes: nodePool.getAll('chat'),
31
+ });
32
+ console.log(await new AgentCoreEngine({ workflow }).run({ text: '访客' }));
33
+ ```
34
+
35
+ id 只来自装饰器;类中重复声明 id 会拒绝。定义类必须可无参构造,在静态初始化完成后构造一次;池读取不再构造。配置字段、handler 及内部方法由类提供,不在装饰器中重复 metadata。私有字段、getter 和原型方法保留真实接收者。构造函数只初始化定义,不执行业务操作。
36
+
37
+ ## 命名空间与读取
38
+
39
+ 每种定义各有一个共享读取池:nodePool、toolPool、fragmentPool、workflowPool、agentPool、modelPool。调用方无需创建池或手动登记。所有装饰器接受可选的 `{ ns: string | readonly string[] }`,缺省为 `'default'`。
40
+
41
+ ```ts
42
+ nodePool.get('greet', 'chat'); // 单个定义,缺失为 undefined
43
+ nodePool.getAll('chat'); // 此命名空间全部定义
44
+ nodePool.pick(['greet', 'answer'], 'chat'); // 按指定顺序选取,缺失或重复 id 抛错
45
+ nodePool.get('greet'); // 等价于 get('greet', 'default')
46
+ nodePool.getAll(); // 只读 default,不跨命名空间合并
47
+ nodePool.pick(['greet']); // 在 default 中选取
48
+ ```
49
+
50
+ 同一类型、同一命名空间内 id 唯一;不同命名空间或不同类型可同名。ns 精确匹配,不支持层级、通配或隐式回退,空白名称与空数组拒绝。数组中的重复命名空间去重。`ns: ['chat', 'assistant']` 将同一已构造定义登记到两个空间,不重复构造、不复制实例;任一目标重名则整次登记拒绝,不留下部分写入。
51
+
52
+ getAll 返回登记顺序的只读数组快照;未知命名空间返回空数组。pick 返回入参顺序的只读数组快照,空 id 数组返回空数组。读取方法可使用原定义类型泛型,如 `nodePool.get<WorkflowNode<State, string>>('greet', 'chat')`;泛型描述调用方预期合同,不是运行时类型证明。
53
+
54
+ ns 仅用于辅助登记,不进入 Core options;取出的 id 仍为原 id。把不同命名空间的同名定义装入同一 Workflow/ToolRegistry 时,原入口仍拒绝重名。Workflow 节点顺序与路由由 App 明确组织,不把模块导入顺序当成业务执行顺序。池不公开创建、写入、清空或封存接口,不支持 tags 或 owner 查询。
55
+
56
+ ## 已有实例:保留业务方法名
57
+
58
+ ```ts
59
+ class GreetingService {
60
+ constructor(private readonly prefix: string) {}
61
+ @node<State, string>('reply', {
62
+ ns: 'chat-service',
63
+ description: '调用已有服务',
64
+ routes: [{ complete: (state: Readonly<State>) => state.answer! }],
65
+ })
66
+ answer(state: Readonly<State>) {
67
+ return { answer: this._format(state.text) };
68
+ }
69
+ private _format(text: string) { return this.prefix + text; }
70
+ }
71
+ const service = new GreetingService('你好,'); // 创建原宿主,自动绑定与登记
72
+ const definition = nodePool.get('reply', 'chat-service');
73
+ ```
74
+
75
+ 方法装饰器用标准实例初始化钩子绑定并登记,不需要 registerFrom、手写 bind 或额外类装饰器。该钩子先于实例字段与构造函数体运行,因此只绑定、不执行 handler,不访问未初始化的业务字段。调用方必须等待宿主构造成功后再读取并运行定义;构造失败不回滚已完成的初始化登记,应视为装配失败。池持有真实宿主的绑定引用,适用于长期存活的定义/服务实例,不是请求级实例工厂。同命名空间同 id 的第二个实例拒绝登记,不覆盖第一个绑定。
76
+
77
+ metadata 中的直接回调同样绑定到宿主;嵌套 schema 与 routes 保留原合同接收者,箭头函数保留词法 this。仅支持公开实例方法,不支持静态/私有方法、普通顶层函数的 @ 语法或隐式继承合并。
78
+
79
+ ## getter 与按状态选路
80
+
81
+ ```ts
82
+ get routes(): Node<State, string>['routes'] {
83
+ return [
84
+ { when: state => this._hasAnswer(state), complete: state => state.answer! },
85
+ { complete: () => '没有可交付的回答' },
86
+ ];
87
+ }
88
+ private _hasAnswer(state: Readonly<State>) {
89
+ return Boolean(state.answer?.trim());
90
+ }
91
+ ```
92
+
93
+ Workflow 装配时读取并固定路由表;getter 应无副作用,不依赖调用次数,也不读取本次请求状态。每次 run 提交补丁后,when/complete 获得最新 state,并可调用原类辅助方法。getter 不会每轮重建路由;不要把请求 state 存在共享节点 this 上。
94
+
95
+ ## 定义类型与原入口
96
+
97
+ | 装饰器 | 类的结构 | 方法适配 | 读取池 | 后续入口 |
98
+ | --- | --- | --- | --- | --- |
99
+ | node | Node<State, Result, App> | run | nodePool | defineWorkflow |
100
+ | tool | Tool<Input, Output, App> | execute | toolPool | defineTool/defineToolCatalog |
101
+ | fragment | Fragment<Input> | format | fragmentPool | defineContextFragments |
102
+ | workflow | Workflow<State, Result, App> | 不提供 | workflowPool | defineWorkflow |
103
+ | agent | Agent<State, Result, App> | 不提供 | agentPool | App 创建 Engine |
104
+ | model | Model | 不提供 | modelPool | App 创建 ModelRunner |
105
+
106
+ agent/model 的 id 是辅助登记键,不覆盖 Core 分支、模型或 transport 身份。工具复用原 defineTool 校验;Workflow 复用原图校验,节点在完整图中验证;fragment 复用原 builder 的 formatter 检查,数据、信任与预算保持原合同;model 复用原配置快照验证,不消耗模型调用。辅助层不公开 metadata+普通函数转换入口,顶层函数直接填入原 options。
107
+
108
+ 同一 JavaScript realm 内 ESM/CJS 副本共享池;不同进程、Worker 或独立 realm 不共享。两次独立加载并装饰同命名空间同 id 的类仍会重名拒绝,不自动合并。定义模块必须被打包图明确保留;被 tree-shaking 删除的纯副作用导入不能保证登记。共享定义不能保存请求身份、signal 或请求预算;池读取不是执行或授权。
@@ -83,13 +83,13 @@ await agent.run(state);
83
83
  | execution.observe.event | Loop/Workflow, model.start/end, tool start/end and permitted dynamic reports | Side channel; a synchronous throw, async rejection or hang does not change execution; still effective when persistent logging is closed |
84
84
  | execution.diagnostics → CoreLogOutput.write | Optional persistent projection of the same fact | The App decides fields, body, storage and retention; it gains no content authorization from other listeners |
85
85
  | ModelRunner.sink.output | model.start/output/parsed of the same outer layer, plus model.end on success, failure or cancellation | Body waits in order, propagates failures and preserves backpressure; the end notification is limited to 250 milliseconds independently, and its failure does not override the result or the original exception |
86
- | context.report({ usedModel, payload, subs }) | Immediate report during the current node's run | usedModel is optional boolean metadata, payload/subs are read only when content is permitted; Core fills in identity, time and sequence, and reports after the node ends or is cancelled are dropped |
87
- | node report(update, context) | usedModel/payload/subs on completion/transition | Evaluated only when content is permitted; does not replace immediate reports during the run |
86
+ | context.report({ usedModel, payload }) | Immediate report during the current node's run | usedModel is optional boolean metadata, payload are read only when content is permitted; Core fills in identity, time and sequence, and reports after the node ends or is cancelled are dropped |
87
+ | node report(update, context) | usedModel/payload on completion/transition | Evaluated only when content is permitted; does not replace immediate reports during the run |
88
88
  | context.createSubFlowLogger() | Sub-log instance bound to this execution | write records first, fix completes later, readAll obtains a fixed-format snapshot; reuses the existing node.progress outlet |
89
89
 
90
90
  Listeners can filter by metadata such as event, nodeId and workflowNodeId. workflowNodeId keeps the owning Workflow node, while model/tool boundaries still have their own nodeId and invocationId. Repeated visits to the same node have different spans, and concurrent runs do not correlate through lastCall or a shared "current request". `withObserver` can be called on an existing request scope to add a listener, inherited along sub-scopes; this is immutable wiring and leaves the original scope unaffected. Canonical methods are bound to their original receivers at assembly time. When a reusable Engine has no request scope configured, each run is assigned an independent identity.
91
91
 
92
- Listeners without accepts receive metadata only. Content events, payload and subs are collected only after the corresponding consumer explicitly accepts them; other metadata consumers do not gain the body as a side effect. Each consumer receives an independent copy. The result snapshot is isolated from the actual return value, so preview edits do not rewrite the final value; model results must still satisfy the existing Journal snapshot contract.
92
+ Listeners without accepts receive metadata only. Content events, payload and sub-log content are collected only after the corresponding consumer explicitly accepts them; other metadata consumers do not gain the body as a side effect. Each consumer receives an independent copy. The result snapshot is isolated from the actual return value, so preview edits do not rewrite the final value; model results must still satisfy the existing Journal snapshot contract.
93
93
 
94
94
  ModelPart does not provide separate reasoning content chunks. State, process notes explicitly provided by the model, tool facts, transient previews and the final answer each keep their own meaning; internal hidden reasoning or the raw protocol must not be treated as a user reply. Selection, compression and assembly can be registered as ordinary nodes separately and use real lifecycle facts; algorithmic reports are delivered as permitted node.progress.
95
95
 
@@ -97,7 +97,7 @@ Adding a listener only changes assembly configuration; the original Workflow, ru
97
97
 
98
98
  ## Sub-log instances and record-first-then-complete
99
99
 
100
- Workflow nodes, simple run(step), ToolRegistry/Support tools and the execution context of executeInvocation all provide `createSubFlowLogger()` and `report()`. Each node visit or call has one instance, and repeated create returns the same instance; no global logger is shared. subs accepts only SubFlowLogger instances belonging to that execution; plain objects, objects imitating readAll, hand-written arrays and readAll() snapshots are rejected; invalid material does not become a main execution fact. The normalized stored field subSteps is still an array, so reading historical data does not require re-creating a logger.
100
+ Workflow nodes, simple run(step), ToolRegistry/Support tools and the execution context of executeInvocation all provide `createSubFlowLogger()` and `report()`. Each node visit or call has one instance, and repeated create returns the same instance; no global logger is shared. The factory is already bound to the execution context; no context argument is required. Identity inherits the App diagnostic scope and identifies the Engine definition, run and node visit without retaining App service objects. The Loop owns the lifecycle: each accepted write/fix immediately commits an immutable snapshot to runtime before publishing node.progress to observers and the log outlet. Nodes never submit a logger or readAll() result. Terminal events use the committed runtime snapshot without reading the logger. Node completion detaches writes and releases the runtime reference; failure and cancellation preserve prior records. A later visit gets a new instance. readAll() remains readable by any holder and emits nothing. Nodes, Apps and external code may retain or lend the instance; Core does not prescribe its use. A later visit receives a new logger and may consult the previous flow through a retained old instance. Apps apply each appended fix to the original write view using operation/targetStepId and order revisions with revision. Stored subSteps remain fixed-format arrays.
101
101
 
102
102
  ```ts
103
103
  async run(state, context) {
@@ -116,23 +116,49 @@ async run(state, context) {
116
116
  if (!context.signal.aborted) subs.fix(id, { status: 'error' });
117
117
  throw error;
118
118
  }
119
- context.report({ usedModel: false, subs });
119
+ context.report({ usedModel: false });
120
120
  }
121
121
  ```
122
122
 
123
123
  | Method | Fixed contract |
124
124
  | --- | --- |
125
- | write(input): string | name and kind are required, status defaults to planned; stepId, sequence, createdAtMs and initial revision=1 are allocated automatically. Optional parentStepId, relatedDefinitionId, caseId, nextStepIds, status, time, summary, payload and usedModel. startedAtMs is recorded automatically only when running is reported explicitly, so planned time does not pass for an actual start. |
126
- | fix(stepId, patch): void | Completes status, model usage, pending times, summary, successors or payload fields for a record in the same instance; a terminal state cannot be reopened, but missing result fields can still be filled while execution is active. Keeps the ID, first order and original times, incrementing revision each time. payload merges missing keys recursively and existing facts cannot be overwritten; a given summary, time or successor cannot be rewritten. |
127
- | readAll(): readonly RecordedSubStep[] | Returns an immutable snapshot in first-write order; later fix calls do not change an earlier snapshot. With no steps it is [], and the snapshot remains readable after the execution is closed. |
125
+ | write(input): string \| -1 | name and kind are required, status defaults to planned; stepId, sequence, createdAtMs and initial revision=1 are allocated automatically. Optional parentStepId, relatedDefinitionId, caseId, nextStepIds, status, time, summary, payload and usedModel. startedAtMs is recorded automatically only when running is reported explicitly, so planned time does not pass for an actual start. |
126
+ | fix(stepId: string \| -1, patch): boolean | Completes status, model usage, pending times, summary, successors or payload fields for a record in the same instance; a terminal state cannot be reopened, but missing result fields can still be filled while execution is active. Every successful fix appends an independent record with operation=fix, a new stepId, sequence and createdAtMs. targetStepId references the original write and revision increases for that original. Each fix carries the resulting state without changing any earlier row. payload merges missing keys recursively and existing facts cannot be overwritten; a given summary, time or successor cannot be rewritten. |
127
+ | readAll(): readonly RecordedSubStep[] | Returns all write/fix records in append order; repeated fixes create separate rows and never mutate earlier rows or snapshots. With no steps it is [], and the snapshot remains readable after the execution is closed. |
128
128
 
129
- The fixed record fields are origin=reported, stepId, sequence, name, kind, status, createdAtMs and revision, plus the optional fields above. planned/unknown can advance to an actual status, while running cannot go back to planned or skipped; success/error/cancelled/skipped are terminal, the status and established facts cannot be rewritten, but pending result fields can still be filled while the owning node is active. Statuses and legal transitions come from the same reportedStepStatusCatalog. A retry writes another record and does not reopen a terminal state. The parent and successors must belong to the same logger; when the successor ID is not yet available, nextStepIds can be completed in one later fix. Pending payload fields are omitted first and are not represented by null or an empty array as an overwritable placeholder. By default at most 256 records, 65536 JSON characters for the full snapshot and 1024 write/fix calls; payload accepts only bounded-depth JSON data and does not accept functions, service instances, circular references or internal getters.
129
+ Records have origin=reported, operation=write/fix, unique stepId, append sequence, name, kind, status, createdAtMs and revision. Fix records reference the original write with targetStepId; fixing a fix ID is rejected. Historical projected snapshots may omit operation. planned/unknown can advance to an actual status, while running cannot go back to planned or skipped; success/error/cancelled/skipped are terminal, the status and established facts cannot be rewritten, but pending result fields can still be filled while the owning node is active. Statuses and legal transitions come from the same reportedStepStatusCatalog. A retry writes another record and does not reopen a terminal state. The parent and successors must belong to the same logger; when the successor ID is not yet available, nextStepIds can be completed in one later fix. Pending payload fields are omitted first and are not represented by null or an empty array as an overwritable placeholder. By default at most 256 original writes, 1024 total write/fix records and 65536 JSON characters for the complete ledger; payload accepts only bounded-depth JSON data and does not accept functions, service instances, circular references or internal getters.
130
130
 
131
- write and fix emit the initial/completion event through the existing node.progress at that moment, with payload.subStep holding operation, stepId, sequence, revision, createdAtMs, status and optional usedModel metadata. When content is permitted, the event carries the fixed-format subSteps snapshot at that point; node end/failure events also carry the last snapshot. The log appends revisions, and events already emitted are not updated in place. Results returning out of order do not change the initial order, and replay can distinguish "only a plan at the time" from "a result later". When only a plan or start was received without a result, the original status is kept; success, failure or an actual model call cannot be inferred.
131
+ write and fix emit the initial/completion event through the existing node.progress at that moment, with payload.subStep holding operation, current record stepId, targetStepId for fixes, sequence, revision, createdAtMs, status and optional usedModel metadata. When content is permitted, the event carries the fixed-format subSteps snapshot at that point; node end/failure events also carry the last snapshot. The log appends revisions, and events already emitted are not updated in place. Results returning out of order do not change the initial order, and replay can distinguish "only a plan at the time" from "a result later". When only a plan or start was received without a result, the original status is kept; success, failure or an actual model call cannot be inferred.
132
132
 
133
133
  The main node reports through context.report({ usedModel: true | false }) whether it used the model this time, and the outer usedModel in the log is kept as metadata; sub-steps report their own usedModel through write/fix. Unreported stays unknown and does not default to false; a reported true cannot change back to false. A child's true is not automatically aggregated into the main node's original marker. This is a node report: a field of true does not fabricate model.start/end or call counts, and model internal hidden reasoning is not collected. The end report(update, context) is still evaluated only when content collection is permitted; markers that must be kept in metadata mode should be submitted during the run with context.report.
134
134
 
135
- Content permission continues to be decided per event and per consumer. Without content authorization, payload/summary getters are not read, nor are the private payload/subs getters in reports; only permitted status, revision and boolean metadata are kept. The instance closes when execution ends or is cancelled, and write/fix reject late modifications. Log shutdown, outlet failure or a process crash still follow the original failure isolation and incomplete mechanism; recording intent first does not constitute a transactional audit guarantee.
135
+ Content permission continues to be decided per event and per consumer. Without content authorization, payload/summary getters are not read, nor are the private payload getters in reports; only permitted status, revision and boolean metadata are kept. The instance closes when execution ends or is cancelled, and write returns numeric -1 and fix returns false without throwing, inspecting arguments, recording or emitting. readAll remains readable. fix returns true on success; invalid IDs, invalid input, forbidden revisions and detachment all return false without throwing, changing the record or emitting a revision. write returns the new ID on success and -1 on any failure, including invalid input, limits and detachment, without throwing, recording or emitting. Log shutdown, outlet failure or a process crash still follow the original failure isolation and incomplete mechanism; recording intent first does not constitute a transactional audit guarantee.
136
+
137
+ ## Subnode grouping and restricted delegation
138
+
139
+ subnodeId groups multiple records within one node visit. It is independent of stepId, which identifies a log record. A group can contain multiple writes and multiple fixes per write; ungrouped logs belong to the parent node. Grouping does not imply dispatch, contiguous phases, scheduling or dependencies.
140
+
141
+ SubFlowLogger and ScopedSubFlowLogger are independent, non-inheriting classes implementing SubFlowLog. The full class owns storage/lifecycle, accepts subnodeId directly on write, and may fork(subnodeId). A scoped instance holds only the original logger and bound ID: write adds the ID, fix checks original-record ownership and delegates. It cannot override its group, fork or be supplied to native invoke. Both readAll methods return the same complete immutable ledger, including other groups.
142
+
143
+ Business type is an arbitrary string; operation is the separate write/fix enum. type and the compatible kind classification fill each other's omitted default. Fix uses the explicit SubFlowStepFix whitelist (status, usedModel, times, summary, nextStepIds, payload), not Partial of the write input. Identity, group, business type and label cannot be revised. A rejected fix returns false; rejected writes return -1. Safe console warnings explain fixed reasons, deduplicated per logger, without payloads or arbitrary error text; console failures do not change results.
144
+
145
+ Registered subnode IDs are local to one node visit: kind:encodedDefinitionId:ordinal, for example tool:product.references:1. Definition IDs are URL-encoded. The owning logger allocates ordinals per kind/definition at call start; failure and cancellation never reuse them, and a new node visit starts from 1. Parent identity stays in its existing fields. relatedInvocationId separately retains the real call identity for correlation with model/tool events; a short ordinal never replaces invocationId. App-owned custom groups need no business enum. Direct write({subnodeId, type, name}) and fork(subnodeId).write({type, name}) are equivalent grouping choices. fork validates its ID and native invoke rejects scoped/forged logger inputs at the boundary.
146
+
147
+ ## Optional native call records
148
+
149
+ logger is a per-call choice. Omission creates no native sub-log; passing it produces a fixed write followed by a separate fix on success, error or cancellation. Components do not create owning logger storage or implicitly forward full logging authority. Tools expose a scoped delegate bound by Core to the actual invocation. It may be lent to business helpers but cannot fork or be passed to another invoke. Business steps remain App-owned.
150
+
151
+ | Entry | Explicit logger location |
152
+ | --- | --- |
153
+ | ModelRunner / Workflow model.invoke; ToolRegistry / Support tool invocation | request.logger |
154
+ | executeInvocation / Tools lifecycle | ports.logger(context) |
155
+ | Retrieval.run; Answering task.read/readAsync, checks.check, flow.run; output.compose | context.logger |
156
+ | Knowledge.read; recovery chain tryRecover | Optional second context argument |
157
+ | Support.processContext / context node run | ports.logger / explicitly supplied context.logger |
158
+ | retryMultipleModelCalls | request.logger; records only an actual fallback attempt |
159
+ | Delivery.deliver; Aggregation.collect | execution.logger; optional third execution argument respectively |
160
+
161
+ Fixed records contain producer/definition identity and execution status, never automatic copies of prompts, results, tool arguments or customer material. A checker returning a rejection or a recovery returning unhandled still completed normally; callers choose whether to record the business result. Existing Core events and receipts remain unchanged. Pure configuration, lookup and formatting functions do not record.
136
162
 
137
163
  ## File adaptation and relational table mapping
138
164
 
@@ -83,13 +83,13 @@ await agent.run(state);
83
83
  | execution.observe.event | Loop/Workflow、model.start/end、工具开始/结束及获准的动态报告 | 旁路;同步抛错、异步拒绝或悬挂不改变执行;关闭持久日志仍有效 |
84
84
  | execution.diagnostics → CoreLogOutput.write | 同一事实的可选持久投影 | App 决定字段、正文、存储和保留期;不获得其他监听器的内容授权 |
85
85
  | ModelRunner.sink.output | 同一外层的 model.start/output/parsed,以及成功、失败或取消时 model.end | 正文按序等待、传播失败、保留背压;结束通知独立限时250毫秒,其失败不覆盖结果或原始异常 |
86
- | context.report({ usedModel, payload, subs }) | 当前节点运行中的即时报告 | usedModel 为可选布尔元数据,payload/subs 仅内容获准才读取;Core 补齐身份、时间和序号,节点结束或取消后的报告丢弃 |
87
- | 节点 report(update, context) | 完成/跳转时的 usedModel/payload/subs | 内容获准才求值,不替代运行中的即时报告 |
86
+ | context.report({ usedModel, payload }) | 当前节点运行中的即时报告 | usedModel 为可选布尔元数据,payload 仅内容获准才读取;Core 补齐身份、时间和序号,节点结束或取消后的报告丢弃 |
87
+ | 节点 report(update, context) | 完成/跳转时的 usedModel/payload | 内容获准才求值,不替代运行中的即时报告 |
88
88
  | context.createSubFlowLogger() | 绑定本次执行的子日志实例 | write 先留记录,fix 随后补全,readAll 取得固定格式快照;复用既有 node.progress 出口 |
89
89
 
90
90
  监听器可用 event、nodeId、workflowNodeId 等元数据过滤。workflowNodeId 保留所属 Workflow 节点,模型/工具边界自身仍有 nodeId 和 invocationId。重复访问同一节点有不同 span,并发运行不借助 lastCall 或共享“当前请求”关联。可在已有请求作用域上调用 `withObserver` 增加监听,沿子作用域继承;这是不可变的接线,原作用域不受影响。规范方法在装配时绑定原接收者。可复用 Engine 未配置请求作用域时,每次 run 分配独立身份。
91
91
 
92
- 无 accepts 的监听器仅接元数据。内容事件、payload 和 subs 只在对应消费者明确接纳后采集;其他元数据消费者不会连带获得正文。消费者拿到独立副本。结果快照与实际返回值隔离,不因预览修改而改写最终值;模型结果仍须符合既有 Journal 快照合同。
92
+ 无 accepts 的监听器仅接元数据。内容事件、payload 和子日志正文 只在对应消费者明确接纳后采集;其他元数据消费者不会连带获得正文。消费者拿到独立副本。结果快照与实际返回值隔离,不因预览修改而改写最终值;模型结果仍须符合既有 Journal 快照合同。
93
93
 
94
94
  ModelPart 不提供独立推理内容分片。状态、模型明确提供的过程说明、工具事实、暂态预览和最终答案各自保持含义,不能把内部隐藏推理或原始协议当用户回复。选择、压缩与装配可分别注册普通节点,使用真实生命周期事实;算法报告按获准的 node.progress 交付。
95
95
 
@@ -97,7 +97,7 @@ ModelPart 不提供独立推理内容分片。状态、模型明确提供的过
97
97
 
98
98
  ## 子日志实例与先记后补
99
99
 
100
- Workflow 节点、简易 run(step)、ToolRegistry/Support 工具及 executeInvocation 的执行 context 均提供 `createSubFlowLogger()` 和 `report()`。每次节点访问或调用各有一个实例,多次 create 返回同一实例;不共用全局 logger。subs 只接受所属执行的 SubFlowLogger 实例,普通对象、仿造 readAll 的对象、手写数组和 readAll() 快照均会拒收;非法材料不成为主执行事实。规范化后的存储字段 subSteps 仍为数组,读取历史数据不需要重新创建 logger。
100
+ Workflow 节点、简易 run(step)、ToolRegistry/Support 工具及 executeInvocation 的执行 context 均提供 `createSubFlowLogger()` 和 `report()`。每次节点访问或调用各有一个实例,多次 create 返回同一实例;不共用全局 logger。创建入口已绑定执行 context,无需再次传参;身份沿 App 装配的诊断作用域继承,自动关联当前 Engine 定义、run 与 node 访问,不保存 App 服务对象。Loop 接管生命周期,write/fix 接受后立即把不可变快照交给 runtime,再通过 node.progress 通知 App 与日志出口。节点不提交 logger 或 readAll() 的结果;终态事件使用 runtime 已接收的快照,不回读 logger。节点结束时解绑写入并释放 runtime 的实例引用,失败与取消保留此前记录;再次访问同一节点创建独立实例。readAll() 向任何持有者提供持续只读查询,不产生事件或提交。实例可由节点、App 或外部代码保留并借用,Core 不限制用途;后续节点访问获得新实例,旧实例可用于读取上次 flow。App 按 operation 与 targetStepId 将追加的 fix 应用到原 write 对应的界面视图,按 revision 判断修订顺序。存储字段 subSteps 为固定格式数组。
101
101
 
102
102
  ```ts
103
103
  async run(state, context) {
@@ -116,23 +116,76 @@ async run(state, context) {
116
116
  if (!context.signal.aborted) subs.fix(id, { status: 'error' });
117
117
  throw error;
118
118
  }
119
- context.report({ usedModel: false, subs });
119
+ context.report({ usedModel: false });
120
120
  }
121
121
  ```
122
122
 
123
123
  | 方法 | 固定合同 |
124
124
  | --- | --- |
125
- | write(input): string | name、kind 必填,status 默认 planned;自动分配 stepId、sequence、createdAtMs 和初始 revision=1。可选 parentStepId、relatedDefinitionId、caseId、nextStepIds、状态、时间、摘要、payload 和 usedModel。只有明确报告 running 才自动记 startedAtMs,计划时间不冒充实际开始。 |
126
- | fix(stepId, patch): void | 给同一实例内的记录补状态、模型使用、待定时间、摘要、后继或 payload 字段;终态不能重开,但执行仍活跃时可补缺失的结果字段。保留 ID、首次顺序和原时间,每次增加 revision。payload 递归补缺失键,已有事实不能覆盖;已给定的摘要、时间和后继不能改写。 |
127
- | readAll(): readonly RecordedSubStep[] | 按首次 write 顺序返回不可变快照;之后的 fix 不改变先前快照。没有步骤为 [],执行关闭后仍可读快照。 |
125
+ | write(input): string \| -1 | name 必填,type 为自定义业务类型;兼容的 kind 分类与 type 至少提供一项,缺省时互为补足,status 默认 planned;自动分配 stepId、sequence、createdAtMs 和初始 revision=1。可选 parentStepId、relatedDefinitionId、caseId、nextStepIds、状态、时间、摘要、payload 和 usedModel。只有明确报告 running 才自动记 startedAtMs,计划时间不冒充实际开始。 |
126
+ | fix(stepId: string \| -1, patch): boolean | 给同一实例内的记录补状态、模型使用、待定时间、摘要、后继或 payload 字段;终态不能重开,但执行仍活跃时可补缺失的结果字段。每次成功均追加独立记录,operation=fix,分配新 stepId、sequence、createdAtMs;targetStepId 指向原 write 的 ID,revision 按该原记录递增。记录携带本次修订后的完整状态,不修改原 write 或先前 fix。payload 递归补缺失键,已有事实不能覆盖;已给定的摘要、时间和后继不能改写。 |
127
+ | readAll(): readonly RecordedSubStep[] | 按实际追加顺序返回全部 write/fix 记录;多次 fix 对应多条记录,旧行和旧快照保持不变。没有步骤为 [],执行关闭后仍可读快照。 |
128
128
 
129
- 记录的固定字段为 origin=reported、stepId、sequence、name、kind、status、createdAtMs、revision,及上述可选字段。planned/unknown 可推进到实际状态,running 不能退回计划或 skipped;success/error/cancelled/skipped 为终态,状态和已确定的事实不能改写,但所属节点仍活跃时可以继续填入等待中的结果字段。状态及合法推进关系来自同一 reportedStepStatusCatalog。重试另 write 一条,不重开终态。父、后继必须属于同一 logger;尚未取得后继 ID 时可在后续 fix 中一次补齐 nextStepIds。待填 payload 字段先省略,不以 null 或空数组冒充可覆盖的占位。默认最多256条、全部快照65536个JSON字符、1024次 write/fix;payload 只收有限深度的 JSON 数据,不接受函数、服务实例、循环引用或内部 getter。
129
+ 记录的固定字段为 origin=reported、operation=write/fix、唯一 stepId、追加序号 sequence、name、kind、status、createdAtMs、revision;fix 另有 targetStepId,始终关联原 write,不能以 fix 记录的 ID 再修订。历史投影快照可缺省 operation。planned/unknown 可推进到实际状态,running 不能退回计划或 skipped;success/error/cancelled/skipped 为终态,状态和已确定的事实不能改写,但所属节点仍活跃时可以继续填入等待中的结果字段。状态及合法推进关系来自同一 reportedStepStatusCatalog。重试另 write 一条,不重开终态。父、后继必须属于同一 logger;尚未取得后继 ID 时可在后续 fix 中一次补齐 nextStepIds。待填 payload 字段先省略,不以 null 或空数组冒充可覆盖的占位。默认最多256条原始 write、总计1024条 write/fix 记录、全部序列65536个JSON字符;payload 只收有限深度的 JSON 数据,不接受函数、服务实例、循环引用或内部 getter。
130
130
 
131
- write 和 fix 当时就经既有 node.progress 发射初始/补全事件,payload.subStep 保存 operation、stepId、sequence、revision、createdAtMs、status 和可选 usedModel 元数据。内容获准时,事件附带该时点的固定格式 subSteps 快照;节点结束/失败事件也附带最后快照。日志追加修订,已经输出的事件不原地更新。结果乱序返回不改变初始顺序,回放可区分“当时只有计划”和“后来有了结果”。只收到计划或开始而没有结果时保持原状态;不能推断已经成功、失败或实际调用了模型。
131
+ write 和 fix 当时就经既有 node.progress 发射初始/补全事件,payload.subStep 保存 operation、当前记录 stepId、fix 的 targetStepId、sequence、revision、createdAtMs、status 和可选 usedModel 元数据。内容获准时,事件附带该时点的固定格式 subSteps 快照;节点结束/失败事件也附带最后快照。日志追加修订,已经输出的事件不原地更新。原始 write 不因乱序结果而重新排序;fix 按实际接收顺序追加,回放可区分“当时只有计划”和“后来有了结果”。只收到计划或开始而没有结果时保持原状态;不能推断已经成功、失败或实际调用了模型。
132
132
 
133
133
  主节点通过 context.report({ usedModel: true | false }) 上报本次自身是否使用模型,日志外层 usedModel 作为元数据保留;子步骤通过 write/fix 上报自身的 usedModel。未上报保持未知,不默认 false;已报告 true 不能改回 false。子节点的 true 不自动汇总进主节点原始标记。此为节点报告,不因字段为 true 而补造 model.start/end 或调用次数,也不采集模型内部隐藏推理。结束 report(update, context) 仍仅在内容采集获准时求值,需在元数据模式保留的标记应在运行中用 context.report 提交。
134
134
 
135
- 内容权限继续按事件和消费者分别判断。未获内容授权时不读取 payload/summary getter,也不读取报告中的私有 payload/subs getter;只保留已获准的状态、修订和布尔元数据。执行结束或取消后实例关闭,write/fix 拒绝迟到修改。日志关闭、出口失败或进程崩溃仍遵守原失败隔离及 incomplete 机制,先记意图不构成事务审计保证。
135
+ 内容权限继续按事件和消费者分别判断。未获内容授权时不读取 payload/summary getter,也不读取报告中的私有 payload getter;只保留已获准的状态、修订和布尔元数据。执行结束或取消后实例关闭,write 固定返回数字 -1,fix 返回 false,不报错、不校验或读取入参、不入账或发事件;readAll 持续可读。fix 成功返回 true;无效 ID、非法输入、禁止的修订及解绑等失败统一返回 false,不抛错、不改变原记录或产生修订事件。write 成功返回新 ID,非法输入、容量限制、解绑等任何失败统一返回 -1,不抛错、不入账或发事件。日志关闭、出口失败或进程崩溃仍遵守原失败隔离及 incomplete 机制,先记意图不构成事务审计保证。
136
+
137
+ ## 子节点归集与受限委托
138
+
139
+ `subnodeId` 是同次 node 执行内的逻辑归集身份,不是某条日志的 ID。一个子节点可以拥有多条 write,以及每条 write 的多次 fix;未指定 subnodeId 的记录直接归父 node。它不规定分发、连续分段、执行顺序或依赖关系。App 按实际职责组织自定义子节点,无需为了日志增加调度节点。
140
+
141
+ - `SubFlowLogger` 是完整类,独占记录存储与生命周期。write 可直接填写任意非空字符串 subnodeId,无业务枚举;也可通过 fork(subnodeId) 创建另一个实例。
142
+ - `ScopedSubFlowLogger` 是独立且不继承完整类的委托类,只持有原 logger 与绑定 ID,不保存记录。write 自动补归属并拒绝覆盖;fix 只能修订该子节点的原记录,越界返回 false。没有 fork,不能作为原生 invoke 的 logger。
143
+ - 两类实现共同读写接口 `SubFlowLog`;它们的 readAll 返回同一份全量不可变记录,包括其他分组。读权限不会扩大受限实例的写权限。
144
+ - fix 使用独立 `SubFlowStepFix`,不是 Partial<write参数>。只接受 status、usedModel、startedAtMs、endedAtMs、summary、nextStepIds、payload,仍受状态推进和已有事实保护。不能改变 subnodeId、type、kind、name、stepId 或其他身份字段。
145
+ - type 是可自定义的业务日志类型,operation 才是 write/fix。fix 继承原记录的业务类型和归属;每次成功都追加独立记录。
146
+
147
+ ```ts
148
+ const logger = context.createSubFlowLogger();
149
+ // 可直接指定分组,fork 不是必需步骤。
150
+ logger.write({ subnodeId: 'prepare', type: 'source.read', name: '读取资料' });
151
+ const logs: SubFlowLog = logger.fork('prepare');
152
+ prepareMaterials(logs); // 子函数只需要公共读写接口。
153
+ await context.tools.invoke({ toolId: 'read', input: {}, logger });
154
+ // Core 传给真实工具的 execution.logger 是 ScopedSubFlowLogger。
155
+ ```
156
+
157
+ 注册子节点 ID 只在本次 node 执行内唯一,格式为 kind:encodedDefinitionId:ordinal,例如 tool:product.references:1。定义 ID 使用 URL 编码,计数按能力种类与定义 ID 分开,在调用开始时分配;失败、取消不复用,下一次 node 访问从1开始。父级访问身份已有独立字段,不重复拼入子节点 ID。真实调用身份单独保存在 relatedInvocationId,用于对应模型/工具主事件,不以短序号代替 invocationId。重复/并发调用分别归集,同次调用的多条日志保持相同 subnodeId。自定义子节点由 node 自行命名,两种归集都没有业务枚举约束。
158
+
159
+ fix 的失败统一返回 false,write 的失败统一返回 -1;调试控制台使用固定原因输出 warn,同一 logger 同类原因去重,不包含入参、正文或任意错误文本。控制台自身异常不改变返回值。fork 对无效 ID 的参数检查与 invoke 对非完整 logger 的边界检查仍保留。
160
+
161
+ ## 原生调用的可选记录
162
+
163
+ logger 是调用级参数,不是全局开关。未传入时不产生原生子日志,开发者可以自行组织全部记录;显式传入时原生组件统一 write 一条运行记录,并在成功、失败或取消时 fix,形成两条独立记录。不自动创建完整 logger 存储或向所有下游传播完整 logger 权限,不改变它所属的 App、Engine、node 或生命周期。Tool handler 取得 Core 为本次调用绑定的 execution.logger,可把该受限委托交给内部业务函数;不能继续 fork 或传给另一次 invoke。
164
+
165
+ | 原生入口 | 显式传入位置 | 固定记录 |
166
+ | --- | --- | --- |
167
+ | ModelRunner.invoke/Workflow model.invoke | request.logger | Model call,关联阶段 ID |
168
+ | ToolRegistry.invoke/invokeAsync、Support 工具 | request.logger | Tool call,关联工具 ID |
169
+ | executeInvocation/Tools Kit 生命周期 | ports.logger(context) | Invocation,关联 handler ID |
170
+ | Retrieval.run | context.logger | Retrieval |
171
+ | Answering task.read/readAsync、checks.check、flow.run | context.logger | 证据读取、每个实际 checker、完整答复流程 |
172
+ | Knowledge.read | 可选第二参数的 logger | Read knowledge |
173
+ | Presentation outputs.compose | context.logger | Compose output |
174
+ | Support.processContext、上下文节点 run | ports.logger/显式提供的 context.logger | Process context |
175
+ | Support.retryMultipleModelCalls | request.logger | 只在实际降级重试时记录 Retry model proposal |
176
+ | Policies 恢复链 tryRecover | 可选第二参数的 logger | 每个实际尝试的恢复策略 |
177
+ | Delivery.deliver | execution.logger | Deliver result |
178
+ | Aggregation.collect | 可选第三参数的 logger | Aggregate queries |
179
+
180
+ 固定记录只描述执行边界,不自动复制模型输入输出、工具参数或客户材料。检查正常返回不通过、恢复正常返回未处理,都属于函数成功返回;业务结果由开发者按需补充。已有 Core 主事件继续提供原有调用事实;可选子日志用于节点内部编排展示,不替代主事件、回执或权限检查。纯配置、清单查询和格式化函数不追加日志。
181
+
182
+ ```ts
183
+ const logger = context.createSubFlowLogger();
184
+ await context.model.invoke({ input: '简要说明', logger });
185
+ await context.tools.invoke({ toolId: 'read', input: {}, logger });
186
+ // 此次调用未传 logger,不追加原生子日志。
187
+ await context.tools.invoke({ toolId: 'read', input: {} });
188
+ ```
136
189
 
137
190
  ## 文件适配与关系表映射
138
191