@themoltnet/agent-daemon 0.10.0 → 0.10.2
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 +40 -2
- package/dist/main.js +107 -34
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -154,6 +154,19 @@ If a resume step assumes `/workspace` is a repo checkout, gate it on
|
|
|
154
154
|
- `scratch_mount` to skip repo-specific steps when the task runs in an empty
|
|
155
155
|
scratch workspace
|
|
156
156
|
|
|
157
|
+
This matters for evals in particular. `run_eval` tasks declare their intended
|
|
158
|
+
workspace shape in `input.execution.workspace`: `none` becomes a
|
|
159
|
+
`scratch_mount`, `shared_mount` uses the daemon mount, and
|
|
160
|
+
`dedicated_worktree` uses an isolated checkout. Downstream
|
|
161
|
+
`judge_eval_attempt` tasks only resolve against a still-live producer
|
|
162
|
+
session/workspace slot. If the producer slot has already expired and been
|
|
163
|
+
reaped, the judge fails with `producer_context_missing`. When the judge does
|
|
164
|
+
claim in time, it immediately forks the producer session and copies the
|
|
165
|
+
producer workspace into judge-owned scratch state so the running judge no
|
|
166
|
+
longer depends on the producer slot after claim time. Repo-specific
|
|
167
|
+
`resumeCommands` that should not run in scratch mode must still be guarded
|
|
168
|
+
with `when.workspaceMode`.
|
|
169
|
+
|
|
157
170
|
### 1. Start the local stack
|
|
158
171
|
|
|
159
172
|
The e2e Compose file ships everything the daemon needs (Postgres, Ory, REST
|
|
@@ -235,9 +248,32 @@ Leave it running. It idles until a task lands in its queue.
|
|
|
235
248
|
|
|
236
249
|
### 4. Create a task
|
|
237
250
|
|
|
238
|
-
In another terminal, with `.moltnet/local-dev/env` sourced
|
|
251
|
+
In another terminal, with `.moltnet/local-dev/env` sourced. Pick the CLI
|
|
252
|
+
form (recommended — schema-validates locally, no Node dependency in the
|
|
253
|
+
imposer path) or the template-driven TS form (legacy path that supports
|
|
254
|
+
`{{placeholder}}` substitution via `--set`).
|
|
255
|
+
|
|
256
|
+
::: code-group
|
|
257
|
+
|
|
258
|
+
```bash [CLI (recommended)]
|
|
259
|
+
BRIEF="Create a feature branch named feat/smoke-hello, write \
|
|
260
|
+
/workspace/demo/out/hello.txt with the single line 'hi from local-dev', \
|
|
261
|
+
commit the file with a signed diary entry per the runtime instructor, \
|
|
262
|
+
and report the branch name and commit sha in the final \
|
|
263
|
+
FulfillBriefOutput JSON. There is no remote to push to — leave \
|
|
264
|
+
pullRequestUrl null."
|
|
265
|
+
|
|
266
|
+
jq -n --arg brief "$BRIEF" \
|
|
267
|
+
--arg title "Smoke: hello file in a feature branch" \
|
|
268
|
+
'{brief: $brief, title: $title, scopeHint: "feature"}' \
|
|
269
|
+
| moltnet task create \
|
|
270
|
+
--task-type fulfill_brief \
|
|
271
|
+
--team-id "$MOLTNET_TEAM_ID" \
|
|
272
|
+
--diary-id "$MOLTNET_DIARY_ID" \
|
|
273
|
+
--credentials "$PWD/.moltnet/local-dev/moltnet.json"
|
|
274
|
+
```
|
|
239
275
|
|
|
240
|
-
```bash
|
|
276
|
+
```bash [tsx (legacy)]
|
|
241
277
|
pnpm exec tsx tools/src/tasks/create-task.ts \
|
|
242
278
|
--agent local-dev \
|
|
243
279
|
--task-file examples/tasks/api/fulfill-brief.create.template.json \
|
|
@@ -247,6 +283,8 @@ pnpm exec tsx tools/src/tasks/create-task.ts \
|
|
|
247
283
|
--set brief="Create a feature branch named feat/smoke-hello, write /workspace/demo/out/hello.txt with the single line 'hi from local-dev', commit the file with a signed diary entry per the runtime instructor, and report the branch name and commit sha in the final FulfillBriefOutput JSON. There is no remote to push to — leave pullRequestUrl null."
|
|
248
284
|
```
|
|
249
285
|
|
|
286
|
+
:::
|
|
287
|
+
|
|
250
288
|
> **Why a real coding brief**: `fulfill_brief` requires the agent to emit a
|
|
251
289
|
> structured `FulfillBriefOutput` JSON
|
|
252
290
|
> (`{ branch, commits, pullRequestUrl, diaryEntryIds, summary }`) as its
|
package/dist/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import crypto, { createHash } from "crypto";
|
|
3
|
-
import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseArgs, parseEnv, promisify } from "node:util";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
7
7
|
import { pino, transport } from "pino";
|
|
8
8
|
import { readFile } from "node:fs/promises";
|
|
@@ -4510,7 +4510,8 @@ var PrReviewSubject = Type$2.Object({
|
|
|
4510
4510
|
var PrReviewInput = Type$2.Object({
|
|
4511
4511
|
subject: PrReviewSubject,
|
|
4512
4512
|
taskPrompt: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4513
|
-
successCriteria: SuccessCriteria
|
|
4513
|
+
successCriteria: SuccessCriteria,
|
|
4514
|
+
context: Type$2.Optional(TaskContext)
|
|
4514
4515
|
}, {
|
|
4515
4516
|
$id: "PrReviewInput",
|
|
4516
4517
|
additionalProperties: false
|
|
@@ -6794,7 +6795,7 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
|
|
|
6794
6795
|
"If the accepted attempt output lists `artifacts[].path`, treat those",
|
|
6795
6796
|
"paths as relative to the current workspace root unless the output",
|
|
6796
6797
|
"explicitly says otherwise.",
|
|
6797
|
-
ctx.workspace.mode === "dedicated_worktree" ? `This attachment is a dedicated producer worktree${ctx.workspace.branch ? ` on branch \`${ctx.workspace.branch}\`` : ""}.` : ctx.workspace.mode === "scratch_mount" ? "This
|
|
6798
|
+
ctx.workspace.mode === "dedicated_worktree" ? `This attachment is a dedicated producer worktree${ctx.workspace.branch ? ` on branch \`${ctx.workspace.branch}\`` : ""}.` : ctx.workspace.mode === "scratch_mount" ? "This workspace is a fresh judge-owned scratch copy of the producer workspace." : "This attachment is the producer shared workspace mounted with shadow writes for safe inspection.",
|
|
6798
6799
|
""
|
|
6799
6800
|
].join("\n") : "";
|
|
6800
6801
|
return [
|
|
@@ -7102,6 +7103,29 @@ function buildRenderPackUserPrompt(input, ctx) {
|
|
|
7102
7103
|
"- Do NOT write diary entries unless a genuine incident occurs",
|
|
7103
7104
|
" (rendering failure, invariant violation).",
|
|
7104
7105
|
"",
|
|
7106
|
+
"## Fidelity Discipline",
|
|
7107
|
+
"",
|
|
7108
|
+
"These rules apply when you are producing the markdown yourself rather",
|
|
7109
|
+
"than relying on a deterministic `server:*` renderer.",
|
|
7110
|
+
"",
|
|
7111
|
+
"1. Preserve hedges and qualifiers verbatim.",
|
|
7112
|
+
" Source phrases like \"typically\", \"roughly\", \"about half\",",
|
|
7113
|
+
" \"in this codebase\", \"for this speaker\", and \"on most slides\"",
|
|
7114
|
+
" are load-bearing. Keep them. Do not turn a partial observation",
|
|
7115
|
+
" into a universal claim by stripping the hedge.",
|
|
7116
|
+
"2. Signal list truncation explicitly.",
|
|
7117
|
+
" If a source entry enumerates examples and you shorten the list,",
|
|
7118
|
+
" say so with markers like `e.g.`, `among others`, or",
|
|
7119
|
+
" `[truncated - see source for full list]`. Do not silently drop",
|
|
7120
|
+
" items from an enumeration in a way that looks lossless.",
|
|
7121
|
+
"3. Calibrate against fidelity scoring.",
|
|
7122
|
+
" A paraphrased rendered pack will be audited claim-by-claim for",
|
|
7123
|
+
" drift on quotes, numbers, file paths, hedges, polarity, and list",
|
|
7124
|
+
" completeness. Optimize for \"no detectable drift across a",
|
|
7125
|
+
" claim-by-claim audit\", not \"shorter at any cost\". When compressing, prefer",
|
|
7126
|
+
" tightening prose around a quote rather than altering the quote,",
|
|
7127
|
+
" and prefer summarising a list over silently truncating it.",
|
|
7128
|
+
"",
|
|
7105
7129
|
buildSelfVerificationBlock(ctx.taskId),
|
|
7106
7130
|
buildFinalOutputBlock({
|
|
7107
7131
|
taskType: "render_pack",
|
|
@@ -16270,6 +16294,8 @@ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
|
|
|
16270
16294
|
});
|
|
16271
16295
|
mkdirSync(scratchDir, { recursive: true });
|
|
16272
16296
|
}
|
|
16297
|
+
const workspaceSeed = executionPlan?.workspaceSeed ?? null;
|
|
16298
|
+
if (workspaceSeed) copyDirectoryContents(workspaceSeed.copyFromPath, scratchDir);
|
|
16273
16299
|
return {
|
|
16274
16300
|
mountPath: scratchDir,
|
|
16275
16301
|
cwdPath: scratchDir,
|
|
@@ -16391,6 +16417,68 @@ function gitRefExists(mainRepo, ref) {
|
|
|
16391
16417
|
return false;
|
|
16392
16418
|
}
|
|
16393
16419
|
}
|
|
16420
|
+
function copyDirectoryContents(sourceDir, targetDir) {
|
|
16421
|
+
if (!existsSync(sourceDir)) throw new Error(`Workspace seed source is missing: ${sourceDir}`);
|
|
16422
|
+
if (existsSync(join(sourceDir, ".git"))) initializeScratchGitRepo(sourceDir, targetDir);
|
|
16423
|
+
const resolvedTargetDir = realpathSync(targetDir);
|
|
16424
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
16425
|
+
const sourceEntry = join(sourceDir, entry);
|
|
16426
|
+
if (shouldSkipSeedEntry(sourceEntry, entry, resolvedTargetDir)) continue;
|
|
16427
|
+
cpSync(sourceEntry, join(targetDir, entry), { recursive: true });
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
function initializeScratchGitRepo(sourceDir, targetDir) {
|
|
16431
|
+
execFileSync("git", [
|
|
16432
|
+
"-C",
|
|
16433
|
+
targetDir,
|
|
16434
|
+
"init"
|
|
16435
|
+
], { stdio: "pipe" });
|
|
16436
|
+
let headCommit = null;
|
|
16437
|
+
try {
|
|
16438
|
+
headCommit = execFileSync("git", [
|
|
16439
|
+
"-C",
|
|
16440
|
+
sourceDir,
|
|
16441
|
+
"rev-parse",
|
|
16442
|
+
"HEAD"
|
|
16443
|
+
], {
|
|
16444
|
+
encoding: "utf8",
|
|
16445
|
+
stdio: "pipe"
|
|
16446
|
+
}).trim();
|
|
16447
|
+
} catch {
|
|
16448
|
+
headCommit = null;
|
|
16449
|
+
}
|
|
16450
|
+
if (!headCommit) return;
|
|
16451
|
+
execFileSync("git", [
|
|
16452
|
+
"-C",
|
|
16453
|
+
targetDir,
|
|
16454
|
+
"remote",
|
|
16455
|
+
"add",
|
|
16456
|
+
"origin",
|
|
16457
|
+
sourceDir
|
|
16458
|
+
], { stdio: "pipe" });
|
|
16459
|
+
execFileSync("git", [
|
|
16460
|
+
"-C",
|
|
16461
|
+
targetDir,
|
|
16462
|
+
"fetch",
|
|
16463
|
+
"--quiet",
|
|
16464
|
+
"--depth=1",
|
|
16465
|
+
"origin",
|
|
16466
|
+
headCommit
|
|
16467
|
+
], { stdio: "pipe" });
|
|
16468
|
+
execFileSync("git", [
|
|
16469
|
+
"-C",
|
|
16470
|
+
targetDir,
|
|
16471
|
+
"checkout",
|
|
16472
|
+
"--quiet",
|
|
16473
|
+
"--detach",
|
|
16474
|
+
"FETCH_HEAD"
|
|
16475
|
+
], { stdio: "pipe" });
|
|
16476
|
+
}
|
|
16477
|
+
function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
|
|
16478
|
+
if (entryName === ".git") return true;
|
|
16479
|
+
const resolvedSourceEntry = realpathSync(sourceEntry);
|
|
16480
|
+
return resolvedTargetDir === resolvedSourceEntry || resolvedTargetDir.startsWith(`${resolvedSourceEntry}${sep}`);
|
|
16481
|
+
}
|
|
16394
16482
|
//#endregion
|
|
16395
16483
|
//#region ../../libs/pi-extension/src/runtime/execute-pi-task.ts
|
|
16396
16484
|
/**
|
|
@@ -16596,7 +16684,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16596
16684
|
workspace: {
|
|
16597
16685
|
mode: activeWorkspace.mode,
|
|
16598
16686
|
branch: activeWorkspace.branch,
|
|
16599
|
-
attached: executionPlan?.workspaceAttachment !== void 0
|
|
16687
|
+
attached: executionPlan?.workspaceAttachment !== void 0 || executionPlan?.workspaceSeed?.source === "producer",
|
|
16688
|
+
source: executionPlan?.workspaceSeed?.source === "producer" ? "producer_copy" : executionPlan?.workspaceAttachment !== void 0 ? "producer_attachment" : void 0
|
|
16600
16689
|
},
|
|
16601
16690
|
extras: opts.promptExtras
|
|
16602
16691
|
});
|
|
@@ -17667,19 +17756,18 @@ function maybeAttachProducerContext(claimedTask, basePlan, stateDirs, slotRegist
|
|
|
17667
17756
|
const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
|
|
17668
17757
|
const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
|
|
17669
17758
|
if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
|
|
17670
|
-
const
|
|
17671
|
-
if (!
|
|
17672
|
-
const sourceSessionPath = resolveProducerSessionPath(
|
|
17759
|
+
const producerContext = slotRegistry.findLatestProducerSlotByTaskAttempt(targetTaskId, targetAttemptN);
|
|
17760
|
+
if (!producerContext) throw new ProducerContextResolutionError(`No live producer daemon slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
|
|
17761
|
+
const sourceSessionPath = resolveProducerSessionPath(producerContext);
|
|
17673
17762
|
if (!sourceSessionPath) throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
|
|
17674
|
-
const
|
|
17763
|
+
const copiedWorkspaceSource = resolveProducerWorkspaceCopySource(producerContext, stateDirs);
|
|
17675
17764
|
return {
|
|
17676
17765
|
...basePlan,
|
|
17677
|
-
workspaceMode:
|
|
17678
|
-
worktreeBranch:
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
shadowWrites: "tmpfs"
|
|
17766
|
+
workspaceMode: "scratch_mount",
|
|
17767
|
+
worktreeBranch: null,
|
|
17768
|
+
workspaceSeed: {
|
|
17769
|
+
copyFromPath: copiedWorkspaceSource,
|
|
17770
|
+
source: "producer"
|
|
17683
17771
|
},
|
|
17684
17772
|
sessionPersistence: {
|
|
17685
17773
|
sessionDir: `${stateDirs.piSessionsDir}/judge-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
|
|
@@ -17695,32 +17783,17 @@ function resolveProducerSessionPath(producer) {
|
|
|
17695
17783
|
const latest = resolveLatestPiSessionPath(sessionDir);
|
|
17696
17784
|
return latest && existsSync(latest) ? latest : null;
|
|
17697
17785
|
}
|
|
17698
|
-
function
|
|
17786
|
+
function resolveProducerWorkspaceCopySource(producer, stateDirs) {
|
|
17699
17787
|
const workspacePath = producer.workspace?.worktreePath ?? null;
|
|
17700
17788
|
if (workspacePath) {
|
|
17701
|
-
if (existsSync(workspacePath)) return
|
|
17702
|
-
mountPath: workspacePath,
|
|
17703
|
-
cwdPath: workspacePath,
|
|
17704
|
-
mode: producer.workspace?.worktreeBranch ? "dedicated_worktree" : "scratch_mount",
|
|
17705
|
-
branch: producer.workspace?.worktreeBranch ?? null
|
|
17706
|
-
};
|
|
17789
|
+
if (existsSync(workspacePath)) return workspacePath;
|
|
17707
17790
|
const recoveredPath = recoverScratchWorkspacePath(producer, stateDirs);
|
|
17708
|
-
if (recoveredPath) return
|
|
17709
|
-
mountPath: recoveredPath,
|
|
17710
|
-
cwdPath: recoveredPath,
|
|
17711
|
-
mode: "scratch_mount",
|
|
17712
|
-
branch: null
|
|
17713
|
-
};
|
|
17791
|
+
if (recoveredPath) return recoveredPath;
|
|
17714
17792
|
throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
|
|
17715
17793
|
}
|
|
17716
17794
|
const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
|
|
17717
17795
|
if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
|
|
17718
|
-
return
|
|
17719
|
-
mountPath: sharedMountRoot,
|
|
17720
|
-
cwdPath: sharedMountRoot,
|
|
17721
|
-
mode: "shared_mount",
|
|
17722
|
-
branch: null
|
|
17723
|
-
};
|
|
17796
|
+
return sharedMountRoot;
|
|
17724
17797
|
}
|
|
17725
17798
|
function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
17726
17799
|
if (producer.workspace?.worktreeBranch) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
-
"@themoltnet/
|
|
37
|
-
"@themoltnet/
|
|
38
|
-
"@themoltnet/
|
|
36
|
+
"@themoltnet/agent-runtime": "0.18.1",
|
|
37
|
+
"@themoltnet/pi-extension": "0.19.2",
|
|
38
|
+
"@themoltnet/sdk": "0.103.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
45
|
"@moltnet/bootstrap": "0.1.0",
|
|
46
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
47
46
|
"@moltnet/database": "0.1.0",
|
|
47
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
48
48
|
"@moltnet/tasks": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|