clawjobs 0.2.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/LICENSE +21 -0
- package/README.md +42 -0
- package/README_CN.md +42 -0
- package/index.ts +4 -0
- package/openclaw.plugin.json +95 -0
- package/package.json +40 -0
- package/src/json-agent.ts +290 -0
- package/src/local-exec.ts +184 -0
- package/src/plugin.ts +24 -0
- package/src/service.ts +713 -0
- package/src/web-ui.ts +375 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ClawJobs
|
|
2
|
+
|
|
3
|
+
ClawJobs is an OpenClaw plugin for peer-powered task collaboration.
|
|
4
|
+
|
|
5
|
+
The assignee contributes reasoning on their own machine, while every real command still executes on the task owner's machine.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install clawjobs
|
|
11
|
+
openclaw config set plugins.allow '["clawjobs"]' --strict-json
|
|
12
|
+
openclaw config set plugins.entries.clawjobs.enabled true
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then write `plugins.entries.clawjobs.config`.
|
|
16
|
+
|
|
17
|
+
## Minimal config
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"hubUrl": "https://your-hub.example.com",
|
|
22
|
+
"hubToken": "your-shared-token",
|
|
23
|
+
"nickname": "Your Nickname",
|
|
24
|
+
"workspaceDir": "/your/workspace"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Task page
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
http://127.0.0.1:18789/plugins/clawjobs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- every participating machine installs the plugin
|
|
37
|
+
- one central hub is reachable by all peers
|
|
38
|
+
- the assignee machine has a usable OpenClaw model configuration
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/README_CN.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ClawJobs
|
|
2
|
+
|
|
3
|
+
`ClawJobs` 是一个 `OpenClaw` 插件,用来做多台小龙虾之间的任务协作。
|
|
4
|
+
|
|
5
|
+
接单人负责推理,真实命令始终只在任务发起人的本机执行。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install clawjobs
|
|
11
|
+
openclaw config set plugins.allow '["clawjobs"]' --strict-json
|
|
12
|
+
openclaw config set plugins.entries.clawjobs.enabled true
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
然后写入 `plugins.entries.clawjobs.config`。
|
|
16
|
+
|
|
17
|
+
## 最小配置
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"hubUrl": "https://你的-hub-地址",
|
|
22
|
+
"hubToken": "共享口令",
|
|
23
|
+
"nickname": "你的昵称",
|
|
24
|
+
"workspaceDir": "/你的工作目录"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 任务页
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
http://127.0.0.1:18789/plugins/clawjobs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 依赖
|
|
35
|
+
|
|
36
|
+
- 每台参与机器都安装插件
|
|
37
|
+
- 所有机器都能访问同一个中心 hub
|
|
38
|
+
- 接单机器本地有可用的 OpenClaw 模型配置
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clawjobs",
|
|
3
|
+
"name": "ClawJobs",
|
|
4
|
+
"description": "Peer-powered jobs for OpenClaw, with remote reasoning and owner-side execution.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"hubUrl": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"hubToken": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"nickname": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"peerId": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"workspaceDir": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"brain": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"properties": {
|
|
28
|
+
"provider": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"model": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"maxSteps": {
|
|
35
|
+
"type": "number"
|
|
36
|
+
},
|
|
37
|
+
"timeoutMs": {
|
|
38
|
+
"type": "number"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"execution": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"properties": {
|
|
46
|
+
"defaultCwd": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"maxCommandMs": {
|
|
50
|
+
"type": "number"
|
|
51
|
+
},
|
|
52
|
+
"maxOutputChars": {
|
|
53
|
+
"type": "number"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"uiHints": {
|
|
60
|
+
"hubUrl": {
|
|
61
|
+
"label": "Hub URL"
|
|
62
|
+
},
|
|
63
|
+
"hubToken": {
|
|
64
|
+
"label": "Hub Token",
|
|
65
|
+
"sensitive": true
|
|
66
|
+
},
|
|
67
|
+
"nickname": {
|
|
68
|
+
"label": "Nickname"
|
|
69
|
+
},
|
|
70
|
+
"workspaceDir": {
|
|
71
|
+
"label": "Default Workspace"
|
|
72
|
+
},
|
|
73
|
+
"brain.provider": {
|
|
74
|
+
"label": "Brain Provider"
|
|
75
|
+
},
|
|
76
|
+
"brain.model": {
|
|
77
|
+
"label": "Brain Model"
|
|
78
|
+
},
|
|
79
|
+
"brain.maxSteps": {
|
|
80
|
+
"label": "Max Reasoning Steps"
|
|
81
|
+
},
|
|
82
|
+
"brain.timeoutMs": {
|
|
83
|
+
"label": "Reasoning Timeout (ms)"
|
|
84
|
+
},
|
|
85
|
+
"execution.defaultCwd": {
|
|
86
|
+
"label": "Default Execution CWD"
|
|
87
|
+
},
|
|
88
|
+
"execution.maxCommandMs": {
|
|
89
|
+
"label": "Command Timeout (ms)"
|
|
90
|
+
},
|
|
91
|
+
"execution.maxOutputChars": {
|
|
92
|
+
"label": "Max Output Characters"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawjobs",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Peer-powered jobs for OpenClaw: remote reasoning with execution kept on the task owner's machine.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"openclaw",
|
|
10
|
+
"plugin",
|
|
11
|
+
"clawjobs",
|
|
12
|
+
"peer-to-peer",
|
|
13
|
+
"tasks",
|
|
14
|
+
"local-execution"
|
|
15
|
+
],
|
|
16
|
+
"openclaw": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./index.ts"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.ts",
|
|
23
|
+
"openclaw.plugin.json",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"README_CN.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/gtoadio-cyber/openclaw-clawjobs.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/gtoadio-cyber/openclaw-clawjobs/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/gtoadio-cyber/openclaw-clawjobs#readme"
|
|
40
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type RunJsonTaskParams = {
|
|
7
|
+
api: {
|
|
8
|
+
config: Record<string, unknown>;
|
|
9
|
+
logger: {
|
|
10
|
+
warn: (message: string) => void;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
workspaceDir: string;
|
|
14
|
+
provider?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
authProfileId?: string;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
prompt: string;
|
|
19
|
+
input: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BrainDecision =
|
|
23
|
+
| {
|
|
24
|
+
action: "owner_exec";
|
|
25
|
+
note: string;
|
|
26
|
+
command: string;
|
|
27
|
+
cwd?: string | null;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
action: "finish";
|
|
32
|
+
note: string;
|
|
33
|
+
resultText: string;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
action: "fail";
|
|
37
|
+
note: string;
|
|
38
|
+
errorText: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<{
|
|
42
|
+
payloads?: Array<{ text?: string; isError?: boolean }>;
|
|
43
|
+
}>;
|
|
44
|
+
|
|
45
|
+
function collectAssistantText(payloads: Array<{ text?: string; isError?: boolean }> | undefined) {
|
|
46
|
+
return (payloads || [])
|
|
47
|
+
.filter((item) => !item.isError && typeof item.text === "string")
|
|
48
|
+
.map((item) => item.text || "")
|
|
49
|
+
.join("\n")
|
|
50
|
+
.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripCodeFence(value: string) {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
const matched = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
56
|
+
return matched ? (matched[1] || "").trim() : trimmed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateDecision(value: unknown): BrainDecision {
|
|
60
|
+
if (!value || typeof value !== "object") {
|
|
61
|
+
throw new Error("The model did not return an object.");
|
|
62
|
+
}
|
|
63
|
+
const action = typeof value.action === "string" ? value.action : "";
|
|
64
|
+
const note = typeof value.note === "string" ? value.note.trim() : "";
|
|
65
|
+
if (!note) {
|
|
66
|
+
throw new Error("The model response is missing note.");
|
|
67
|
+
}
|
|
68
|
+
if (action === "owner_exec") {
|
|
69
|
+
const command = typeof value.command === "string" ? value.command.trim() : "";
|
|
70
|
+
if (!command) {
|
|
71
|
+
throw new Error("owner_exec is missing command.");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
action,
|
|
75
|
+
note,
|
|
76
|
+
command,
|
|
77
|
+
cwd: typeof value.cwd === "string" ? value.cwd : null,
|
|
78
|
+
timeoutMs:
|
|
79
|
+
typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs)
|
|
80
|
+
? value.timeoutMs
|
|
81
|
+
: undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (action === "finish") {
|
|
85
|
+
const resultText = typeof value.resultText === "string" ? value.resultText.trim() : "";
|
|
86
|
+
if (!resultText) {
|
|
87
|
+
throw new Error("finish is missing resultText.");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
action,
|
|
91
|
+
note,
|
|
92
|
+
resultText,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (action === "fail") {
|
|
96
|
+
const errorText = typeof value.errorText === "string" ? value.errorText.trim() : "";
|
|
97
|
+
if (!errorText) {
|
|
98
|
+
throw new Error("fail is missing errorText.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
action,
|
|
102
|
+
note,
|
|
103
|
+
errorText,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Unknown action: ${action || "<empty>"}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function pathExists(targetPath: string) {
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(targetPath);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addCandidate(candidatePaths: Set<string>, basePath: string) {
|
|
119
|
+
const normalizedBase = path.resolve(basePath);
|
|
120
|
+
let current = normalizedBase;
|
|
121
|
+
|
|
122
|
+
while (true) {
|
|
123
|
+
candidatePaths.add(path.join(current, "dist", "extensionAPI.js"));
|
|
124
|
+
const parent = path.dirname(current);
|
|
125
|
+
if (parent === current) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
current = parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function tryAddRealPathCandidates(candidatePaths: Set<string>, inputPath: string) {
|
|
133
|
+
if (!inputPath || typeof inputPath !== "string") {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const realPath = await fs.realpath(inputPath);
|
|
138
|
+
addCandidate(candidatePaths, path.dirname(realPath));
|
|
139
|
+
} catch {
|
|
140
|
+
addCandidate(candidatePaths, path.dirname(inputPath));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveCliPathFromShell() {
|
|
145
|
+
const lookup = process.platform === "win32" ? "where" : "which";
|
|
146
|
+
const result = spawnSync(lookup, ["openclaw"], {
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
});
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const firstLine = String(result.stdout || "")
|
|
154
|
+
.split(/\r?\n/)
|
|
155
|
+
.map((line) => line.trim())
|
|
156
|
+
.find(Boolean);
|
|
157
|
+
return firstLine || null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function resolveExtensionApiPath(): Promise<string> {
|
|
161
|
+
const candidatePaths = new Set<string>();
|
|
162
|
+
|
|
163
|
+
for (const runtimePath of [process.argv[1], process.execPath]) {
|
|
164
|
+
if (!runtimePath || typeof runtimePath !== "string") {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
await tryAddRealPathCandidates(candidatePaths, runtimePath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cliPath = resolveCliPathFromShell();
|
|
171
|
+
if (cliPath) {
|
|
172
|
+
await tryAddRealPathCandidates(candidatePaths, cliPath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const candidatePath of candidatePaths) {
|
|
176
|
+
if (await pathExists(candidatePath)) {
|
|
177
|
+
return candidatePath;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error("Unable to locate OpenClaw extensionAPI.js.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
|
|
185
|
+
const extensionApiPath = await resolveExtensionApiPath();
|
|
186
|
+
const mod = (await import(pathToFileURL(extensionApiPath).href)) as {
|
|
187
|
+
runEmbeddedPiAgent?: unknown;
|
|
188
|
+
};
|
|
189
|
+
if (typeof mod.runEmbeddedPiAgent !== "function") {
|
|
190
|
+
throw new Error("OpenClaw extensionAPI does not expose runEmbeddedPiAgent.");
|
|
191
|
+
}
|
|
192
|
+
return mod.runEmbeddedPiAgent as RunEmbeddedPiAgentFn;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function runJsonTask(params: RunJsonTaskParams): Promise<BrainDecision> {
|
|
196
|
+
const inputJson = JSON.stringify(params.input ?? null, null, 2);
|
|
197
|
+
const schemaText = JSON.stringify(
|
|
198
|
+
{
|
|
199
|
+
type: "object",
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
required: ["action", "note"],
|
|
202
|
+
properties: {
|
|
203
|
+
action: {
|
|
204
|
+
type: "string",
|
|
205
|
+
enum: ["owner_exec", "finish", "fail"],
|
|
206
|
+
},
|
|
207
|
+
note: {
|
|
208
|
+
type: "string",
|
|
209
|
+
},
|
|
210
|
+
command: {
|
|
211
|
+
type: "string",
|
|
212
|
+
},
|
|
213
|
+
cwd: {
|
|
214
|
+
type: ["string", "null"],
|
|
215
|
+
},
|
|
216
|
+
timeoutMs: {
|
|
217
|
+
type: "number",
|
|
218
|
+
},
|
|
219
|
+
resultText: {
|
|
220
|
+
type: "string",
|
|
221
|
+
},
|
|
222
|
+
errorText: {
|
|
223
|
+
type: "string",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const prompt = [
|
|
232
|
+
"You are the assignee brain for a remote ClawJobs task.",
|
|
233
|
+
"You cannot execute commands yourself and you must not pretend that local execution already happened.",
|
|
234
|
+
"All real execution must be sent back to the task owner through owner_exec.",
|
|
235
|
+
"Return exactly one action at a time.",
|
|
236
|
+
"If more information is needed, return owner_exec.",
|
|
237
|
+
"If the answer is ready, return finish.",
|
|
238
|
+
"If the task cannot continue safely, return fail.",
|
|
239
|
+
"Return strict JSON only, with no markdown or extra commentary.",
|
|
240
|
+
"",
|
|
241
|
+
"TASK_PROMPT:",
|
|
242
|
+
params.prompt,
|
|
243
|
+
"",
|
|
244
|
+
"OUTPUT_SCHEMA_JSON:",
|
|
245
|
+
schemaText,
|
|
246
|
+
"",
|
|
247
|
+
"INPUT_JSON:",
|
|
248
|
+
inputJson,
|
|
249
|
+
].join("\n");
|
|
250
|
+
|
|
251
|
+
const tmpDir = await fs.mkdtemp(path.join(process.env.TMPDIR || "/tmp", "clawjobs-"));
|
|
252
|
+
const sessionId = `clawjobs-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
253
|
+
const sessionFile = path.join(tmpDir, "session.json");
|
|
254
|
+
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
258
|
+
const runResult = await runEmbeddedPiAgent({
|
|
259
|
+
sessionId: `${sessionId}-${attempt}`,
|
|
260
|
+
sessionFile,
|
|
261
|
+
workspaceDir: params.workspaceDir,
|
|
262
|
+
config: params.api.config,
|
|
263
|
+
prompt:
|
|
264
|
+
attempt === 0
|
|
265
|
+
? prompt
|
|
266
|
+
: `${prompt}\n\nYour last response was not valid JSON. Return valid JSON only this time.`,
|
|
267
|
+
timeoutMs: params.timeoutMs,
|
|
268
|
+
runId: `${sessionId}-run-${attempt}`,
|
|
269
|
+
provider: params.provider,
|
|
270
|
+
model: params.model,
|
|
271
|
+
authProfileId: params.authProfileId,
|
|
272
|
+
authProfileIdSource: params.authProfileId ? "user" : "auto",
|
|
273
|
+
disableTools: true,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const text = collectAssistantText(runResult.payloads);
|
|
277
|
+
if (!text) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
return validateDecision(JSON.parse(stripCodeFence(text)));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
params.api.logger.warn(`clawjobs json parse failed: ${error.message || String(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
throw new Error("The model returned invalid JSON twice in a row.");
|
|
287
|
+
} finally {
|
|
288
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
289
|
+
}
|
|
290
|
+
}
|