doer-agent 0.4.2 → 0.4.4
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/dist/agent-codex-auth-rpc.js +322 -0
- package/dist/agent-codex-cli.js +210 -0
- package/dist/agent-fs-rpc.js +405 -0
- package/dist/agent-git-rpc.js +299 -0
- package/dist/agent-jetstream.js +120 -0
- package/dist/agent-run-execution.js +39 -0
- package/dist/agent-run-lifecycle.js +67 -0
- package/dist/agent-run-rpc.js +93 -0
- package/dist/agent-run-state.js +229 -0
- package/dist/agent-runtime-env.js +147 -0
- package/dist/agent-runtime-io.js +112 -0
- package/dist/agent-runtime-utils.js +253 -0
- package/dist/agent-session-loop.js +53 -0
- package/dist/agent-session-rpc.js +867 -0
- package/dist/agent-settings-rpc.js +75 -0
- package/dist/agent-settings.js +397 -0
- package/dist/agent-skill-rpc.js +164 -0
- package/dist/agent-task-execution.js +275 -0
- package/dist/agent.js +376 -4275
- package/package.json +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { mkdir, open, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { StringCodec } from "nats";
|
|
4
|
+
const runEventsCodec = StringCodec();
|
|
5
|
+
async function resolveRunsDir(workspaceRoot) {
|
|
6
|
+
const dir = path.join(workspaceRoot, ".doer-agent", "runs");
|
|
7
|
+
await mkdir(dir, { recursive: true });
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
function sanitizeRunLockSegment(value) {
|
|
11
|
+
return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "lock";
|
|
12
|
+
}
|
|
13
|
+
async function resolveRunLocksDir(workspaceRoot) {
|
|
14
|
+
const dir = path.join(await resolveRunsDir(workspaceRoot), "locks");
|
|
15
|
+
await mkdir(dir, { recursive: true });
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
async function resolveRunStartLockPath(args) {
|
|
19
|
+
const dir = await resolveRunLocksDir(args.workspaceRoot);
|
|
20
|
+
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
21
|
+
return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
|
|
22
|
+
}
|
|
23
|
+
return path.join(dir, "pending_new_session.lock");
|
|
24
|
+
}
|
|
25
|
+
function normalizePersistedRunTask(value) {
|
|
26
|
+
if (!value || typeof value !== "object") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const record = value;
|
|
30
|
+
const id = typeof record.runId === "string" && record.runId.trim()
|
|
31
|
+
? record.runId.trim()
|
|
32
|
+
: typeof record.id === "string" && record.id.trim()
|
|
33
|
+
? record.id.trim()
|
|
34
|
+
: "";
|
|
35
|
+
const userId = typeof record.userId === "string" ? record.userId : "";
|
|
36
|
+
const agentId = typeof record.agentId === "string" ? record.agentId : "";
|
|
37
|
+
const status = record.status;
|
|
38
|
+
if (!id || !userId || !agentId || !["queued", "running", "completed", "failed", "canceled"].includes(String(status))) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
userId,
|
|
44
|
+
agentId,
|
|
45
|
+
processPid: typeof record.processPid === "number" ? record.processPid : null,
|
|
46
|
+
sessionId: typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId.trim() : null,
|
|
47
|
+
sessionFilePath: typeof record.sessionFilePath === "string" && record.sessionFilePath.trim() ? record.sessionFilePath.trim() : null,
|
|
48
|
+
status: status,
|
|
49
|
+
cancelRequested: Boolean(record.cancelRequested),
|
|
50
|
+
resultExitCode: typeof record.resultExitCode === "number" ? record.resultExitCode : null,
|
|
51
|
+
resultSignal: typeof record.resultSignal === "string" && record.resultSignal.trim() ? record.resultSignal.trim() : null,
|
|
52
|
+
error: typeof record.error === "string" && record.error.trim() ? record.error : null,
|
|
53
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
54
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "",
|
|
55
|
+
startedAt: typeof record.startedAt === "string" && record.startedAt.trim() ? record.startedAt : null,
|
|
56
|
+
finishedAt: typeof record.finishedAt === "string" && record.finishedAt.trim() ? record.finishedAt : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildImmediateRunEvent(task, type) {
|
|
60
|
+
return {
|
|
61
|
+
type,
|
|
62
|
+
agentId: task.agentId,
|
|
63
|
+
sessionId: task.sessionId,
|
|
64
|
+
filePath: task.sessionFilePath,
|
|
65
|
+
runId: task.id,
|
|
66
|
+
updatedAt: task.updatedAt,
|
|
67
|
+
status: task.status,
|
|
68
|
+
cancelRequested: task.cancelRequested,
|
|
69
|
+
resultExitCode: task.resultExitCode,
|
|
70
|
+
resultSignal: task.resultSignal,
|
|
71
|
+
error: task.error,
|
|
72
|
+
finishedAt: task.finishedAt,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function resetRunsDir(workspaceRoot) {
|
|
76
|
+
const dir = await resolveRunsDir(workspaceRoot);
|
|
77
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
78
|
+
await mkdir(dir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
export async function persistRunTask(workspaceRoot, task) {
|
|
81
|
+
const dir = await resolveRunsDir(workspaceRoot);
|
|
82
|
+
const payload = {
|
|
83
|
+
runId: task.id,
|
|
84
|
+
agentId: task.agentId,
|
|
85
|
+
userId: task.userId,
|
|
86
|
+
processPid: task.processPid,
|
|
87
|
+
sessionId: task.sessionId,
|
|
88
|
+
sessionFilePath: task.sessionFilePath,
|
|
89
|
+
status: task.status,
|
|
90
|
+
cancelRequested: task.cancelRequested,
|
|
91
|
+
resultExitCode: task.resultExitCode,
|
|
92
|
+
resultSignal: task.resultSignal,
|
|
93
|
+
createdAt: task.createdAt,
|
|
94
|
+
updatedAt: task.updatedAt,
|
|
95
|
+
startedAt: task.startedAt,
|
|
96
|
+
finishedAt: task.finishedAt,
|
|
97
|
+
error: task.error,
|
|
98
|
+
};
|
|
99
|
+
await writeFile(path.join(dir, `${task.id}.json`), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
100
|
+
}
|
|
101
|
+
export async function removeRunTask(workspaceRoot, runId) {
|
|
102
|
+
const dir = await resolveRunsDir(workspaceRoot);
|
|
103
|
+
await unlink(path.join(dir, `${runId}.json`)).catch(() => undefined);
|
|
104
|
+
}
|
|
105
|
+
export async function claimRunStartSlot(args) {
|
|
106
|
+
const lockPath = await resolveRunStartLockPath(args);
|
|
107
|
+
try {
|
|
108
|
+
const handle = await open(lockPath, "wx");
|
|
109
|
+
try {
|
|
110
|
+
const payload = {
|
|
111
|
+
runId: args.runId,
|
|
112
|
+
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
createdAt: args.formatTimestamp(),
|
|
115
|
+
};
|
|
116
|
+
await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await handle.close().catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error?.code === "EEXIST") {
|
|
124
|
+
const lockContents = await readFile(lockPath, "utf8").catch(() => "");
|
|
125
|
+
const existingRunId = (() => {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(lockContents);
|
|
128
|
+
return typeof parsed.runId === "string" && parsed.runId.trim() ? parsed.runId.trim() : null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
throw new Error(existingRunId ? `Another run is already active: ${existingRunId}` : "Another run is already active");
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function updateRunStartSlotSession(args) {
|
|
140
|
+
const nextSessionId = args.sessionId.trim();
|
|
141
|
+
if (!nextSessionId) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const previousSessionId = typeof args.previousSessionId === "string" && args.previousSessionId.trim() ? args.previousSessionId.trim() : null;
|
|
145
|
+
if (previousSessionId === nextSessionId) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const currentPath = await resolveRunStartLockPath({
|
|
149
|
+
workspaceRoot: args.workspaceRoot,
|
|
150
|
+
runId: args.runId,
|
|
151
|
+
sessionId: previousSessionId,
|
|
152
|
+
});
|
|
153
|
+
const nextPath = await resolveRunStartLockPath({
|
|
154
|
+
workspaceRoot: args.workspaceRoot,
|
|
155
|
+
runId: args.runId,
|
|
156
|
+
sessionId: nextSessionId,
|
|
157
|
+
});
|
|
158
|
+
if (currentPath === nextPath) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
await rename(currentPath, nextPath);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const code = error?.code;
|
|
166
|
+
if (code === "ENOENT") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (code === "EEXIST") {
|
|
170
|
+
throw new Error(`Another run is already active for session: ${nextSessionId}`);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
const payload = {
|
|
175
|
+
runId: args.runId,
|
|
176
|
+
sessionId: nextSessionId,
|
|
177
|
+
pid: process.pid,
|
|
178
|
+
createdAt: args.formatTimestamp(),
|
|
179
|
+
};
|
|
180
|
+
await writeFile(nextPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
181
|
+
}
|
|
182
|
+
export async function releaseRunStartSlot(args) {
|
|
183
|
+
const paths = new Set();
|
|
184
|
+
paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: args.sessionId ?? null }));
|
|
185
|
+
paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: null }));
|
|
186
|
+
for (const lockPath of paths) {
|
|
187
|
+
await unlink(lockPath).catch(() => undefined);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function cloneRunTask(task, _sinceSeq) {
|
|
191
|
+
return { ...task };
|
|
192
|
+
}
|
|
193
|
+
export function publishImmediateRunEvent(args) {
|
|
194
|
+
args.nc.publish(args.buildRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunEvent(args.task, args.type ?? "run.changed"))));
|
|
195
|
+
}
|
|
196
|
+
export async function listPersistedRunTasks(workspaceRoot) {
|
|
197
|
+
const dir = await resolveRunsDir(workspaceRoot);
|
|
198
|
+
const names = await readdir(dir).catch(() => []);
|
|
199
|
+
const tasks = await Promise.all(names
|
|
200
|
+
.filter((name) => name.endsWith(".json"))
|
|
201
|
+
.map(async (name) => {
|
|
202
|
+
const raw = await readFile(path.join(dir, name), "utf8").catch(() => null);
|
|
203
|
+
if (!raw) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return normalizePersistedRunTask(JSON.parse(raw));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}));
|
|
213
|
+
return tasks.filter((task) => task !== null);
|
|
214
|
+
}
|
|
215
|
+
export async function getStoredRun(workspaceRoot, runId) {
|
|
216
|
+
const persisted = await readFile(path.join(await resolveRunsDir(workspaceRoot), `${runId}.json`), "utf8").catch(() => null);
|
|
217
|
+
if (persisted) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = normalizePersistedRunTask(JSON.parse(persisted));
|
|
220
|
+
if (parsed) {
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Ignore malformed persisted state.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function pickFirstNonEmpty(values) {
|
|
6
|
+
for (const value of values) {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const normalized = value.trim();
|
|
11
|
+
if (normalized) {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
async function ensureGitAskpassScript(agentProjectDir) {
|
|
18
|
+
const binDir = path.join(agentProjectDir, "runtime/bin");
|
|
19
|
+
const scriptPath = path.join(binDir, "git-askpass.sh");
|
|
20
|
+
const scriptBody = `#!/bin/sh
|
|
21
|
+
case "$1" in
|
|
22
|
+
*Username*) printf "%s\\n" "x-access-token" ;;
|
|
23
|
+
*Password*) printf "%s\\n" "\${GITHUB_TOKEN:-\${GH_TOKEN:-}}" ;;
|
|
24
|
+
*) printf "\\n" ;;
|
|
25
|
+
esac
|
|
26
|
+
`;
|
|
27
|
+
await mkdir(binDir, { recursive: true });
|
|
28
|
+
await writeFile(scriptPath, scriptBody, "utf8");
|
|
29
|
+
await chmod(scriptPath, 0o700).catch(() => undefined);
|
|
30
|
+
return scriptPath;
|
|
31
|
+
}
|
|
32
|
+
function applyGitIdentityIfPossible(args) {
|
|
33
|
+
if (!args.cwd) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const inRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
37
|
+
cwd: args.cwd,
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
});
|
|
40
|
+
if (inRepo.status !== 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const setName = spawnSync("git", ["config", "--local", "user.name", args.userName], {
|
|
44
|
+
cwd: args.cwd,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
});
|
|
47
|
+
if (setName.status !== 0) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const setEmail = spawnSync("git", ["config", "--local", "user.email", args.userEmail], {
|
|
51
|
+
cwd: args.cwd,
|
|
52
|
+
stdio: "ignore",
|
|
53
|
+
});
|
|
54
|
+
return setEmail.status === 0;
|
|
55
|
+
}
|
|
56
|
+
export function createRuntimeEnvHelpers(args) {
|
|
57
|
+
function resolveCodexHomePath() {
|
|
58
|
+
return path.join(args.resolveWorkspaceRoot(), ".codex");
|
|
59
|
+
}
|
|
60
|
+
function resolveShellPath() {
|
|
61
|
+
if (process.platform === "win32") {
|
|
62
|
+
return process.env.ComSpec || "cmd.exe";
|
|
63
|
+
}
|
|
64
|
+
const candidates = [process.env.SHELL, "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh"].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (existsSync(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
|
|
71
|
+
}
|
|
72
|
+
function resolveTaskWorkspace(rawCwd) {
|
|
73
|
+
const workspaceRoot = args.resolveWorkspaceRoot();
|
|
74
|
+
const requestedCwd = rawCwd?.trim() || "";
|
|
75
|
+
const resolvedCwd = requestedCwd
|
|
76
|
+
? path.isAbsolute(requestedCwd)
|
|
77
|
+
? path.resolve(requestedCwd)
|
|
78
|
+
: path.resolve(workspaceRoot, requestedCwd)
|
|
79
|
+
: workspaceRoot;
|
|
80
|
+
if (!existsSync(resolvedCwd)) {
|
|
81
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (path does not exist)`);
|
|
82
|
+
}
|
|
83
|
+
let stats;
|
|
84
|
+
try {
|
|
85
|
+
stats = statSync(resolvedCwd);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
90
|
+
}
|
|
91
|
+
if (!stats.isDirectory()) {
|
|
92
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (not a directory)`);
|
|
93
|
+
}
|
|
94
|
+
return resolvedCwd;
|
|
95
|
+
}
|
|
96
|
+
async function prepareTaskGitEnv(prepareArgs) {
|
|
97
|
+
const envPatch = {
|
|
98
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
99
|
+
GCM_INTERACTIVE: "Never",
|
|
100
|
+
};
|
|
101
|
+
const githubToken = pickFirstNonEmpty([
|
|
102
|
+
prepareArgs.baseEnvPatch.GITHUB_TOKEN,
|
|
103
|
+
prepareArgs.baseEnvPatch.GH_TOKEN,
|
|
104
|
+
process.env.GITHUB_TOKEN,
|
|
105
|
+
process.env.GH_TOKEN,
|
|
106
|
+
]);
|
|
107
|
+
if (githubToken) {
|
|
108
|
+
envPatch.GITHUB_TOKEN = githubToken;
|
|
109
|
+
envPatch.GH_TOKEN = githubToken;
|
|
110
|
+
envPatch.GIT_ASKPASS_REQUIRE = "force";
|
|
111
|
+
envPatch.GIT_ASKPASS = await ensureGitAskpassScript(args.agentProjectDir);
|
|
112
|
+
}
|
|
113
|
+
const userName = pickFirstNonEmpty([
|
|
114
|
+
prepareArgs.baseEnvPatch.DOER_GIT_USER_NAME,
|
|
115
|
+
prepareArgs.baseEnvPatch.GIT_USER_NAME,
|
|
116
|
+
prepareArgs.baseEnvPatch.GIT_AUTHOR_NAME,
|
|
117
|
+
prepareArgs.baseEnvPatch.GIT_COMMITTER_NAME,
|
|
118
|
+
]);
|
|
119
|
+
const userEmail = pickFirstNonEmpty([
|
|
120
|
+
prepareArgs.baseEnvPatch.DOER_GIT_USER_EMAIL,
|
|
121
|
+
prepareArgs.baseEnvPatch.GIT_USER_EMAIL,
|
|
122
|
+
prepareArgs.baseEnvPatch.GIT_AUTHOR_EMAIL,
|
|
123
|
+
prepareArgs.baseEnvPatch.GIT_COMMITTER_EMAIL,
|
|
124
|
+
]);
|
|
125
|
+
const gitIdentityApplied = userName && userEmail
|
|
126
|
+
? applyGitIdentityIfPossible({
|
|
127
|
+
cwd: prepareArgs.cwd,
|
|
128
|
+
userName,
|
|
129
|
+
userEmail,
|
|
130
|
+
})
|
|
131
|
+
: false;
|
|
132
|
+
return {
|
|
133
|
+
envPatch,
|
|
134
|
+
meta: {
|
|
135
|
+
gitAskpassEnabled: Boolean(envPatch.GIT_ASKPASS),
|
|
136
|
+
gitIdentityApplied,
|
|
137
|
+
gitIdentityProvided: Boolean(userName && userEmail),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
resolveCodexHomePath,
|
|
143
|
+
resolveShellPath,
|
|
144
|
+
resolveTaskWorkspace,
|
|
145
|
+
prepareTaskGitEnv,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export async function postJson(url, body) {
|
|
2
|
+
const res = await fetch(url, {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: { "Content-Type": "application/json" },
|
|
5
|
+
body: JSON.stringify(body),
|
|
6
|
+
});
|
|
7
|
+
const text = await res.text();
|
|
8
|
+
let data = {};
|
|
9
|
+
if (text) {
|
|
10
|
+
try {
|
|
11
|
+
data = JSON.parse(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
data = {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const errObj = (data && typeof data === "object" ? data : {});
|
|
19
|
+
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
export async function getJson(url) {
|
|
25
|
+
const res = await fetch(url);
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
let data = {};
|
|
28
|
+
if (text) {
|
|
29
|
+
try {
|
|
30
|
+
data = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
data = {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const errObj = (data && typeof data === "object" ? data : {});
|
|
38
|
+
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
39
|
+
throw new Error(message);
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
export function createEventPersistenceHelpers(args) {
|
|
44
|
+
const nextEventSeqByTask = new Map();
|
|
45
|
+
async function recordAgentEvent(recordArgs) {
|
|
46
|
+
await args.publishEvent(recordArgs);
|
|
47
|
+
}
|
|
48
|
+
function reserveNextEventSeq(taskId) {
|
|
49
|
+
const current = nextEventSeqByTask.get(taskId) ?? 1;
|
|
50
|
+
nextEventSeqByTask.set(taskId, current + 1);
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
function emitAgentMetaLog(level, message) {
|
|
54
|
+
const ctx = args.getActiveTaskLogContext();
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const seq = reserveNextEventSeq(ctx.taskId);
|
|
59
|
+
void recordAgentEvent({
|
|
60
|
+
jetstream: ctx.jetstream,
|
|
61
|
+
serverBaseUrl: ctx.serverBaseUrl,
|
|
62
|
+
taskId: ctx.taskId,
|
|
63
|
+
userId: ctx.userId,
|
|
64
|
+
type: "meta",
|
|
65
|
+
seq,
|
|
66
|
+
payload: {
|
|
67
|
+
channel: "agent",
|
|
68
|
+
level,
|
|
69
|
+
message,
|
|
70
|
+
at: args.formatTimestamp(),
|
|
71
|
+
},
|
|
72
|
+
}).catch((error) => {
|
|
73
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
74
|
+
process.stderr.write(`[doer-agent] meta log persist failed task=${ctx.taskId}: ${detail}\n`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function persistEventOrFatal(persistArgs) {
|
|
78
|
+
void (async () => {
|
|
79
|
+
let attempt = 0;
|
|
80
|
+
let delayMs = 150;
|
|
81
|
+
while (attempt < 3) {
|
|
82
|
+
attempt += 1;
|
|
83
|
+
try {
|
|
84
|
+
await recordAgentEvent(persistArgs);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (attempt >= 3) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
args.onError(`task=${persistArgs.taskId} ${persistArgs.context}: ${message} (dropped after ${attempt} attempts)`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await args.sleep(delayMs);
|
|
94
|
+
delayMs *= 2;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
reserveNextEventSeq,
|
|
101
|
+
emitAgentMetaLog,
|
|
102
|
+
recordAgentEvent,
|
|
103
|
+
persistEventOrFatal,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function heartbeatAgentSession(args) {
|
|
107
|
+
await args.nc.flush();
|
|
108
|
+
await args.postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
|
|
109
|
+
userId: args.userId,
|
|
110
|
+
agentToken: args.agentToken,
|
|
111
|
+
});
|
|
112
|
+
}
|