@xgjktech/xg-openclaw-harness-tools 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -24
- package/dist/plan-execute-tool.d.ts +2 -16
- package/dist/plan-execute-tool.d.ts.map +1 -1
- package/dist/plan-execute-tool.js +16 -125
- package/dist/plan-origin.d.ts +9 -0
- package/dist/plan-origin.d.ts.map +1 -0
- package/dist/plan-origin.js +33 -0
- package/dist/plan-view.d.ts +3 -0
- package/dist/plan-view.d.ts.map +1 -0
- package/dist/plan-view.js +26 -0
- package/dist/plan-write-tool.d.ts +2 -6
- package/dist/plan-write-tool.d.ts.map +1 -1
- package/dist/plan-write-tool.js +50 -37
- package/package.json +2 -2
- package/dist/notify.d.ts +0 -16
- package/dist/notify.d.ts.map +0 -1
- package/dist/notify.js +0 -20
package/README.md
CHANGED
|
@@ -3,26 +3,22 @@
|
|
|
3
3
|
OpenClaw 第三方工具插件:企业计划工具 `xg_plan_write` / `xg_plan_execute`。
|
|
4
4
|
|
|
5
5
|
用一份持久化的计划(SQLite 落盘)取代内置 `update_plan`(无状态、只回显、不落库),
|
|
6
|
-
|
|
6
|
+
`xg_plan_write` 整张维护草稿计划,`xg_plan_execute` 只读查看计划或最近摘要。
|
|
7
7
|
|
|
8
8
|
## 架构裁决:实时事件当前处于事实性休眠(务必先读)
|
|
9
9
|
|
|
10
|
-
**当前 OpenClaw 2026.6.10
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
`notify.emitPlanEvent` 据此恒定返回 `{emitted:false,reason:"no runId"}`,**不会真的发出事件**
|
|
14
|
-
(不伪造 runId 是硬性红线,见 `docs/04-互通设计.md` §2.4)。
|
|
10
|
+
**当前 OpenClaw 2026.6.10 版本下,实时事件推送链路休眠**:工具 execute/factory 期没有
|
|
11
|
+
正当途径获取真实 runId(详见 `docs/notes-plugin-entry.md` N2),且 0.3.0 已移除工具侧
|
|
12
|
+
`notify.ts`。`xg_plan_write` 落库后不发事件,`xg_plan_execute` 全程只读。
|
|
15
13
|
|
|
16
14
|
**因此 IM 侧(或任何下游消费方)不得依赖实时事件感知计划进度。** 当前唯一可靠的主感知路径是:
|
|
17
15
|
|
|
18
|
-
1. **SQLite
|
|
19
|
-
|
|
16
|
+
1. **SQLite 读取**——IM 用 `openPlanStoreReadonly(dbPath)` 按 origin 只读反查
|
|
17
|
+
`<stateDir>/xgjktech/plans.sqlite` 并推送全量快照,这是权威状态源。
|
|
20
18
|
2. **agent 最终回复**——agent 完成任务后的文本回复本身会体现进度。
|
|
21
19
|
|
|
22
|
-
事件契约(`PlanEventPayload` / `isPlanEventPayload` / `planEventStream
|
|
23
|
-
|
|
24
|
-
获取当次 `runId` 的 API,事件推送即可在**不改本插件代码**的前提下自动激活(`notify.ts` 的判断只看
|
|
25
|
-
`payload.runId` 是否非空)。
|
|
20
|
+
事件契约(`PlanEventPayload` / `isPlanEventPayload` / `planEventStream`)在 shared 中原样保留,
|
|
21
|
+
仅用于下游编译兼容。未来恢复实时事件需要重新实现工具侧生产与 IM 侧消费,并单独发布。
|
|
26
22
|
|
|
27
23
|
## 环境要求
|
|
28
24
|
|
|
@@ -94,15 +90,13 @@ P1 阶段不锁死具体安装命令,避免与实际安装流程漂移(详
|
|
|
94
90
|
|
|
95
91
|
### `xg_plan_write`
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
整张创建或覆盖一份草稿计划(goal + tasks[])并写入 SQLite。更新时必须重交所有要保留的
|
|
94
|
+
task;写入 schema 不接受 steps,但读取旧计划仍兼容旧 steps 数据。
|
|
99
95
|
|
|
100
96
|
### `xg_plan_execute`
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
不变量校验 → 落盘 → 尽力而为发事件**(先落盘、事件不影响主流程成败)。`block_task`
|
|
105
|
-
必须携带非空 `note` 说明阻塞原因。
|
|
98
|
+
只读查看计划。带 planId 返回完整紧凑 view;不带 planId 返回最近 10 条计划摘要,用于跨会话
|
|
99
|
+
找回 planId。更新进度统一使用 `xg_plan_write` 整张重交。
|
|
106
100
|
|
|
107
101
|
两个工具的完整参数 schema 见 `src/plan-write-tool.ts` / `src/plan-execute-tool.ts`
|
|
108
102
|
中的 `PLAN_WRITE_DESCRIPTION` / `PLAN_EXECUTE_DESCRIPTION`。
|
|
@@ -128,25 +122,23 @@ P1 阶段不锁死具体安装命令,避免与实际安装流程漂移(详
|
|
|
128
122
|
3. 拉起一个真实 agent 会话,触发一次会列出可用工具的动作(例如让 agent 描述自己有哪些工具,
|
|
129
123
|
或查看 gateway/日志里该次 attempt 实际注册的工具名列表)。
|
|
130
124
|
4. 核对该工具列表:应包含 `xg_plan_write` 与 `xg_plan_execute`,**不应包含** `update_plan`。
|
|
131
|
-
5. 调用一次 `xg_plan_write`
|
|
132
|
-
`
|
|
133
|
-
一致,属预期行为非故障);用 `openPlanStoreReadonly` 反查该 planId,确认落盘内容与预期一致。
|
|
125
|
+
5. 调用一次 `xg_plan_write` 建一份最小计划,确认返回 `planId + view`;再用该 planId 调
|
|
126
|
+
`xg_plan_execute`,确认只读 view 一致。用 `openPlanStoreReadonly` 反查并核对落盘内容。
|
|
134
127
|
|
|
135
128
|
**本项在本次交付中未在真实 OpenClaw gateway 环境实机执行**——本仓库当前开发环境没有可用的
|
|
136
129
|
真实 OpenClaw gateway/agent 运行环境,因此无法诚实地报告"已实拉 agent 验证通过"。以下是
|
|
137
130
|
已经做过、可以作为间接佐证的验证,但**明确说明这不等于实机验证**:
|
|
138
131
|
|
|
139
|
-
-
|
|
140
|
-
`
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
策略引擎,**不能**佐证"生产 deny/allow 配置生效后工具清单精确符合预期"这一条——那一条
|
|
146
|
-
必须在真实 gateway 环境按上面 5 步实测。
|
|
132
|
+
- 当前 factory 集成测试用**手工构造的 mock `api`**捕获 2 个工具的注册工厂,并提供带
|
|
133
|
+
`sessionKey`/`deliveryContext` 的 `toolContext`:`xg_plan_write` 写入后通过只读存储反查
|
|
134
|
+
`origin`,`xg_plan_execute` 校验指定计划的 view 与最近计划列表。**这验证的是插件代码本身
|
|
135
|
+
可以被正确加载、注册并贯通当前契约**,但 mock `api` 不是真实 OpenClaw gateway 的
|
|
136
|
+
`tools.allow`/`deny` 策略引擎,**不能**佐证"生产 deny/allow 配置生效后工具清单精确符合
|
|
137
|
+
预期"这一条——那一条必须在真实 gateway 环境按上面 5 步实测。
|
|
147
138
|
- T6 阶段(`packages/xg-harness-tools/src/integration.test.ts`)用真实的 `SqlitePlanStore`
|
|
148
139
|
(临时目录)+ 真实工具函数(`buildPlanWriteTool`/`buildPlanExecuteTool`)跑通了完整的
|
|
149
|
-
|
|
140
|
+
写入→整张重交→execute 只读查看→SQLite 只读反查链路,但同样不经过 OpenClaw 的工具
|
|
141
|
+
allowlist/deny 策略引擎。
|
|
150
142
|
|
|
151
143
|
**结论:部署方在生产环境落实上述配置后,必须按"实机验证步骤"重新执行一次实拉 agent 验证,
|
|
152
144
|
不能以本仓库现有的 mock/集成测试结果代替。**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAYA,wBAmCG"}
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,12 @@ import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
|
|
2
2
|
import { resolvePlanDbPath, SqlitePlanStore } from "@xgjktech/xg-openclaw-shared";
|
|
3
3
|
import { PLAN_WRITE_DESCRIPTION, PlanWriteParams, buildPlanWriteTool } from "./plan-write-tool.js";
|
|
4
4
|
import { PLAN_EXECUTE_DESCRIPTION, PlanExecuteParams, buildPlanExecuteTool, } from "./plan-execute-tool.js";
|
|
5
|
+
import { toPlanOrigin } from "./plan-origin.js";
|
|
5
6
|
const PLUGIN_ID = "xg-harness-tools";
|
|
6
7
|
export default defineToolPlugin({
|
|
7
8
|
id: PLUGIN_ID,
|
|
8
9
|
name: "XG Harness Tools",
|
|
9
|
-
description: "
|
|
10
|
+
description: "企业工具插件:计划编写与查看(plan_write / plan_execute)",
|
|
10
11
|
tools: (tool) => {
|
|
11
12
|
// store 单例:defineToolPlugin 的 tools(...) 只在插件加载时调用一次,
|
|
12
13
|
// 闭包在此建一次即可;factory 本身会在每次 agent attempt 重建工具集时重新调用。
|
|
@@ -25,35 +26,15 @@ export default defineToolPlugin({
|
|
|
25
26
|
description: PLAN_WRITE_DESCRIPTION,
|
|
26
27
|
parameters: PlanWriteParams,
|
|
27
28
|
optional: true,
|
|
28
|
-
factory: ({ api }) => buildPlanWriteTool(getStore(api)),
|
|
29
|
+
factory: ({ api, toolContext }) => buildPlanWriteTool(getStore(api), toPlanOrigin(toolContext)),
|
|
29
30
|
}),
|
|
30
31
|
tool({
|
|
31
32
|
name: "xg_plan_execute",
|
|
32
|
-
label: "【xg-harness
|
|
33
|
+
label: "【xg-harness】查看计划",
|
|
33
34
|
description: PLAN_EXECUTE_DESCRIPTION,
|
|
34
35
|
parameters: PlanExecuteParams,
|
|
35
36
|
optional: true,
|
|
36
|
-
|
|
37
|
-
// 获取路径,routing.runId 只能留空——notify.emitPlanEvent 据此走
|
|
38
|
-
// {emitted:false,reason:"no runId"} 降级分支,不伪造 runId(04-互通设计 §2.4 红线1)。
|
|
39
|
-
factory: ({ api, toolContext }) => buildPlanExecuteTool({
|
|
40
|
-
store: getStore(api),
|
|
41
|
-
pluginId: PLUGIN_ID,
|
|
42
|
-
emitAgentEvent: (params) => api.agent.events.emitAgentEvent(params),
|
|
43
|
-
routing: {
|
|
44
|
-
runId: "",
|
|
45
|
-
...(toolContext.sessionKey !== undefined ? { sessionKey: toolContext.sessionKey } : {}),
|
|
46
|
-
...(toolContext.deliveryContext !== undefined
|
|
47
|
-
? { deliveryContext: toolContext.deliveryContext }
|
|
48
|
-
: {}),
|
|
49
|
-
...(toolContext.agentAccountId !== undefined
|
|
50
|
-
? { agentAccountId: toolContext.agentAccountId }
|
|
51
|
-
: {}),
|
|
52
|
-
...(toolContext.messageChannel !== undefined
|
|
53
|
-
? { messageChannel: toolContext.messageChannel }
|
|
54
|
-
: {}),
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
37
|
+
factory: ({ api }) => buildPlanExecuteTool({ store: getStore(api) }),
|
|
57
38
|
}),
|
|
58
39
|
];
|
|
59
40
|
},
|
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
-
import {
|
|
4
|
-
import { type EmitAgentEventFn } from "./notify.js";
|
|
3
|
+
import type { PlanStore } from "@xgjktech/xg-openclaw-shared";
|
|
5
4
|
export declare const PlanExecuteParams: Type.TObject<{
|
|
6
|
-
planId: Type.TString
|
|
7
|
-
action: Type.TUnion<[Type.TLiteral<"start_task">, Type.TLiteral<"complete_task">, Type.TLiteral<"block_task">, Type.TLiteral<"start_step">, Type.TLiteral<"complete_step">]>;
|
|
8
|
-
taskIndex: Type.TInteger;
|
|
9
|
-
stepIndex: Type.TOptional<Type.TInteger>;
|
|
10
|
-
note: Type.TOptional<Type.TString>;
|
|
5
|
+
planId: Type.TOptional<Type.TString>;
|
|
11
6
|
}>;
|
|
12
7
|
export type PlanExecuteParamsT = Static<typeof PlanExecuteParams>;
|
|
13
8
|
export declare const PLAN_EXECUTE_DESCRIPTION: string;
|
|
14
9
|
export declare function buildPlanExecuteTool(deps: {
|
|
15
10
|
store: PlanStore;
|
|
16
|
-
pluginId: string;
|
|
17
|
-
emitAgentEvent: EmitAgentEventFn;
|
|
18
|
-
routing: {
|
|
19
|
-
runId: string;
|
|
20
|
-
sessionKey?: string;
|
|
21
|
-
deliveryContext?: unknown;
|
|
22
|
-
agentAccountId?: string;
|
|
23
|
-
messageChannel?: string;
|
|
24
|
-
};
|
|
25
11
|
}): AnyAgentTool;
|
|
26
12
|
//# sourceMappingURL=plan-execute-tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-execute-tool.d.ts","sourceRoot":"","sources":["../src/plan-execute-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,
|
|
1
|
+
{"version":3,"file":"plan-execute-tool.d.ts","sourceRoot":"","sources":["../src/plan-execute-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,8BAA8B,CAAC;AAI3E,eAAO,MAAM,iBAAiB;;EAK7B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,wBAAwB,QAOzB,CAAC;AAOb,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GAAG,YAAY,CAsC7E"}
|
|
@@ -1,156 +1,47 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
2
|
+
import { renderPlanView } from "./plan-view.js";
|
|
3
3
|
import { toolJsonResult } from "./tool-json-result.js";
|
|
4
|
-
import { emitPlanEvent } from "./notify.js";
|
|
5
4
|
export const PlanExecuteParams = Type.Object({
|
|
6
|
-
planId: Type.String({
|
|
7
|
-
action: Type.Union([
|
|
8
|
-
Type.Literal("start_task"),
|
|
9
|
-
Type.Literal("complete_task"),
|
|
10
|
-
Type.Literal("block_task"),
|
|
11
|
-
Type.Literal("start_step"),
|
|
12
|
-
Type.Literal("complete_step"),
|
|
13
|
-
], { description: "推进动作" }),
|
|
14
|
-
taskIndex: Type.Integer({ minimum: 0, description: "目标 task 在 tasks[] 的下标" }),
|
|
15
|
-
stepIndex: Type.Optional(Type.Integer({ minimum: 0, description: "start_step/complete_step 时必填,目标 step 下标" })),
|
|
16
|
-
note: Type.Optional(Type.String({ description: "block_task 时必填,说明阻塞原因;其他动作可选备注" })),
|
|
5
|
+
planId: Type.Optional(Type.String({ description: "要查看的计划 id;留空则列出最近计划" })),
|
|
17
6
|
}, { additionalProperties: false });
|
|
18
7
|
export const PLAN_EXECUTE_DESCRIPTION = [
|
|
19
|
-
"
|
|
8
|
+
"用途:只读查看已保存的计划;不执行任务,也不修改计划。",
|
|
20
9
|
"",
|
|
21
|
-
"
|
|
22
|
-
"-
|
|
23
|
-
" 想开始新任务前,先把当前任务收尾(completed 或 blocked)。",
|
|
24
|
-
"- 验证后才 completed:只有真正做完并验证通过,才把 task/step 标 completed;",
|
|
25
|
-
" 没验证就别标完成。",
|
|
26
|
-
"- 遇阻塞就停下并记录:卡住时用 action=block_task 并在 note 写清楚卡在哪、需要谁/什么才能继续,",
|
|
27
|
-
" 不要绕过、不要假装完成。阻塞会被推送到群里等人回复——这正是本工具的价值。",
|
|
10
|
+
"- 带 planId:返回该计划当前的完整清单。",
|
|
11
|
+
"- 不带 planId:返回最近计划的摘要列表,用于忘记或跨会话找回 planId。",
|
|
28
12
|
"",
|
|
29
|
-
"
|
|
30
|
-
"每次成功推进都会落盘并发出进度事件;调用方无需手动通知。",
|
|
13
|
+
"需要更新进度时,用 xg_plan_write 重新提交整张计划。",
|
|
31
14
|
].join("\n");
|
|
32
|
-
class PlanExecuteValidationError extends Error {
|
|
33
|
-
code;
|
|
34
|
-
constructor(code, message) {
|
|
35
|
-
super(message ?? code);
|
|
36
|
-
this.name = "PlanExecuteValidationError";
|
|
37
|
-
this.code = code;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function requireTask(plan, taskIndex) {
|
|
41
|
-
const task = plan.tasks[taskIndex];
|
|
42
|
-
if (task === undefined) {
|
|
43
|
-
throw new PlanExecuteValidationError("INDEX_OUT_OF_RANGE", `taskIndex ${taskIndex} 越界`);
|
|
44
|
-
}
|
|
45
|
-
return task;
|
|
46
|
-
}
|
|
47
|
-
function requireStep(task, stepIndex) {
|
|
48
|
-
const step = task.steps?.[stepIndex];
|
|
49
|
-
if (step === undefined) {
|
|
50
|
-
throw new PlanExecuteValidationError("INDEX_OUT_OF_RANGE", `stepIndex ${stepIndex} 越界`);
|
|
51
|
-
}
|
|
52
|
-
return step;
|
|
53
|
-
}
|
|
54
|
-
function applyAction(plan, params) {
|
|
55
|
-
const task = requireTask(plan, params.taskIndex);
|
|
56
|
-
switch (params.action) {
|
|
57
|
-
case "start_task": {
|
|
58
|
-
task.status = "in_progress";
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
case "complete_task": {
|
|
62
|
-
task.status = "completed";
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
case "block_task": {
|
|
66
|
-
if (params.note === undefined || params.note.trim() === "") {
|
|
67
|
-
throw new PlanExecuteValidationError("NOTE_REQUIRED", "block_task 必须带非空 note");
|
|
68
|
-
}
|
|
69
|
-
task.status = "blocked";
|
|
70
|
-
task.note = params.note;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
case "start_step": {
|
|
74
|
-
if (params.stepIndex === undefined) {
|
|
75
|
-
throw new PlanExecuteValidationError("STEP_INDEX_REQUIRED", "start_step 必须带 stepIndex");
|
|
76
|
-
}
|
|
77
|
-
const step = requireStep(task, params.stepIndex);
|
|
78
|
-
step.status = "in_progress";
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
case "complete_step": {
|
|
82
|
-
if (params.stepIndex === undefined) {
|
|
83
|
-
throw new PlanExecuteValidationError("STEP_INDEX_REQUIRED", "complete_step 必须带 stepIndex");
|
|
84
|
-
}
|
|
85
|
-
const step = requireStep(task, params.stepIndex);
|
|
86
|
-
step.status = "completed";
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function summarize(plan) {
|
|
92
|
-
const summary = { total: 0, completed: 0, blocked: 0, inProgress: 0 };
|
|
93
|
-
for (const task of plan.tasks) {
|
|
94
|
-
summary.total += 1;
|
|
95
|
-
if (task.status === "completed")
|
|
96
|
-
summary.completed += 1;
|
|
97
|
-
if (task.status === "blocked")
|
|
98
|
-
summary.blocked += 1;
|
|
99
|
-
if (task.status === "in_progress")
|
|
100
|
-
summary.inProgress += 1;
|
|
101
|
-
}
|
|
102
|
-
return summary;
|
|
103
|
-
}
|
|
104
15
|
export function buildPlanExecuteTool(deps) {
|
|
105
|
-
const { store
|
|
16
|
+
const { store } = deps;
|
|
106
17
|
return {
|
|
107
18
|
name: "xg_plan_execute",
|
|
108
|
-
label: "【xg-harness
|
|
19
|
+
label: "【xg-harness】查看计划",
|
|
109
20
|
description: PLAN_EXECUTE_DESCRIPTION,
|
|
110
21
|
parameters: PlanExecuteParams,
|
|
111
22
|
async execute(_toolCallId, rawParams) {
|
|
112
23
|
const params = rawParams;
|
|
113
24
|
try {
|
|
25
|
+
if (params.planId === undefined) {
|
|
26
|
+
const result = { ok: true, plans: store.list().slice(0, 10) };
|
|
27
|
+
return toolJsonResult(result);
|
|
28
|
+
}
|
|
114
29
|
const plan = store.get(params.planId);
|
|
115
30
|
if (plan === undefined) {
|
|
116
31
|
const result = { ok: false, error: { code: "PLAN_NOT_FOUND" } };
|
|
117
32
|
return toolJsonResult(result);
|
|
118
33
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
assertPlanInvariants(plan);
|
|
122
|
-
store.set(plan);
|
|
123
|
-
const task = plan.tasks[params.taskIndex];
|
|
124
|
-
const payload = {
|
|
125
|
-
kind: "plan_update",
|
|
34
|
+
const result = {
|
|
35
|
+
ok: true,
|
|
126
36
|
planId: plan.planId,
|
|
127
|
-
|
|
128
|
-
action: params.action,
|
|
129
|
-
taskIndex: params.taskIndex,
|
|
130
|
-
...(params.stepIndex !== undefined ? { stepIndex: params.stepIndex } : {}),
|
|
131
|
-
taskTitle: task.title,
|
|
132
|
-
taskStatus: task.status,
|
|
133
|
-
...(task.status === "blocked" && task.note !== undefined ? { blockedNote: task.note } : {}),
|
|
134
|
-
summary: summarize(plan),
|
|
135
|
-
runId: routing.runId,
|
|
136
|
-
...(routing.sessionKey !== undefined ? { sessionKey: routing.sessionKey } : {}),
|
|
137
|
-
...(routing.deliveryContext !== undefined ? { deliveryContext: routing.deliveryContext } : {}),
|
|
138
|
-
...(routing.agentAccountId !== undefined ? { agentAccountId: routing.agentAccountId } : {}),
|
|
139
|
-
...(routing.messageChannel !== undefined ? { messageChannel: routing.messageChannel } : {}),
|
|
37
|
+
view: renderPlanView(plan),
|
|
140
38
|
};
|
|
141
|
-
const event = emitPlanEvent({ pluginId, emitAgentEvent, payload });
|
|
142
|
-
const result = { ok: true, planId: plan.planId, plan, event };
|
|
143
39
|
return toolJsonResult(result);
|
|
144
40
|
}
|
|
145
41
|
catch (e) {
|
|
146
|
-
const code = e instanceof PlanInvariantError
|
|
147
|
-
? "INVARIANT_VIOLATION"
|
|
148
|
-
: e instanceof PlanExecuteValidationError
|
|
149
|
-
? e.code
|
|
150
|
-
: "STORE_ERROR";
|
|
151
42
|
const result = {
|
|
152
43
|
ok: false,
|
|
153
|
-
error: { code, detail: String(e.message) },
|
|
44
|
+
error: { code: "STORE_ERROR", detail: String(e.message) },
|
|
154
45
|
};
|
|
155
46
|
return toolJsonResult(result);
|
|
156
47
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlanOrigin } from "@xgjktech/xg-openclaw-shared";
|
|
2
|
+
/** 从工具工厂 ctx 采集来源上下文。全字段皆空时返回 undefined(不落空对象)。 */
|
|
3
|
+
export declare function toPlanOrigin(toolContext: {
|
|
4
|
+
sessionKey?: string;
|
|
5
|
+
messageChannel?: string;
|
|
6
|
+
agentAccountId?: string;
|
|
7
|
+
deliveryContext?: unknown;
|
|
8
|
+
}): PlanOrigin | undefined;
|
|
9
|
+
//# sourceMappingURL=plan-origin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-origin.d.ts","sourceRoot":"","sources":["../src/plan-origin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAa/D,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,WAAW,EAAE;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,UAAU,GAAG,SAAS,CAuBzB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function nonEmptyString(value) {
|
|
2
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
function readDeliveryField(deliveryContext, field) {
|
|
5
|
+
if (typeof deliveryContext !== "object" || deliveryContext === null || Array.isArray(deliveryContext)) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
return deliveryContext[field];
|
|
9
|
+
}
|
|
10
|
+
/** 从工具工厂 ctx 采集来源上下文。全字段皆空时返回 undefined(不落空对象)。 */
|
|
11
|
+
export function toPlanOrigin(toolContext) {
|
|
12
|
+
const deliveryContext = toolContext.deliveryContext;
|
|
13
|
+
const channel = nonEmptyString(readDeliveryField(deliveryContext, "channel")) ??
|
|
14
|
+
nonEmptyString(toolContext.messageChannel);
|
|
15
|
+
const to = nonEmptyString(readDeliveryField(deliveryContext, "to"));
|
|
16
|
+
const accountId = nonEmptyString(readDeliveryField(deliveryContext, "accountId")) ??
|
|
17
|
+
nonEmptyString(toolContext.agentAccountId);
|
|
18
|
+
const rawThreadId = readDeliveryField(deliveryContext, "threadId");
|
|
19
|
+
const threadId = typeof rawThreadId === "number" ? String(rawThreadId) : nonEmptyString(rawThreadId);
|
|
20
|
+
const sessionKey = nonEmptyString(toolContext.sessionKey);
|
|
21
|
+
const origin = {};
|
|
22
|
+
if (channel !== undefined)
|
|
23
|
+
origin.channel = channel;
|
|
24
|
+
if (to !== undefined)
|
|
25
|
+
origin.to = to;
|
|
26
|
+
if (accountId !== undefined)
|
|
27
|
+
origin.accountId = accountId;
|
|
28
|
+
if (threadId !== undefined)
|
|
29
|
+
origin.threadId = threadId;
|
|
30
|
+
if (sessionKey !== undefined)
|
|
31
|
+
origin.sessionKey = sessionKey;
|
|
32
|
+
return Object.keys(origin).length === 0 ? undefined : origin;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-view.d.ts","sourceRoot":"","sources":["../src/plan-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAA0B,MAAM,8BAA8B,CAAC;AAoBjF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAejD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const TASK_STATUS_MARKERS = {
|
|
2
|
+
pending: "[ ]",
|
|
3
|
+
in_progress: "[>]",
|
|
4
|
+
completed: "[x]",
|
|
5
|
+
blocked: "[!]",
|
|
6
|
+
};
|
|
7
|
+
const STEP_STATUS_MARKERS = {
|
|
8
|
+
pending: "[ ]",
|
|
9
|
+
in_progress: "[>]",
|
|
10
|
+
completed: "[x]",
|
|
11
|
+
};
|
|
12
|
+
/** view 依赖"一行一项"的格式约定;字段内容里的换行统一压成空格,防止伪造出多余的行。 */
|
|
13
|
+
function singleLine(text) {
|
|
14
|
+
return text.replace(/\s*\r?\n\s*/g, " ");
|
|
15
|
+
}
|
|
16
|
+
export function renderPlanView(plan) {
|
|
17
|
+
const lines = [`goal: ${singleLine(plan.goal)}`];
|
|
18
|
+
plan.tasks.forEach((task, taskIndex) => {
|
|
19
|
+
const note = task.note !== undefined ? ` | note: ${singleLine(task.note)}` : "";
|
|
20
|
+
lines.push(`T${taskIndex} ${TASK_STATUS_MARKERS[task.status]} ${singleLine(task.title)}${note}`);
|
|
21
|
+
task.steps?.forEach((step, stepIndex) => {
|
|
22
|
+
lines.push(` T${taskIndex}.S${stepIndex} ${STEP_STATUS_MARKERS[step.status]} ${singleLine(step.text)}`);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return lines.join("\n");
|
|
26
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
-
import { type PlanStore } from "@xgjktech/xg-openclaw-shared";
|
|
3
|
+
import { type PlanOrigin, type PlanStore } from "@xgjktech/xg-openclaw-shared";
|
|
4
4
|
export declare const PlanWriteParams: Type.TObject<{
|
|
5
5
|
planId: Type.TOptional<Type.TString>;
|
|
6
6
|
goal: Type.TString;
|
|
@@ -8,13 +8,9 @@ export declare const PlanWriteParams: Type.TObject<{
|
|
|
8
8
|
title: Type.TString;
|
|
9
9
|
status: Type.TOptional<Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">, Type.TLiteral<"blocked">]>>;
|
|
10
10
|
note: Type.TOptional<Type.TString>;
|
|
11
|
-
steps: Type.TOptional<Type.TArray<Type.TObject<{
|
|
12
|
-
text: Type.TString;
|
|
13
|
-
status: Type.TOptional<Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">]>>;
|
|
14
|
-
}>>>;
|
|
15
11
|
}>>;
|
|
16
12
|
}>;
|
|
17
13
|
export type PlanWriteParamsT = Static<typeof PlanWriteParams>;
|
|
18
14
|
export declare const PLAN_WRITE_DESCRIPTION: string;
|
|
19
|
-
export declare function buildPlanWriteTool(store: PlanStore): AnyAgentTool;
|
|
15
|
+
export declare function buildPlanWriteTool(store: PlanStore, origin?: PlanOrigin): AnyAgentTool;
|
|
20
16
|
//# sourceMappingURL=plan-write-tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-write-tool.d.ts","sourceRoot":"","sources":["../src/plan-write-tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,
|
|
1
|
+
{"version":3,"file":"plan-write-tool.d.ts","sourceRoot":"","sources":["../src/plan-write-tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,SAAS,EAGf,MAAM,8BAA8B,CAAC;AA2BtC,eAAO,MAAM,eAAe;;;;;;;;EAiB3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,eAAO,MAAM,sBAAsB,QAkBvB,CAAC;AAwCb,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CA2CtF"}
|
package/dist/plan-write-tool.js
CHANGED
|
@@ -1,48 +1,54 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { assertPlanInvariants,
|
|
3
|
+
import { assertPlanInvariants, } from "@xgjktech/xg-openclaw-shared";
|
|
4
4
|
import { toolJsonResult } from "./tool-json-result.js";
|
|
5
|
-
|
|
6
|
-
text: Type.String({ minLength: 1, description: "单条可执行步骤,动词开头,无占位符" }),
|
|
7
|
-
status: Type.Optional(Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], { default: "pending" })),
|
|
8
|
-
}, { additionalProperties: false });
|
|
5
|
+
import { renderPlanView } from "./plan-view.js";
|
|
9
6
|
const TaskInput = Type.Object({
|
|
10
|
-
title: Type.String({
|
|
7
|
+
title: Type.String({
|
|
8
|
+
minLength: 1,
|
|
9
|
+
maxLength: 200,
|
|
10
|
+
description: "能判断是否完成的具体任务",
|
|
11
|
+
}),
|
|
11
12
|
status: Type.Optional(Type.Union([
|
|
12
13
|
Type.Literal("pending"),
|
|
13
14
|
Type.Literal("in_progress"),
|
|
14
15
|
Type.Literal("completed"),
|
|
15
16
|
Type.Literal("blocked"),
|
|
16
17
|
], { default: "pending" })),
|
|
17
|
-
note: Type.Optional(Type.String({ description: "备注;
|
|
18
|
-
steps: Type.Optional(Type.Array(StepInput, { description: "可选子步骤" })),
|
|
18
|
+
note: Type.Optional(Type.String({ maxLength: 1000, description: "备注;blocked 时写明原因" })),
|
|
19
19
|
}, { additionalProperties: false });
|
|
20
20
|
export const PlanWriteParams = Type.Object({
|
|
21
|
-
planId: Type.Optional(Type.String({ description: "
|
|
22
|
-
goal: Type.String({
|
|
23
|
-
|
|
21
|
+
planId: Type.Optional(Type.String({ maxLength: 128, description: "留空=新建;填写=整张覆盖该计划" })),
|
|
22
|
+
goal: Type.String({
|
|
23
|
+
minLength: 1,
|
|
24
|
+
maxLength: 500,
|
|
25
|
+
description: "计划总目标,一句话说明完成态",
|
|
26
|
+
}),
|
|
27
|
+
tasks: Type.Array(TaskInput, {
|
|
28
|
+
minItems: 1,
|
|
29
|
+
maxItems: 20,
|
|
30
|
+
description: "完整任务清单;更新时未提交的旧任务会被删除",
|
|
31
|
+
}),
|
|
24
32
|
}, { additionalProperties: false });
|
|
25
33
|
export const PLAN_WRITE_DESCRIPTION = [
|
|
26
|
-
"
|
|
34
|
+
"用途:维护一份对用户可见、可跨回合保存的任务清单。每次调用都是整张替换,不是增量更新。",
|
|
27
35
|
"",
|
|
28
|
-
"
|
|
36
|
+
"何时不用(除非用户明确要求追踪):",
|
|
37
|
+
"- 纯问答、查询、解释或闲聊。",
|
|
38
|
+
"- 一两个动作即可完成,或预计当前回合可以直接做完。",
|
|
39
|
+
"- 不要把分析、思考和试错过程写成 task;只记录需要向用户展示的工作项。",
|
|
29
40
|
"",
|
|
30
|
-
"
|
|
31
|
-
"-
|
|
32
|
-
"-
|
|
33
|
-
"-
|
|
34
|
-
"- 顺序清晰:tasks 按依赖顺序排列,读者不需要猜先后。",
|
|
41
|
+
"何时用:",
|
|
42
|
+
"- 工作有 3 个以上可独立完成的任务,且预计需要多轮推进或中断后继续。",
|
|
43
|
+
"- 需要等待人的回复、审批或外部系统,即使任务较少。",
|
|
44
|
+
"- 用户明确要求任务清单或进度追踪。",
|
|
35
45
|
"",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
46
|
+
"提交规则:",
|
|
47
|
+
"- 新建时不传 planId;更新时传入原 planId。",
|
|
48
|
+
"- 更新时必须重新提交 goal 和所有要保留的 tasks,包括未变化任务及其当前 status/note。只提交变化项会删除其他任务;记不清时先用 xg_plan_execute 查看。",
|
|
49
|
+
"- goal 用一句话;tasks 通常 3-7 项,每项是能判断是否完成的具体动作,不写 TODO、待定或内部思考。",
|
|
50
|
+
"- status 为 pending、in_progress、completed 或 blocked。通常只有一项 in_progress,确实并行时可以有多项;blocked 时必须用 note 写明原因和继续所需条件。",
|
|
39
51
|
].join("\n");
|
|
40
|
-
function toStep(input) {
|
|
41
|
-
return {
|
|
42
|
-
text: input.text,
|
|
43
|
-
status: (input.status ?? "pending"),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
52
|
function toTask(input) {
|
|
47
53
|
const task = {
|
|
48
54
|
title: input.title,
|
|
@@ -51,22 +57,26 @@ function toTask(input) {
|
|
|
51
57
|
if (input.note !== undefined) {
|
|
52
58
|
task.note = input.note;
|
|
53
59
|
}
|
|
54
|
-
if (input.steps !== undefined) {
|
|
55
|
-
task.steps = input.steps.map(toStep);
|
|
56
|
-
}
|
|
57
60
|
return task;
|
|
58
61
|
}
|
|
59
|
-
function buildPlan(params, existing, planId) {
|
|
62
|
+
function buildPlan(params, existing, planId, origin) {
|
|
60
63
|
const now = new Date().toISOString();
|
|
61
|
-
|
|
64
|
+
const plan = {
|
|
62
65
|
planId,
|
|
63
66
|
goal: params.goal,
|
|
64
67
|
createdAt: existing?.createdAt ?? now,
|
|
65
68
|
updatedAt: now,
|
|
66
69
|
tasks: params.tasks.map(toTask),
|
|
67
70
|
};
|
|
71
|
+
// 改写已有计划时保留原 origin(改写是内容编辑,不是重新发起);
|
|
72
|
+
// 仅当旧计划本就没有 origin(旧数据补齐)才写入本次采集到的 origin。
|
|
73
|
+
const resolvedOrigin = existing !== undefined ? (existing.origin ?? origin) : origin;
|
|
74
|
+
if (resolvedOrigin !== undefined) {
|
|
75
|
+
plan.origin = resolvedOrigin;
|
|
76
|
+
}
|
|
77
|
+
return plan;
|
|
68
78
|
}
|
|
69
|
-
export function buildPlanWriteTool(store) {
|
|
79
|
+
export function buildPlanWriteTool(store, origin) {
|
|
70
80
|
return {
|
|
71
81
|
name: "xg_plan_write",
|
|
72
82
|
label: "【xg-harness】编写计划",
|
|
@@ -87,17 +97,20 @@ export function buildPlanWriteTool(store) {
|
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
const planId = params.planId ?? randomUUID();
|
|
90
|
-
const plan = buildPlan(params, existing, planId);
|
|
100
|
+
const plan = buildPlan(params, existing, planId, origin);
|
|
91
101
|
assertPlanInvariants(plan);
|
|
92
102
|
store.set(plan);
|
|
93
|
-
const result = {
|
|
103
|
+
const result = {
|
|
104
|
+
ok: true,
|
|
105
|
+
planId: plan.planId,
|
|
106
|
+
view: renderPlanView(plan),
|
|
107
|
+
};
|
|
94
108
|
return toolJsonResult(result);
|
|
95
109
|
}
|
|
96
110
|
catch (e) {
|
|
97
|
-
const code = e instanceof PlanInvariantError ? "INVARIANT_VIOLATION" : "STORE_ERROR";
|
|
98
111
|
const result = {
|
|
99
112
|
ok: false,
|
|
100
|
-
error: { code, detail: String(e.message) },
|
|
113
|
+
error: { code: "STORE_ERROR", detail: String(e.message) },
|
|
101
114
|
};
|
|
102
115
|
return toolJsonResult(result);
|
|
103
116
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgjktech/xg-openclaw-harness-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "企业工具插件:plan_write / plan_execute 计划工具(OpenClaw 第三方工具插件)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"typebox": "1.1.39",
|
|
30
|
-
"@xgjktech/xg-openclaw-shared": "^0.1
|
|
30
|
+
"@xgjktech/xg-openclaw-shared": "^0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.19.0",
|
package/dist/notify.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { PluginAgentEventEmitParams, PluginAgentEventEmitResult } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
-
import { type PlanEventPayload } from "@xgjktech/xg-openclaw-shared";
|
|
3
|
-
export type EmitAgentEventFn = (params: PluginAgentEventEmitParams) => PluginAgentEventEmitResult;
|
|
4
|
-
/**
|
|
5
|
-
* 缝隙B 事件发送:尽力而为,不因失败中断主流程(04-互通设计 §2.4 契约)。
|
|
6
|
-
*
|
|
7
|
-
* 红线:禁止伪造 runId(架构裁决 2026-07-24)。T3 已实证 factory ctx / execute ctx /
|
|
8
|
-
* 插件可达的 runtime API 均无正当获取当次 run 真实 runId 的路径(见
|
|
9
|
-
* docs/notes-plugin-entry.md N2)——payload.runId 缺失时直接跳过发送,不抛异常。
|
|
10
|
-
*/
|
|
11
|
-
export declare function emitPlanEvent(params: {
|
|
12
|
-
pluginId: string;
|
|
13
|
-
emitAgentEvent: EmitAgentEventFn;
|
|
14
|
-
payload: PlanEventPayload;
|
|
15
|
-
}): PluginAgentEventEmitResult;
|
|
16
|
-
//# sourceMappingURL=notify.d.ts.map
|
package/dist/notify.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,0BAA0B,EAE3B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEtF,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,0BAA0B,KAAK,0BAA0B,CAAC;AAElG;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,gBAAgB,CAAC;IACjC,OAAO,EAAE,gBAAgB,CAAC;CAC3B,GAAG,0BAA0B,CAW7B"}
|
package/dist/notify.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { planEventStream } from "@xgjktech/xg-openclaw-shared";
|
|
2
|
-
/**
|
|
3
|
-
* 缝隙B 事件发送:尽力而为,不因失败中断主流程(04-互通设计 §2.4 契约)。
|
|
4
|
-
*
|
|
5
|
-
* 红线:禁止伪造 runId(架构裁决 2026-07-24)。T3 已实证 factory ctx / execute ctx /
|
|
6
|
-
* 插件可达的 runtime API 均无正当获取当次 run 真实 runId 的路径(见
|
|
7
|
-
* docs/notes-plugin-entry.md N2)——payload.runId 缺失时直接跳过发送,不抛异常。
|
|
8
|
-
*/
|
|
9
|
-
export function emitPlanEvent(params) {
|
|
10
|
-
const { pluginId, emitAgentEvent, payload } = params;
|
|
11
|
-
if (payload.runId.trim() === "") {
|
|
12
|
-
return { emitted: false, reason: "no runId" };
|
|
13
|
-
}
|
|
14
|
-
return emitAgentEvent({
|
|
15
|
-
runId: payload.runId,
|
|
16
|
-
stream: planEventStream(pluginId),
|
|
17
|
-
data: payload,
|
|
18
|
-
...(payload.sessionKey ? { sessionKey: payload.sessionKey } : {}),
|
|
19
|
-
});
|
|
20
|
-
}
|