@themoltnet/agent-daemon 0.23.0 → 0.25.1
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 +7 -8
- package/dist/main.js +307 -26
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -179,14 +179,13 @@ This matters for evals in particular. `run_eval` tasks declare their intended
|
|
|
179
179
|
workspace shape in `input.execution.workspace`: `none` becomes a
|
|
180
180
|
`scratch_mount`, `shared_mount` uses the daemon mount, and
|
|
181
181
|
`dedicated_worktree` uses an isolated checkout. Downstream
|
|
182
|
-
`judge_eval_attempt` tasks
|
|
183
|
-
session
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
producer
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
with `when.workspaceMode`.
|
|
182
|
+
`judge_eval_attempt` tasks can hydrate the producer Pi session from durable
|
|
183
|
+
runtime-session storage when producer slot/workspace metadata is available but
|
|
184
|
+
the local session file is unavailable. Workspace copying still depends on
|
|
185
|
+
producer slot/workspace metadata; if the daemon cannot resolve the required
|
|
186
|
+
producer context, the judge fails with `producer_context_missing`.
|
|
187
|
+
Repo-specific `resumeCommands` that should not run in scratch mode must still
|
|
188
|
+
be guarded with `when.workspaceMode`.
|
|
190
189
|
|
|
191
190
|
### 1. Start the local stack
|
|
192
191
|
|
package/dist/main.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseArgs, promisify } from "node:util";
|
|
|
12
12
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
|
|
13
13
|
import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
|
|
14
14
|
import { execFile, execFileSync } from "node:child_process";
|
|
15
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
15
|
+
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
16
16
|
import { MoltNetError, connect } from "@themoltnet/sdk";
|
|
17
17
|
import { once } from "node:events";
|
|
18
18
|
import { pino, transport } from "pino";
|
|
@@ -22,6 +22,8 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
|
22
22
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
23
23
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
|
+
import { mkdir } from "node:fs/promises";
|
|
26
|
+
import { pipeline } from "node:stream/promises";
|
|
25
27
|
//#region ../../libs/observability/src/instrumentation.ts
|
|
26
28
|
/**
|
|
27
29
|
* Register OTel auto-instrumentation for common Node.js modules.
|
|
@@ -4399,6 +4401,16 @@ var RuntimeProfileToolName = String$1({
|
|
|
4399
4401
|
maxLength: 128,
|
|
4400
4402
|
pattern: "^[a-zA-Z0-9._/-]+$"
|
|
4401
4403
|
});
|
|
4404
|
+
var RuntimeProfileWorkspaceMode = Union([
|
|
4405
|
+
Literal("none"),
|
|
4406
|
+
Literal("shared_mount"),
|
|
4407
|
+
Literal("dedicated_worktree")
|
|
4408
|
+
]);
|
|
4409
|
+
var RuntimeProfileAllowedWorkspaceModes = _Array_(RuntimeProfileWorkspaceMode, {
|
|
4410
|
+
minItems: 1,
|
|
4411
|
+
maxItems: 3,
|
|
4412
|
+
uniqueItems: true
|
|
4413
|
+
});
|
|
4402
4414
|
var SandboxResumeCommandWhenSchema = _Object_({ workspaceMode: Optional(_Array_(Union([
|
|
4403
4415
|
Literal("shared_mount"),
|
|
4404
4416
|
Literal("dedicated_worktree"),
|
|
@@ -4502,6 +4514,14 @@ var RuntimeProfileMaxBatchSize = Integer({
|
|
|
4502
4514
|
minimum: 1,
|
|
4503
4515
|
maximum: 1e3
|
|
4504
4516
|
});
|
|
4517
|
+
var RuntimeProfileMaxTurns = Integer({
|
|
4518
|
+
minimum: 0,
|
|
4519
|
+
maximum: 1e4
|
|
4520
|
+
});
|
|
4521
|
+
var RuntimeProfileMaxBashTimeouts = Integer({
|
|
4522
|
+
minimum: 0,
|
|
4523
|
+
maximum: 1e3
|
|
4524
|
+
});
|
|
4505
4525
|
_Object_({
|
|
4506
4526
|
id: String$1({ format: "uuid" }),
|
|
4507
4527
|
teamId: String$1({ format: "uuid" }),
|
|
@@ -4519,6 +4539,8 @@ _Object_({
|
|
|
4519
4539
|
sandbox: RuntimeProfileSandbox,
|
|
4520
4540
|
sessionStorageMode: Literal("local"),
|
|
4521
4541
|
workspaceStorageMode: Literal("local"),
|
|
4542
|
+
defaultWorkspaceMode: Union([RuntimeProfileWorkspaceMode, Null()]),
|
|
4543
|
+
allowedWorkspaceModes: RuntimeProfileAllowedWorkspaceModes,
|
|
4522
4544
|
sessionTtlSec: Integer({
|
|
4523
4545
|
minimum: 1,
|
|
4524
4546
|
maximum: 86400
|
|
@@ -4530,6 +4552,8 @@ _Object_({
|
|
|
4530
4552
|
leaseTtlSec: RuntimeProfileLeaseTtlSec,
|
|
4531
4553
|
heartbeatIntervalMs: RuntimeProfileHeartbeatIntervalMs,
|
|
4532
4554
|
maxBatchSize: RuntimeProfileMaxBatchSize,
|
|
4555
|
+
maxTurns: RuntimeProfileMaxTurns,
|
|
4556
|
+
maxBashTimeouts: RuntimeProfileMaxBashTimeouts,
|
|
4533
4557
|
requiredEnv: _Array_(RuntimeProfileEnvName, { maxItems: 100 }),
|
|
4534
4558
|
requiredTools: _Array_(RuntimeProfileToolName, { maxItems: 100 }),
|
|
4535
4559
|
context: _Array_(RuntimeProfileContext, { maxItems: 5 }),
|
|
@@ -4547,6 +4571,64 @@ _Object_({
|
|
|
4547
4571
|
additionalProperties: false
|
|
4548
4572
|
});
|
|
4549
4573
|
//#endregion
|
|
4574
|
+
//#region ../../libs/tasks/src/runtime-sessions.ts
|
|
4575
|
+
var RuntimeSessionKind = Union([
|
|
4576
|
+
Literal("root"),
|
|
4577
|
+
Literal("extend"),
|
|
4578
|
+
Literal("fork")
|
|
4579
|
+
]);
|
|
4580
|
+
var RuntimeSessionCheckpointKind = Union([Literal("attempt_final")]);
|
|
4581
|
+
_Object_({
|
|
4582
|
+
id: String$1({ format: "uuid" }),
|
|
4583
|
+
teamId: String$1({ format: "uuid" }),
|
|
4584
|
+
taskId: String$1({ format: "uuid" }),
|
|
4585
|
+
attemptN: Integer({ minimum: 1 }),
|
|
4586
|
+
sourceSlotId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4587
|
+
sourceRuntimeProfileId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4588
|
+
sessionKind: RuntimeSessionKind,
|
|
4589
|
+
parentSessionId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4590
|
+
contentType: String$1({
|
|
4591
|
+
minLength: 1,
|
|
4592
|
+
maxLength: 200
|
|
4593
|
+
}),
|
|
4594
|
+
contentEncoding: Union([String$1({
|
|
4595
|
+
minLength: 1,
|
|
4596
|
+
maxLength: 100
|
|
4597
|
+
}), Null()]),
|
|
4598
|
+
sizeBytes: Integer({ minimum: 0 }),
|
|
4599
|
+
sha256: String$1({
|
|
4600
|
+
minLength: 64,
|
|
4601
|
+
maxLength: 64
|
|
4602
|
+
}),
|
|
4603
|
+
storageClass: String$1({
|
|
4604
|
+
minLength: 1,
|
|
4605
|
+
maxLength: 100
|
|
4606
|
+
}),
|
|
4607
|
+
checkpointKind: RuntimeSessionCheckpointKind,
|
|
4608
|
+
uploadedAt: String$1({ format: "date-time" })
|
|
4609
|
+
}, { $id: "RuntimeSession" });
|
|
4610
|
+
_Object_({
|
|
4611
|
+
sourceSlotId: Optional(String$1({ format: "uuid" })),
|
|
4612
|
+
sourceRuntimeProfileId: Optional(String$1({ format: "uuid" })),
|
|
4613
|
+
sessionKind: RuntimeSessionKind,
|
|
4614
|
+
parentSessionId: Optional(String$1({ format: "uuid" }))
|
|
4615
|
+
}, {
|
|
4616
|
+
$id: "UploadRuntimeSessionQuery",
|
|
4617
|
+
additionalProperties: false
|
|
4618
|
+
});
|
|
4619
|
+
String$1({
|
|
4620
|
+
$id: "RuntimeSessionContent",
|
|
4621
|
+
description: "Runtime session content stream.",
|
|
4622
|
+
format: "binary"
|
|
4623
|
+
});
|
|
4624
|
+
_Object_({
|
|
4625
|
+
taskId: String$1({ format: "uuid" }),
|
|
4626
|
+
attemptN: Integer({ minimum: 1 })
|
|
4627
|
+
}, {
|
|
4628
|
+
$id: "RuntimeSessionAttemptParams",
|
|
4629
|
+
additionalProperties: false
|
|
4630
|
+
});
|
|
4631
|
+
//#endregion
|
|
4550
4632
|
//#region ../../libs/tasks/src/runtime-slots.ts
|
|
4551
4633
|
var RuntimeWorkspaceKind = Union([
|
|
4552
4634
|
Literal("origin"),
|
|
@@ -9449,9 +9531,9 @@ function slugifySessionComponent(input) {
|
|
|
9449
9531
|
}
|
|
9450
9532
|
//#endregion
|
|
9451
9533
|
//#region src/lib/task-execution-plan.ts
|
|
9452
|
-
function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec) {
|
|
9534
|
+
function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec, runtimeProfileWorkspacePolicy = {}) {
|
|
9453
9535
|
const descriptor = deriveTaskSessionDescriptor(task);
|
|
9454
|
-
const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy);
|
|
9536
|
+
const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
|
|
9455
9537
|
const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
|
|
9456
9538
|
const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
|
|
9457
9539
|
const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
|
|
@@ -9501,14 +9583,40 @@ function resolveTaskWorktreeBranch(task, workspaceMode) {
|
|
|
9501
9583
|
}
|
|
9502
9584
|
return `task/${slugifyAsciiLower(task.taskType, 60) || "task"}-${task.id.slice(0, 8)}`;
|
|
9503
9585
|
}
|
|
9504
|
-
function resolveTaskWorkspaceMode(task, policy) {
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9586
|
+
function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
|
|
9587
|
+
const allowed = resolveAllowedWorkspaceModes(runtimeProfileWorkspacePolicy);
|
|
9588
|
+
const profileDefault = runtimeProfileWorkspacePolicy.defaultWorkspaceMode ?? null;
|
|
9589
|
+
const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
|
|
9590
|
+
if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
|
|
9591
|
+
if (allowed.has(requestedWorkspace)) return toDaemonWorkspaceMode(requestedWorkspace);
|
|
9592
|
+
}
|
|
9593
|
+
if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
|
|
9594
|
+
if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
|
|
9595
|
+
return toDaemonWorkspaceMode(firstAllowedWorkspaceMode(allowed));
|
|
9596
|
+
}
|
|
9597
|
+
var ALL_WORKSPACE_MODES = [
|
|
9598
|
+
"none",
|
|
9599
|
+
"shared_mount",
|
|
9600
|
+
"dedicated_worktree"
|
|
9601
|
+
];
|
|
9602
|
+
var WORKSPACE_MODE_FALLBACK_ORDER = [
|
|
9603
|
+
"none",
|
|
9604
|
+
"dedicated_worktree",
|
|
9605
|
+
"shared_mount"
|
|
9606
|
+
];
|
|
9607
|
+
function resolveAllowedWorkspaceModes(policy) {
|
|
9608
|
+
const modes = policy.allowedWorkspaceModes && policy.allowedWorkspaceModes.length > 0 ? policy.allowedWorkspaceModes : ALL_WORKSPACE_MODES;
|
|
9609
|
+
return new Set(modes.filter((mode) => isRuntimeProfileWorkspaceMode(mode)));
|
|
9610
|
+
}
|
|
9611
|
+
function firstAllowedWorkspaceMode(allowed) {
|
|
9612
|
+
for (const mode of WORKSPACE_MODE_FALLBACK_ORDER) if (allowed.has(mode)) return mode;
|
|
9613
|
+
return "none";
|
|
9614
|
+
}
|
|
9615
|
+
function isRuntimeProfileWorkspaceMode(value) {
|
|
9616
|
+
return value === "none" || value === "shared_mount" || value === "dedicated_worktree";
|
|
9617
|
+
}
|
|
9618
|
+
function toDaemonWorkspaceMode(mode) {
|
|
9619
|
+
return mode === "none" ? "scratch_mount" : mode;
|
|
9512
9620
|
}
|
|
9513
9621
|
function resolveTaskWorkspaceId(task, executionPlan) {
|
|
9514
9622
|
if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
|
|
@@ -9524,12 +9632,14 @@ var ProducerContextResolutionError = class extends Error {
|
|
|
9524
9632
|
};
|
|
9525
9633
|
function createExecutionPlanCache(args) {
|
|
9526
9634
|
const cache = /* @__PURE__ */ new Map();
|
|
9635
|
+
const runtimeSessionStore = args.runtimeSessionStore ?? createNullRuntimeSessionStore();
|
|
9527
9636
|
return {
|
|
9528
9637
|
async getOrCreate(claimedTask) {
|
|
9529
9638
|
const key = buildClaimedTaskKey(claimedTask);
|
|
9530
9639
|
const existing = cache.get(key);
|
|
9531
9640
|
if (existing) return existing;
|
|
9532
|
-
const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec), args.stateDirs, args.slotRegistry);
|
|
9641
|
+
const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy), args.stateDirs, args.slotRegistry, runtimeSessionStore);
|
|
9642
|
+
assertPlanAllowedByWorkspacePolicy(plan, args.workspacePolicy);
|
|
9533
9643
|
cache.set(key, plan);
|
|
9534
9644
|
return plan;
|
|
9535
9645
|
},
|
|
@@ -9538,13 +9648,45 @@ function createExecutionPlanCache(args) {
|
|
|
9538
9648
|
}
|
|
9539
9649
|
};
|
|
9540
9650
|
}
|
|
9651
|
+
function createNullRuntimeSessionStore() {
|
|
9652
|
+
return {
|
|
9653
|
+
async findRuntimeSessionByTaskAttempt() {
|
|
9654
|
+
return null;
|
|
9655
|
+
},
|
|
9656
|
+
async hydrateSession() {
|
|
9657
|
+
throw new ProducerContextResolutionError("Cannot hydrate runtime session: no runtime session store configured");
|
|
9658
|
+
},
|
|
9659
|
+
async uploadAttemptFinal() {}
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
function assertPlanAllowedByWorkspacePolicy(plan, policy) {
|
|
9663
|
+
const allowed = new Set(policy?.allowedWorkspaceModes && policy.allowedWorkspaceModes.length > 0 ? policy.allowedWorkspaceModes : [
|
|
9664
|
+
"none",
|
|
9665
|
+
"shared_mount",
|
|
9666
|
+
"dedicated_worktree"
|
|
9667
|
+
]);
|
|
9668
|
+
const effectiveMode = planToRuntimeProfileWorkspaceMode(plan);
|
|
9669
|
+
if (!allowed.has(effectiveMode)) throw new Error(`Runtime profile forbids final workspace mode "${effectiveMode}" for this task`);
|
|
9670
|
+
}
|
|
9671
|
+
function planToRuntimeProfileWorkspaceMode(plan) {
|
|
9672
|
+
if (plan.workspaceMode === "scratch_mount") return "none";
|
|
9673
|
+
if (plan.workspaceMode === "dedicated_worktree" && !plan.worktreeBranch) return "shared_mount";
|
|
9674
|
+
return plan.workspaceMode;
|
|
9675
|
+
}
|
|
9541
9676
|
function buildClaimedTaskKey(task) {
|
|
9542
9677
|
return `${task.task.id}:${task.attemptN}`;
|
|
9543
9678
|
}
|
|
9544
|
-
async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
|
|
9679
|
+
async function resolveWarmSlot(slotRegistry, runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
|
|
9545
9680
|
const producerContext = await slotRegistry.findLatestSlotByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
|
|
9546
9681
|
if (!producerContext) return { kind: "missing" };
|
|
9547
|
-
const
|
|
9682
|
+
const localSessionPath = resolveProducerSessionPath(producerContext);
|
|
9683
|
+
const remoteSession = localSessionPath ? null : await runtimeSessionStore.findRuntimeSessionByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
|
|
9684
|
+
const sourceSessionPath = localSessionPath ? localSessionPath : remoteSession ? await runtimeSessionStore.hydrateSession({
|
|
9685
|
+
attemptN: sourceAttemptN,
|
|
9686
|
+
destinationDir: `${stateDirs.piSessionsDir}/remote-${sourceTaskId}-attempt-${sourceAttemptN}`,
|
|
9687
|
+
taskId: sourceTaskId,
|
|
9688
|
+
teamId
|
|
9689
|
+
}) : null;
|
|
9548
9690
|
if (!sourceSessionPath) return { kind: "no-session-path" };
|
|
9549
9691
|
return {
|
|
9550
9692
|
kind: "found",
|
|
@@ -9553,11 +9695,11 @@ async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttempt
|
|
|
9553
9695
|
workspacePath: resolveProducerWorkspaceCopySource(producerContext, stateDirs)
|
|
9554
9696
|
};
|
|
9555
9697
|
}
|
|
9556
|
-
async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry) {
|
|
9698
|
+
async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore) {
|
|
9557
9699
|
if (claimedTask.task.taskType === "freeform") {
|
|
9558
9700
|
const continueFrom = claimedTask.task.input.continueFrom;
|
|
9559
9701
|
if (!continueFrom) return basePlan;
|
|
9560
|
-
const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
|
|
9702
|
+
const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
|
|
9561
9703
|
if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no live runtime slot on this daemon — claim affinity filter should have prevented this claim`);
|
|
9562
9704
|
if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
|
|
9563
9705
|
const sessionDir = `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
|
|
@@ -9594,7 +9736,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
|
|
|
9594
9736
|
const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
|
|
9595
9737
|
const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
|
|
9596
9738
|
if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
|
|
9597
|
-
const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
|
|
9739
|
+
const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
|
|
9598
9740
|
if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
|
|
9599
9741
|
if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
|
|
9600
9742
|
return {
|
|
@@ -9798,6 +9940,8 @@ function parseCommonOptions(args, options = {}) {
|
|
|
9798
9940
|
leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
|
|
9799
9941
|
heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
|
|
9800
9942
|
maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
|
|
9943
|
+
maxTurns: options.runtimeDefaults?.maxTurns ?? DEFAULTS.maxTurns,
|
|
9944
|
+
maxBashTimeouts: options.runtimeDefaults?.maxBashTimeouts ?? DEFAULTS.maxBashTimeouts,
|
|
9801
9945
|
warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
|
|
9802
9946
|
};
|
|
9803
9947
|
if (!args.agent) throw new MissingRequiredOptionError("agent");
|
|
@@ -9808,8 +9952,8 @@ function parseCommonOptions(args, options = {}) {
|
|
|
9808
9952
|
heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
|
|
9809
9953
|
maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
|
|
9810
9954
|
flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
|
|
9811
|
-
maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns",
|
|
9812
|
-
maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts",
|
|
9955
|
+
maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", runtimeDefaults.maxTurns),
|
|
9956
|
+
maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", runtimeDefaults.maxBashTimeouts),
|
|
9813
9957
|
warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
|
|
9814
9958
|
debug: args.debug === true
|
|
9815
9959
|
};
|
|
@@ -9941,8 +10085,12 @@ async function resolveRuntimeProfile(options) {
|
|
|
9941
10085
|
leaseTtlSec: profile.leaseTtlSec,
|
|
9942
10086
|
heartbeatIntervalMs: profile.heartbeatIntervalMs,
|
|
9943
10087
|
maxBatchSize: profile.maxBatchSize,
|
|
10088
|
+
maxTurns: profile.maxTurns,
|
|
10089
|
+
maxBashTimeouts: profile.maxBashTimeouts,
|
|
9944
10090
|
sessionTtlSec: profile.sessionTtlSec,
|
|
9945
10091
|
workspaceTtlSec: profile.workspaceTtlSec,
|
|
10092
|
+
defaultWorkspaceMode: profile.defaultWorkspaceMode ?? null,
|
|
10093
|
+
allowedWorkspaceModes: profile.allowedWorkspaceModes,
|
|
9946
10094
|
requiredEnv: profile.requiredEnv,
|
|
9947
10095
|
requiredTools: profile.requiredTools,
|
|
9948
10096
|
sandboxConfig: profile.sandbox,
|
|
@@ -9998,6 +10146,70 @@ function isExecutable(path) {
|
|
|
9998
10146
|
}
|
|
9999
10147
|
}
|
|
10000
10148
|
//#endregion
|
|
10149
|
+
//#region src/lib/runtime-sessions.ts
|
|
10150
|
+
function resolveRuntimeSessionKind(claimedTask) {
|
|
10151
|
+
const continueFrom = resolveContinueFrom(claimedTask);
|
|
10152
|
+
if (!continueFrom) return "root";
|
|
10153
|
+
return continueFrom.mode === "fork" ? "fork" : "extend";
|
|
10154
|
+
}
|
|
10155
|
+
async function resolveParentRuntimeSession(runtimeSessionStore, claimedTask) {
|
|
10156
|
+
const continueFrom = resolveContinueFrom(claimedTask);
|
|
10157
|
+
if (!continueFrom) return null;
|
|
10158
|
+
return runtimeSessionStore.findRuntimeSessionByTaskAttempt(claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN);
|
|
10159
|
+
}
|
|
10160
|
+
function applyRuntimeSessionUploadFailure(output, err) {
|
|
10161
|
+
if (output.status !== "completed") return output;
|
|
10162
|
+
return {
|
|
10163
|
+
...output,
|
|
10164
|
+
contentSignature: void 0,
|
|
10165
|
+
error: {
|
|
10166
|
+
code: "runtime_session_upload_failed",
|
|
10167
|
+
message: "Task completed, but durable runtime session checkpoint upload failed: " + (err instanceof Error ? err.message : String(err)),
|
|
10168
|
+
retryable: true
|
|
10169
|
+
},
|
|
10170
|
+
output: null,
|
|
10171
|
+
outputCid: null,
|
|
10172
|
+
status: "failed"
|
|
10173
|
+
};
|
|
10174
|
+
}
|
|
10175
|
+
function createApiRuntimeSessionStore(args) {
|
|
10176
|
+
const { agent } = args;
|
|
10177
|
+
return {
|
|
10178
|
+
async findRuntimeSessionByTaskAttempt(teamId, taskId, attemptN) {
|
|
10179
|
+
return agent.runtimeSessions.getForAttempt({
|
|
10180
|
+
attemptN,
|
|
10181
|
+
taskId
|
|
10182
|
+
}, { teamId });
|
|
10183
|
+
},
|
|
10184
|
+
async hydrateSession(input) {
|
|
10185
|
+
const downloaded = await agent.runtimeSessions.download({
|
|
10186
|
+
attemptN: input.attemptN,
|
|
10187
|
+
taskId: input.taskId
|
|
10188
|
+
}, { teamId: input.teamId });
|
|
10189
|
+
await mkdir(input.destinationDir, { recursive: true });
|
|
10190
|
+
const sessionPath = join(input.destinationDir, `remote-${input.taskId}-attempt-${input.attemptN}.jsonl`);
|
|
10191
|
+
await pipeline(downloaded, createWriteStream(sessionPath));
|
|
10192
|
+
return sessionPath;
|
|
10193
|
+
},
|
|
10194
|
+
async uploadAttemptFinal(input) {
|
|
10195
|
+
const sessionPath = resolveLatestPiSessionPath(input.sessionDir);
|
|
10196
|
+
if (!sessionPath) throw new Error(`Cannot upload runtime session for ${input.taskId}/${input.attemptN}: no local session file in ${input.sessionDir}`);
|
|
10197
|
+
await agent.runtimeSessions.upload({
|
|
10198
|
+
attemptN: input.attemptN,
|
|
10199
|
+
taskId: input.taskId
|
|
10200
|
+
}, createReadStream(sessionPath), {
|
|
10201
|
+
parentSessionId: input.parentSessionId ?? void 0,
|
|
10202
|
+
sessionKind: input.sessionKind,
|
|
10203
|
+
sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
|
|
10204
|
+
sourceSlotId: input.sourceSlotId ?? void 0
|
|
10205
|
+
}, { teamId: input.teamId });
|
|
10206
|
+
}
|
|
10207
|
+
};
|
|
10208
|
+
}
|
|
10209
|
+
function resolveContinueFrom(claimedTask) {
|
|
10210
|
+
return claimedTask.task.input.continueFrom;
|
|
10211
|
+
}
|
|
10212
|
+
//#endregion
|
|
10001
10213
|
//#region src/lib/runtime-slots.ts
|
|
10002
10214
|
function createApiRuntimeSlotStore(args) {
|
|
10003
10215
|
const { agent } = args;
|
|
@@ -10039,7 +10251,11 @@ function createApiRuntimeSlotStore(args) {
|
|
|
10039
10251
|
}, { teamId });
|
|
10040
10252
|
if (!resolved) return null;
|
|
10041
10253
|
return {
|
|
10042
|
-
slot: {
|
|
10254
|
+
slot: {
|
|
10255
|
+
expiresAtMs: resolved.slot.expiresAtMs,
|
|
10256
|
+
id: resolved.slot.id,
|
|
10257
|
+
runtimeProfileId: resolved.slot.runtimeProfileId
|
|
10258
|
+
},
|
|
10043
10259
|
session: resolved.slot.sessionDir ? {
|
|
10044
10260
|
sessionDir: resolved.slot.sessionDir,
|
|
10045
10261
|
sessionPath: resolved.slot.sessionPath
|
|
@@ -10194,12 +10410,15 @@ async function runPolling(opts) {
|
|
|
10194
10410
|
});
|
|
10195
10411
|
for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
|
|
10196
10412
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
10413
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
|
|
10197
10414
|
const runtimes = /* @__PURE__ */ new Map();
|
|
10198
10415
|
for (const profile of profiles) {
|
|
10199
10416
|
const common = parseCommonOptions(values, { runtimeDefaults: {
|
|
10200
10417
|
leaseTtlSec: profile.leaseTtlSec,
|
|
10201
10418
|
heartbeatIntervalMs: profile.heartbeatIntervalMs,
|
|
10202
10419
|
maxBatchSize: profile.maxBatchSize,
|
|
10420
|
+
maxTurns: profile.maxTurns,
|
|
10421
|
+
maxBashTimeouts: profile.maxBashTimeouts,
|
|
10203
10422
|
warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
|
|
10204
10423
|
} });
|
|
10205
10424
|
const sandbox = {
|
|
@@ -10217,7 +10436,12 @@ async function runPolling(opts) {
|
|
|
10217
10436
|
stateDirs,
|
|
10218
10437
|
slotIdentity,
|
|
10219
10438
|
warmSessionTtlSec: common.warmSessionTtlSec,
|
|
10220
|
-
|
|
10439
|
+
workspacePolicy: {
|
|
10440
|
+
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
10441
|
+
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
10442
|
+
},
|
|
10443
|
+
slotRegistry,
|
|
10444
|
+
runtimeSessionStore
|
|
10221
10445
|
});
|
|
10222
10446
|
runtimes.set(profile.id, {
|
|
10223
10447
|
common,
|
|
@@ -10293,9 +10517,13 @@ async function runPolling(opts) {
|
|
|
10293
10517
|
sandbox: runtime.sandbox.path,
|
|
10294
10518
|
leaseTtlSec: runtime.common.leaseTtlSec,
|
|
10295
10519
|
heartbeatIntervalMs: runtime.common.heartbeatIntervalMs,
|
|
10520
|
+
maxTurns: runtime.common.maxTurns,
|
|
10521
|
+
maxBashTimeouts: runtime.common.maxBashTimeouts,
|
|
10296
10522
|
warmSessionTtlSec: runtime.common.warmSessionTtlSec,
|
|
10297
10523
|
profileSessionTtlSec: profile.sessionTtlSec,
|
|
10298
|
-
profileWorkspaceTtlSec: profile.workspaceTtlSec
|
|
10524
|
+
profileWorkspaceTtlSec: profile.workspaceTtlSec,
|
|
10525
|
+
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
10526
|
+
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
10299
10527
|
};
|
|
10300
10528
|
}),
|
|
10301
10529
|
piAgentDir: piAgentDir.path,
|
|
@@ -10322,7 +10550,8 @@ async function runPolling(opts) {
|
|
|
10322
10550
|
stopWhenEmpty: opts.stopWhenEmpty,
|
|
10323
10551
|
debug: baseCommon.debug,
|
|
10324
10552
|
logger: rootLogger,
|
|
10325
|
-
slotRegistry
|
|
10553
|
+
slotRegistry,
|
|
10554
|
+
sessionRegistry: runtimeSessionStore
|
|
10326
10555
|
}),
|
|
10327
10556
|
makeReporter: (claimedTask) => {
|
|
10328
10557
|
const selected = runtimeForClaimedTask(runtimes, claimedTask);
|
|
@@ -10337,7 +10566,28 @@ async function runPolling(opts) {
|
|
|
10337
10566
|
onTaskFinished: async (output, claimedTask) => {
|
|
10338
10567
|
const selected = runtimeForClaimedTask(runtimes, claimedTask);
|
|
10339
10568
|
const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
|
|
10340
|
-
|
|
10569
|
+
let terminalOutput = output;
|
|
10570
|
+
if (resolved?.session?.sessionDir) try {
|
|
10571
|
+
const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
|
|
10572
|
+
await runtimeSessionStore.uploadAttemptFinal({
|
|
10573
|
+
attemptN: claimedTask.attemptN,
|
|
10574
|
+
parentSessionId: parentSession?.id ?? null,
|
|
10575
|
+
sessionDir: resolved.session.sessionDir,
|
|
10576
|
+
sessionKind: resolveRuntimeSessionKind(claimedTask),
|
|
10577
|
+
sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
|
|
10578
|
+
sourceSlotId: resolved.slot.id,
|
|
10579
|
+
taskId: claimedTask.task.id,
|
|
10580
|
+
teamId: claimedTask.task.teamId
|
|
10581
|
+
});
|
|
10582
|
+
} catch (err) {
|
|
10583
|
+
rootLogger.error({
|
|
10584
|
+
err,
|
|
10585
|
+
taskId: claimedTask.task.id,
|
|
10586
|
+
attemptN: claimedTask.attemptN
|
|
10587
|
+
}, "agent-daemon.runtime_session_upload_failed");
|
|
10588
|
+
terminalOutput = applyRuntimeSessionUploadFailure(output, err);
|
|
10589
|
+
}
|
|
10590
|
+
return finalizeTask(ctx.agent, terminalOutput, {
|
|
10341
10591
|
task: claimedTask.task,
|
|
10342
10592
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
10343
10593
|
writeCorrelationAnchors: makePrBodyAnchorWriter({
|
|
@@ -10589,6 +10839,8 @@ async function runOnce(argv) {
|
|
|
10589
10839
|
leaseTtlSec: profile.leaseTtlSec,
|
|
10590
10840
|
heartbeatIntervalMs: profile.heartbeatIntervalMs,
|
|
10591
10841
|
maxBatchSize: profile.maxBatchSize,
|
|
10842
|
+
maxTurns: profile.maxTurns,
|
|
10843
|
+
maxBashTimeouts: profile.maxBashTimeouts,
|
|
10592
10844
|
warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
|
|
10593
10845
|
} });
|
|
10594
10846
|
const sandbox = {
|
|
@@ -10600,6 +10852,7 @@ async function runOnce(argv) {
|
|
|
10600
10852
|
activatePiCodingAgentDir(piAgentDir.path);
|
|
10601
10853
|
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
10602
10854
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
10855
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
|
|
10603
10856
|
const slotIdentity = {
|
|
10604
10857
|
agentName: opts.agent,
|
|
10605
10858
|
runtimeProfileId: profile.id
|
|
@@ -10608,7 +10861,12 @@ async function runOnce(argv) {
|
|
|
10608
10861
|
stateDirs,
|
|
10609
10862
|
slotIdentity,
|
|
10610
10863
|
warmSessionTtlSec: opts.warmSessionTtlSec,
|
|
10611
|
-
|
|
10864
|
+
workspacePolicy: {
|
|
10865
|
+
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
10866
|
+
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
10867
|
+
},
|
|
10868
|
+
slotRegistry,
|
|
10869
|
+
runtimeSessionStore
|
|
10612
10870
|
});
|
|
10613
10871
|
const otelShutdown = await initWorkerOtel({
|
|
10614
10872
|
serviceName: "moltnet.agent-daemon.once",
|
|
@@ -10639,6 +10897,8 @@ async function runOnce(argv) {
|
|
|
10639
10897
|
taskId,
|
|
10640
10898
|
leaseTtlSec: opts.leaseTtlSec,
|
|
10641
10899
|
heartbeatIntervalMs: opts.heartbeatIntervalMs,
|
|
10900
|
+
maxTurns: opts.maxTurns,
|
|
10901
|
+
maxBashTimeouts: opts.maxBashTimeouts,
|
|
10642
10902
|
warmSessionTtlSec: opts.warmSessionTtlSec,
|
|
10643
10903
|
profileId: profile.id,
|
|
10644
10904
|
profileSessionTtlSec: profile.sessionTtlSec,
|
|
@@ -10766,7 +11026,28 @@ async function runOnce(argv) {
|
|
|
10766
11026
|
}),
|
|
10767
11027
|
onTaskFinished: async (output, claimedTask) => {
|
|
10768
11028
|
const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
|
|
10769
|
-
|
|
11029
|
+
let terminalOutput = output;
|
|
11030
|
+
if (resolved?.session?.sessionDir) try {
|
|
11031
|
+
const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
|
|
11032
|
+
await runtimeSessionStore.uploadAttemptFinal({
|
|
11033
|
+
attemptN: claimedTask.attemptN,
|
|
11034
|
+
parentSessionId: parentSession?.id ?? null,
|
|
11035
|
+
sessionDir: resolved.session.sessionDir,
|
|
11036
|
+
sessionKind: resolveRuntimeSessionKind(claimedTask),
|
|
11037
|
+
sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
|
|
11038
|
+
sourceSlotId: resolved.slot.id,
|
|
11039
|
+
taskId: claimedTask.task.id,
|
|
11040
|
+
teamId: claimedTask.task.teamId
|
|
11041
|
+
});
|
|
11042
|
+
} catch (err) {
|
|
11043
|
+
rootLogger.error({
|
|
11044
|
+
err,
|
|
11045
|
+
taskId: claimedTask.task.id,
|
|
11046
|
+
attemptN: claimedTask.attemptN
|
|
11047
|
+
}, "agent-daemon.runtime_session_upload_failed");
|
|
11048
|
+
terminalOutput = applyRuntimeSessionUploadFailure(output, err);
|
|
11049
|
+
}
|
|
11050
|
+
return finalizeTask(ctx.agent, terminalOutput, {
|
|
10770
11051
|
task: claimedTask.task,
|
|
10771
11052
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
10772
11053
|
writeCorrelationAnchors,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
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.",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
46
46
|
"pino": "^10.3.1",
|
|
47
47
|
"pino-pretty": "^13.1.3",
|
|
48
|
-
"@themoltnet/pi-extension": "0.27.
|
|
49
|
-
"@themoltnet/sdk": "0.
|
|
50
|
-
"@themoltnet/agent-runtime": "0.
|
|
48
|
+
"@themoltnet/pi-extension": "0.27.2",
|
|
49
|
+
"@themoltnet/sdk": "0.113.1",
|
|
50
|
+
"@themoltnet/agent-runtime": "0.30.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"tsx": "^4.7.0",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"vite": "^8.0.0",
|
|
56
56
|
"vitest": "^3.0.0",
|
|
57
57
|
"@moltnet/bootstrap": "0.1.0",
|
|
58
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
58
59
|
"@moltnet/observability": "0.1.0",
|
|
59
|
-
"@moltnet/tasks": "0.1.0"
|
|
60
|
-
"@moltnet/crypto-service": "0.1.0"
|
|
60
|
+
"@moltnet/tasks": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"nx": {
|
|
63
63
|
"tags": [
|