@themoltnet/agent-daemon 0.57.0 → 0.58.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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/cli.js +543 -342
  3. package/package.json +9 -9
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as cryptoService, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
3
2
  import { assertRuntimeAdapterSupportsProfile } from "./runtime.js";
3
+ import { a as cryptoService, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { pino, transport } from "pino";
6
6
  import { Type } from "typebox";
@@ -10,7 +10,7 @@ import "multiformats/hashes/sha2";
10
10
  import "typebox/value";
11
11
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
12
12
  import { parseArgs, promisify } from "node:util";
13
- import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
13
+ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
14
14
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
15
15
  import { FILE_SECRET_PROVIDER, FileSecretProvider, RegisterIdentityError, boundedIdentitySignal, connect, createNodeSecretProviderRegistry, register } from "@themoltnet/sdk/node";
16
16
  import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -27,7 +27,7 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
27
27
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
28
28
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
29
29
  import { homedir, platform, tmpdir } from "node:os";
30
- import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
30
+ import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
31
31
  import { AsyncLocalStorage } from "node:async_hooks";
32
32
  import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
33
33
  import { pipeline } from "node:stream/promises";
@@ -1937,18 +1937,6 @@ var RuntimeProfileRef = Type.Object({ profileId: Type.String({ format: "uuid" })
1937
1937
  $id: "RuntimeProfileRef",
1938
1938
  additionalProperties: false
1939
1939
  });
1940
- var RuntimeProfileLeaseTtlSec = Type.Integer({
1941
- minimum: 1,
1942
- maximum: 86400
1943
- });
1944
- var RuntimeProfileHeartbeatIntervalMs = Type.Integer({
1945
- minimum: 0,
1946
- maximum: 36e5
1947
- });
1948
- var RuntimeProfileMaxBatchSize = Type.Integer({
1949
- minimum: 1,
1950
- maximum: 1e3
1951
- });
1952
1940
  var RuntimeProfileMaxTurns = Type.Integer({
1953
1941
  minimum: 0,
1954
1942
  maximum: 1e4
@@ -1977,21 +1965,8 @@ Type.Object({
1977
1965
  maxOutputTokens: RuntimeProfileNullableMaxOutputTokens,
1978
1966
  runtimeKind: RuntimeProfileRuntimeKind,
1979
1967
  sandbox: RuntimeProfileSandbox,
1980
- sessionStorageMode: Type.Literal("local"),
1981
- workspaceStorageMode: Type.Literal("local"),
1982
1968
  defaultWorkspaceMode: Type.Union([RuntimeProfileWorkspaceMode, Type.Null()]),
1983
1969
  allowedWorkspaceModes: RuntimeProfileAllowedWorkspaceModes,
1984
- sessionTtlSec: Type.Integer({
1985
- minimum: 1,
1986
- maximum: 86400
1987
- }),
1988
- workspaceTtlSec: Type.Integer({
1989
- minimum: 1,
1990
- maximum: 86400
1991
- }),
1992
- leaseTtlSec: RuntimeProfileLeaseTtlSec,
1993
- heartbeatIntervalMs: RuntimeProfileHeartbeatIntervalMs,
1994
- maxBatchSize: RuntimeProfileMaxBatchSize,
1995
1970
  maxTurns: RuntimeProfileMaxTurns,
1996
1971
  maxBashTimeouts: RuntimeProfileMaxBashTimeouts,
1997
1972
  toolEnforcement: RuntimeProfileToolEnforcement,
@@ -2124,6 +2099,7 @@ var ResolvedRuntimeSlot = Type.Object({
2124
2099
  workspace: Type.Union([RuntimeWorkspace, Type.Null()])
2125
2100
  }, { $id: "ResolvedRuntimeSlot" });
2126
2101
  Type.Object({ items: Type.Array(ResolvedRuntimeSlot) }, { $id: "RuntimeSlotListResponse" });
2102
+ var MAX_RUNTIME_WARM_RETENTION_SEC = 86400;
2127
2103
  Type.Object({
2128
2104
  agentName: Type.String({
2129
2105
  minLength: 1,
@@ -2150,7 +2126,11 @@ Type.Object({
2150
2126
  worktreeBranch: Type.Optional(Type.String({ minLength: 1 })),
2151
2127
  workspaceKind: Type.Optional(RuntimeWorkspaceKind),
2152
2128
  lastTaskId: Type.String({ format: "uuid" }),
2153
- lastAttemptN: Type.Integer({ minimum: 1 })
2129
+ lastAttemptN: Type.Integer({ minimum: 1 }),
2130
+ warmRetentionSec: Type.Integer({
2131
+ minimum: 0,
2132
+ maximum: MAX_RUNTIME_WARM_RETENTION_SEC
2133
+ })
2154
2134
  }, {
2155
2135
  $id: "BeginRuntimeSlotBody",
2156
2136
  additionalProperties: false
@@ -2172,7 +2152,11 @@ Type.Object({
2172
2152
  slotKey: Type.String({ minLength: 1 }),
2173
2153
  taskId: Type.String({ format: "uuid" }),
2174
2154
  attemptN: Type.Integer({ minimum: 1 }),
2175
- sessionPath: Type.Optional(Type.String({ minLength: 1 }))
2155
+ sessionPath: Type.Optional(Type.String({ minLength: 1 })),
2156
+ warmRetentionSec: Type.Integer({
2157
+ minimum: 0,
2158
+ maximum: MAX_RUNTIME_WARM_RETENTION_SEC
2159
+ })
2176
2160
  }, {
2177
2161
  $id: "FinishRuntimeSlotBody",
2178
2162
  additionalProperties: false
@@ -3461,7 +3445,7 @@ var COMMON_REQUIRED_FLAGS = `\
3461
3445
  --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
3462
3446
  to declare priority order. Provider, model,
3463
3447
  sandbox policy, prerequisites, and runtime
3464
- defaults come from the selected profile.`;
3448
+ execution policy come from the selected profile.`;
3465
3449
  var COMMON_OPTIONAL_FLAGS = `\
3466
3450
  --sandbox <path> Deprecated. Remote runtime profiles define
3467
3451
  sandbox policy.
@@ -3473,24 +3457,10 @@ var COMMON_OPTIONAL_FLAGS = `\
3473
3457
  host git config. Configless agent-key runs must
3474
3458
  provide this flag or MOLTNET_GIT_AUTHOR.
3475
3459
  Env: MOLTNET_GIT_AUTHOR.
3476
- --lease-ttl-sec <n> Sliding liveness window. Silence longer than
3477
- this ends the attempt with lease_expired.
3478
- Default: 300.
3479
3460
  --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
3480
- --max-batch-size <n> Reporter message batch size. Default: 50.
3481
- --flush-interval-ms <n> Reporter flush window. Default: 200.
3482
- --max-turns <n> Cap on tool-use turns per attempt; over this,
3483
- the pi session aborts and the attempt fails
3484
- with code 'max_turns_exceeded'. 0 = disabled.
3485
- Default: 0. Recommended ~30 for fulfill_brief.
3486
- --max-bash-timeouts <n> Cap on bash-tool timeouts per attempt; over
3487
- this, the pi session aborts and the attempt
3488
- fails with code 'max_bash_timeouts_exceeded'.
3489
- 0 = disabled. Default: 3.
3490
- --warm-session-ttl-sec <n> Resumability window for runtime slots
3461
+ --warm-retention-sec <n> Resumability window for runtime slots
3491
3462
  (Pi sessions + reusable worktrees) after use.
3492
- Default: 1800, or min(profile session/workspace
3493
- TTL) when --profile is set.
3463
+ Default: 1800.
3494
3464
  --debug Verbose logging: also log successful list/claim
3495
3465
  outcomes (candidate counts, claim attempts).`;
3496
3466
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -3681,10 +3651,17 @@ Options:
3681
3651
  (or MOLTNET_AGENT_SERVER_ROOT).
3682
3652
  --api-url <url> Default MoltNet API for new managed agents.
3683
3653
  Default: https://api.themolt.net.
3654
+ --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
3655
+ --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
3656
+ --supervised Also stop gracefully when stdin reaches EOF.
3684
3657
 
3685
3658
  On macOS, the first interactive run asks to trust a per-user local CA in the
3686
- login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
3687
- remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
3659
+ login keychain and serves HTTPS. Native supervisors use:
3660
+ server trust --status --json
3661
+ server trust --yes --json
3662
+ server trust --remove --yes --json
3663
+ Run \`agent-daemon server trust --remove\` interactively to remove that exact
3664
+ CA. Linux continues to use the Chromium PNA HTTP path.
3688
3665
  `;
3689
3666
  var PROVIDERS_HELP = `\
3690
3667
  moltnet-agent providers — manage local model providers.
@@ -3693,6 +3670,7 @@ Usage:
3693
3670
  moltnet-agent providers list [--json] [--root <path>]
3694
3671
  moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
3695
3672
  [--model <id> ... | --clear-models]
3673
+ [--model-input <id>=text,image ...]
3696
3674
  [--api-key-stdin | --clear-api-key] [--root <path>]
3697
3675
  moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
3698
3676
  moltnet-agent providers remove <id> [--yes] [--root <path>]
@@ -3703,6 +3681,11 @@ Usage:
3703
3681
  The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
3704
3682
  environment override. API keys are accepted only from redirected stdin; they
3705
3683
  are stored separately and providers.json contains only a secret reference.
3684
+
3685
+ --model declares a text-only model. --model-input declares a model together
3686
+ with the input modalities it accepts, and is what makes a vision model usable:
3687
+ a model with no declared modalities is text-only to Pi, which drops image
3688
+ content parts before the request leaves the runtime.
3706
3689
  `;
3707
3690
  //#endregion
3708
3691
  //#region src/lib/identity-pin.ts
@@ -4189,10 +4172,10 @@ function slugifySessionComponent(input) {
4189
4172
  }
4190
4173
  //#endregion
4191
4174
  //#region src/lib/task-execution-plan.ts
4192
- function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
4175
+ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmRetentionSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
4193
4176
  const descriptor = deriveTaskSessionDescriptor(task);
4194
4177
  const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
4195
- const slotKey = warmSessionTtlSec > 0 && descriptor.sessionKey ? buildRuntimeSlotKey(descriptor.sessionKey, identity.runtimeInstanceId) : null;
4178
+ const slotKey = warmRetentionSec > 0 && descriptor.sessionKey ? buildRuntimeSlotKey(descriptor.sessionKey, identity.runtimeInstanceId) : null;
4196
4179
  const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
4197
4180
  const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
4198
4181
  const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${boundedKeyDirComponent(slotId)}` : null;
@@ -4331,7 +4314,7 @@ function createExecutionPlanCache(args) {
4331
4314
  const key = buildClaimedTaskKey(claimedTask);
4332
4315
  const existing = cache.get(key);
4333
4316
  if (existing) return existing;
4334
- const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy, claimedTask.attemptN), args.stateDirs, args.slotRegistry, runtimeSessionStore, sourceAttemptResolver);
4317
+ const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmRetentionSec, args.workspacePolicy, claimedTask.attemptN), args.stateDirs, args.slotRegistry, runtimeSessionStore, sourceAttemptResolver);
4335
4318
  assertPlanAllowedByWorkspacePolicy(plan, args.workspacePolicy, args.slotIdentity.runtimeProfileId);
4336
4319
  cache.set(key, plan);
4337
4320
  return plan;
@@ -5022,7 +5005,8 @@ function ownBinding(bindings, name) {
5022
5005
  }
5023
5006
  //#endregion
5024
5007
  //#region ../../libs/execution-integrations/src/runtime-profile.ts
5025
- var CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION = "effective-policy:v1";
5008
+ var CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION = "effective-policy:v2";
5009
+ var DEFAULT_EXECUTION_LEASE_TTL_SEC = 300;
5026
5010
  /**
5027
5011
  * Maps resolved product authority into portable intent. Policy composition is
5028
5012
  * deliberately upstream: this boundary accepts no policy IDs and performs no
@@ -5046,7 +5030,7 @@ function executionIntentFromRuntimeProfile(input) {
5046
5030
  credentialRequirements: structuredClone(input.credentialRequirements),
5047
5031
  requiredCapabilities: [...input.requiredCapabilities ?? []],
5048
5032
  lease: {
5049
- ttlSec: input.profile.leaseTtlSec,
5033
+ ttlSec: DEFAULT_EXECUTION_LEASE_TTL_SEC,
5050
5034
  requiredControls: [...input.requiredLeaseControls ?? []]
5051
5035
  },
5052
5036
  network: {
@@ -5296,14 +5280,9 @@ async function logDaemonStartupFailure(input) {
5296
5280
  }
5297
5281
  //#endregion
5298
5282
  //#region src/lib/options.ts
5299
- var DEFAULTS = {
5300
- leaseTtlSec: 300,
5283
+ var DEFAULT_LOCAL_OPERATIONAL_SETTINGS = {
5301
5284
  heartbeatIntervalMs: 6e4,
5302
- maxBatchSize: 50,
5303
- flushIntervalMs: 200,
5304
- maxTurns: 0,
5305
- maxBashTimeouts: 3,
5306
- warmSessionTtlSec: 1800
5285
+ warmRetentionSec: 1800
5307
5286
  };
5308
5287
  var MissingRequiredOptionError = class extends Error {
5309
5288
  constructor(flag) {
@@ -5312,42 +5291,33 @@ var MissingRequiredOptionError = class extends Error {
5312
5291
  this.name = "MissingRequiredOptionError";
5313
5292
  }
5314
5293
  };
5315
- function parseCommonOptions(args, options = {}) {
5316
- const runtimeDefaults = {
5317
- leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
5318
- heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
5319
- maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
5320
- maxTurns: options.runtimeDefaults?.maxTurns ?? DEFAULTS.maxTurns,
5321
- maxBashTimeouts: options.runtimeDefaults?.maxBashTimeouts ?? DEFAULTS.maxBashTimeouts,
5322
- warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
5323
- };
5294
+ function parseIdentityProcessOptions(args) {
5324
5295
  if (!args.agent) throw new MissingRequiredOptionError("agent");
5325
5296
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
5326
5297
  return {
5327
5298
  agent: args.agent,
5328
- leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
5329
- heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
5330
- maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
5331
- flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
5332
- maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", runtimeDefaults.maxTurns),
5333
- maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", runtimeDefaults.maxBashTimeouts),
5334
- warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
5335
5299
  debug: args.debug === true
5336
5300
  };
5337
5301
  }
5338
- function parsePositiveInt(raw, name, defaultValue) {
5339
- if (raw === void 0) return defaultValue;
5340
- const v = Number(raw);
5341
- if (!Number.isInteger(v) || v < 1) throw new Error(`Invalid --${name} "${raw}": must be a positive integer`);
5342
- return v;
5302
+ function parseLocalOperationalSettings(args) {
5303
+ return {
5304
+ heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULT_LOCAL_OPERATIONAL_SETTINGS.heartbeatIntervalMs),
5305
+ warmRetentionSec: parseNonNegativeInt(args["warm-retention-sec"], "warm-retention-sec", DEFAULT_LOCAL_OPERATIONAL_SETTINGS.warmRetentionSec, MAX_RUNTIME_WARM_RETENTION_SEC)
5306
+ };
5307
+ }
5308
+ function parseRuntimeCommandOptions(args) {
5309
+ return {
5310
+ identity: parseIdentityProcessOptions(args),
5311
+ operations: parseLocalOperationalSettings(args)
5312
+ };
5343
5313
  }
5344
- function parseNonNegativeInt(raw, name, defaultValue) {
5314
+ function parseNonNegativeInt(raw, name, defaultValue, maximum = Number.MAX_SAFE_INTEGER) {
5345
5315
  if (raw === void 0) return defaultValue;
5346
- const v = Number(raw);
5347
- if (!Number.isInteger(v) || v < 0) throw new Error(`Invalid --${name} "${raw}": must be a non-negative integer`);
5348
- return v;
5316
+ const value = Number(raw);
5317
+ if (!Number.isInteger(value) || value < 0 || value > maximum) throw new Error(`Invalid --${name} "${raw}": must be a non-negative integer no greater than ${maximum}`);
5318
+ return value;
5349
5319
  }
5350
- function commonOptionDefs() {
5320
+ function identityOptionDefs() {
5351
5321
  return {
5352
5322
  agent: {
5353
5323
  type: "string",
@@ -5355,18 +5325,18 @@ function commonOptionDefs() {
5355
5325
  },
5356
5326
  "agent-root": { type: "string" },
5357
5327
  "git-author": { type: "string" },
5358
- "lease-ttl-sec": { type: "string" },
5359
- "heartbeat-interval-ms": { type: "string" },
5360
- "max-batch-size": { type: "string" },
5361
- "flush-interval-ms": { type: "string" },
5362
- "max-turns": { type: "string" },
5363
- "max-bash-timeouts": { type: "string" },
5364
- "warm-session-ttl-sec": { type: "string" },
5365
5328
  debug: { type: "boolean" }
5366
5329
  };
5367
5330
  }
5331
+ function runtimeCommandOptionDefs() {
5332
+ return {
5333
+ ...identityOptionDefs(),
5334
+ "heartbeat-interval-ms": { type: "string" },
5335
+ "warm-retention-sec": { type: "string" }
5336
+ };
5337
+ }
5368
5338
  function validateTaskTypes(types) {
5369
- const unknown = types.filter((t) => !Object.prototype.hasOwnProperty.call(BUILT_IN_TASK_TYPES, t));
5339
+ const unknown = types.filter((taskType) => !Object.prototype.hasOwnProperty.call(BUILT_IN_TASK_TYPES, taskType));
5370
5340
  if (unknown.length > 0) throw new Error(`Unknown task type(s): ${unknown.join(", ")}. Known types: ${knownTaskTypesList()}.`);
5371
5341
  return [...types];
5372
5342
  }
@@ -5465,6 +5435,17 @@ function resolveAgentServerRoot(input) {
5465
5435
  if (override) return override;
5466
5436
  return getConfigDir();
5467
5437
  }
5438
+ /**
5439
+ * Copy a model entry, dropping an empty `input` so it never reaches the wire.
5440
+ * Not a compatibility shim: `providers.json` has exactly one model shape, and
5441
+ * `validateProviders` rejects anything else on read.
5442
+ */
5443
+ function copyProviderModel(entry) {
5444
+ return {
5445
+ id: entry.id,
5446
+ ...entry.input && entry.input.length > 0 ? { input: [...entry.input] } : {}
5447
+ };
5448
+ }
5468
5449
  function providerEnvName(providerId) {
5469
5450
  return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
5470
5451
  }
@@ -5663,6 +5644,7 @@ var AgentServerStore = class {
5663
5644
  for (const [id, provider] of Object.entries(state)) {
5664
5645
  assertProviderId(id);
5665
5646
  assertProviderEnvName(id, provider.envName);
5647
+ for (const model of provider.models ?? []) if (typeof model !== "object" || model === null || !model.id) throw new AgentServerStoreError("invalid_state", `provider "${id}" has a model entry that is not { id, input? }; rewrite providers.json entries as objects`);
5666
5648
  }
5667
5649
  }
5668
5650
  runDir(id) {
@@ -5936,6 +5918,64 @@ function mergePiModels(store, repo) {
5936
5918
  return { providers };
5937
5919
  }
5938
5920
  //#endregion
5921
+ //#region src/lib/state-dir.ts
5922
+ function ensureDaemonStateDirs(mountPath) {
5923
+ const rootDir = join(mountPath, ".moltnet", "d");
5924
+ const piSessionsDir = join(rootDir, "pi-sessions");
5925
+ mkdirSync(piSessionsDir, { recursive: true });
5926
+ return {
5927
+ rootDir,
5928
+ piSessionsDir
5929
+ };
5930
+ }
5931
+ //#endregion
5932
+ //#region src/lib/prepare-runtime-profile.ts
5933
+ /** Validate and prepare a profile through the shared daemon execution path. */
5934
+ async function prepareRuntimeProfile(input) {
5935
+ const { profile } = input;
5936
+ assertRuntimeAdapterSupportsProfile(input.runtimeAdapter, profile);
5937
+ const preparedRuntime = attestPreparedRuntime(await input.runtimeAdapter.prepare({ profile }), input.signingPrivateKey);
5938
+ validateRuntimeProfilePrerequisites(profile, input.prerequisiteEnv, {
5939
+ tools: preparedRuntime.tools,
5940
+ executables: preparedRuntime.executables
5941
+ });
5942
+ assertGuestEnvironmentBoundary({
5943
+ forwardEnv: profile.requiredEnv,
5944
+ sandboxEnv: profile.sandboxConfig.env
5945
+ });
5946
+ await input.agent.tasks.registerExecutorManifest(await preparedRuntime.attestor.registration());
5947
+ const sandbox = {
5948
+ config: profile.sandboxConfig,
5949
+ rootDir: profile.mountPath,
5950
+ path: profile.source
5951
+ };
5952
+ const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
5953
+ const slotIdentity = {
5954
+ agentName: input.agentName,
5955
+ runtimeProfileId: profile.id,
5956
+ runtimeInstanceId: input.runtimeInstanceId
5957
+ };
5958
+ return {
5959
+ profile,
5960
+ preparedRuntime,
5961
+ sandbox,
5962
+ stateDirs,
5963
+ slotIdentity,
5964
+ executionPlans: createExecutionPlanCache({
5965
+ stateDirs,
5966
+ slotIdentity,
5967
+ warmRetentionSec: input.warmRetentionSec,
5968
+ workspacePolicy: {
5969
+ defaultWorkspaceMode: profile.defaultWorkspaceMode,
5970
+ allowedWorkspaceModes: profile.allowedWorkspaceModes
5971
+ },
5972
+ slotRegistry: input.slotRegistry,
5973
+ runtimeSessionStore: input.runtimeSessionStore,
5974
+ sourceAttemptResolver: input.sourceAttemptResolver
5975
+ })
5976
+ };
5977
+ }
5978
+ //#endregion
5939
5979
  //#region src/lib/runtime-context.ts
5940
5980
  var storage = new AsyncLocalStorage();
5941
5981
  function runWithDaemonRuntimeContext(context, callback) {
@@ -6331,13 +6371,14 @@ function createApiRuntimeSlotStore(args) {
6331
6371
  sessionPath: input.sessionPath ?? void 0,
6332
6372
  slotKey: input.slotKey,
6333
6373
  taskType: input.taskType,
6374
+ warmRetentionSec: input.warmRetentionSec,
6334
6375
  workspaceId: input.workspaceId ?? void 0,
6335
6376
  workspaceKind: input.workspaceKind,
6336
6377
  worktreeBranch: input.worktreeBranch ?? void 0,
6337
6378
  worktreePath: input.worktreePath ?? void 0
6338
6379
  }, { teamId: input.teamId });
6339
6380
  },
6340
- async finishSlot(teamId, taskId, attemptN, identity, slotKey, provider, model, sessionPath) {
6381
+ async finishSlot(teamId, taskId, attemptN, identity, slotKey, provider, model, sessionPath, warmRetentionSec) {
6341
6382
  await agent.runtimeSlots.finish({
6342
6383
  agentName: identity.agentName,
6343
6384
  attemptN,
@@ -6346,7 +6387,8 @@ function createApiRuntimeSlotStore(args) {
6346
6387
  provider,
6347
6388
  sessionPath: sessionPath ?? void 0,
6348
6389
  slotKey,
6349
- taskId
6390
+ taskId,
6391
+ warmRetentionSec
6350
6392
  }, { teamId });
6351
6393
  },
6352
6394
  async findLatestSlotByTaskAttempt(teamId, taskId, attemptN) {
@@ -6482,17 +6524,6 @@ function resolveOutputBranch(output) {
6482
6524
  return typeof branch === "string" && branch.length > 0 ? branch : null;
6483
6525
  }
6484
6526
  //#endregion
6485
- //#region src/lib/state-dir.ts
6486
- function ensureDaemonStateDirs(mountPath) {
6487
- const rootDir = join(mountPath, ".moltnet", "d");
6488
- const piSessionsDir = join(rootDir, "pi-sessions");
6489
- mkdirSync(piSessionsDir, { recursive: true });
6490
- return {
6491
- rootDir,
6492
- piSessionsDir
6493
- };
6494
- }
6495
- //#endregion
6496
6527
  //#region src/lib/turn-event-logger.ts
6497
6528
  function makeTurnEventHandler(base, context = {}) {
6498
6529
  const log = base.child({
@@ -6523,7 +6554,7 @@ async function runPolling(opts) {
6523
6554
  const { values } = parseArgs({
6524
6555
  args: opts.argv,
6525
6556
  options: {
6526
- ...commonOptionDefs(),
6557
+ ...runtimeCommandOptionDefs(),
6527
6558
  team: { type: "string" },
6528
6559
  "task-types": { type: "string" },
6529
6560
  "correlation-id": { type: "string" },
@@ -6561,9 +6592,9 @@ async function runPolling(opts) {
6561
6592
  return 1;
6562
6593
  }
6563
6594
  const diaryIds = parseCsv(values["diary-ids"]);
6564
- let baseCommon;
6595
+ let commandOptions;
6565
6596
  try {
6566
- baseCommon = parseCommonOptions(values);
6597
+ commandOptions = parseRuntimeCommandOptions(values);
6567
6598
  } catch (err) {
6568
6599
  if (err instanceof MissingRequiredOptionError) {
6569
6600
  console.error(`${err.message}\n`);
@@ -6572,6 +6603,7 @@ async function runPolling(opts) {
6572
6603
  }
6573
6604
  throw err;
6574
6605
  }
6606
+ const { identity, operations } = commandOptions;
6575
6607
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
6576
6608
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
6577
6609
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
@@ -6596,7 +6628,7 @@ async function runPolling(opts) {
6596
6628
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
6597
6629
  let gate = "resolve_agent_context";
6598
6630
  try {
6599
- const resolvedContext = await resolveAgentContext(baseCommon.agent, {
6631
+ const resolvedContext = await resolveAgentContext(identity.agent, {
6600
6632
  agentRootDir: explicitAgentRootDir,
6601
6633
  credentialSource: cfg.credentialSource,
6602
6634
  envApiUrl: cfg.apiUrl
@@ -6623,7 +6655,7 @@ async function runPolling(opts) {
6623
6655
  });
6624
6656
  gate = "resolve_agent_identity";
6625
6657
  const agentIdentity = await resolveDaemonAgentIdentity({
6626
- agentName: baseCommon.agent,
6658
+ agentName: identity.agent,
6627
6659
  whoami,
6628
6660
  credentialSource: cfg.credentialSource,
6629
6661
  agentDir: resolvedContext.agentDir,
@@ -6643,9 +6675,9 @@ async function runPolling(opts) {
6643
6675
  } catch (error) {
6644
6676
  await logDaemonStartupFailure({
6645
6677
  serviceName: `agent-daemon.${opts.modeLabel}`,
6646
- level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
6678
+ level: cfg.logLevel || (identity.debug ? "debug" : "info"),
6647
6679
  gate,
6648
- agent: baseCommon.agent,
6680
+ agent: identity.agent,
6649
6681
  credentialSource: cfg.credentialSource,
6650
6682
  error
6651
6683
  });
@@ -6659,23 +6691,43 @@ async function runPolling(opts) {
6659
6691
  teamId,
6660
6692
  cwd: daemonRootDir
6661
6693
  });
6694
+ const { logger, shutdown: shutdownLogger } = createRootLogger({
6695
+ name: `agent-daemon.${opts.modeLabel}`,
6696
+ level: cfg.logLevel || (identity.debug ? "debug" : "info")
6697
+ });
6698
+ const rootLogger = logger.child({
6699
+ mode: opts.modeLabel,
6700
+ agent: identity.agent,
6701
+ teamId,
6702
+ runtimeProfileIds: resolvedProfiles.map((profile) => profile.id),
6703
+ runtimeProfileNames: resolvedProfiles.map((profile) => profile.name)
6704
+ });
6662
6705
  const runtimeAdapter = opts.runtimeAdapter ?? defaultPiDaemonAdapter;
6663
- const preparedRuntimes = /* @__PURE__ */ new Map();
6664
6706
  const profiles = [];
6707
+ const runtimes = /* @__PURE__ */ new Map();
6665
6708
  const skippedProfileBoundaries = [];
6709
+ const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
6710
+ const runtimeSessionStore = createApiRuntimeSessionStore({
6711
+ agent: ctx.agent,
6712
+ logger: { warn: (context, message) => rootLogger.warn(context, message) }
6713
+ });
6714
+ const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
6715
+ const runtimeInstanceId = createRuntimeInstanceId();
6666
6716
  for (const profile of resolvedProfiles) try {
6667
- assertRuntimeAdapterSupportsProfile(runtimeAdapter, profile);
6668
- const prepared = attestPreparedRuntime(await runtimeAdapter.prepare({ profile }), signingPrivateKey);
6669
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, {
6670
- tools: prepared.tools,
6671
- executables: prepared.executables
6672
- });
6673
- assertGuestEnvironmentBoundary({
6674
- forwardEnv: profile.requiredEnv,
6675
- sandboxEnv: profile.sandboxConfig.env
6717
+ const prepared = await prepareRuntimeProfile({
6718
+ agent: ctx.agent,
6719
+ agentName: identity.agent,
6720
+ profile,
6721
+ prerequisiteEnv: cfg.profilePrerequisiteEnv,
6722
+ runtimeAdapter,
6723
+ runtimeInstanceId,
6724
+ signingPrivateKey,
6725
+ slotRegistry,
6726
+ runtimeSessionStore,
6727
+ sourceAttemptResolver,
6728
+ warmRetentionSec: operations.warmRetentionSec
6676
6729
  });
6677
- await ctx.agent.tasks.registerExecutorManifest(await prepared.attestor.registration());
6678
- preparedRuntimes.set(profile.id, prepared);
6730
+ runtimes.set(profile.id, prepared);
6679
6731
  profiles.push(profile);
6680
6732
  } catch (error) {
6681
6733
  if (!(error instanceof GuestEnvironmentBoundaryError)) throw error;
@@ -6688,84 +6740,22 @@ async function runPolling(opts) {
6688
6740
  const details = skippedProfileBoundaries.map(({ profile, error }) => `${profile.name}: ${error.message}`).join("; ");
6689
6741
  throw new Error(`No safe runtime profiles remain. ${details}`);
6690
6742
  }
6691
- const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
6692
- const runtimeSessionStore = createApiRuntimeSessionStore({
6693
- agent: ctx.agent,
6694
- logger: { warn: (context, message) => rootLogger.warn(context, message) }
6695
- });
6696
- const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
6697
- const runtimeInstanceId = createRuntimeInstanceId();
6698
6743
  const piAgentDir = await resolvePiAgentDir(cfg, profiles[0].mountPath, profiles);
6699
6744
  process.once("exit", piAgentDir.cleanup);
6700
6745
  activatePiCodingAgentDir(piAgentDir.path, piAgentDir.env);
6701
- const runtimes = /* @__PURE__ */ new Map();
6702
- for (const profile of profiles) {
6703
- const common = parseCommonOptions(values, { runtimeDefaults: {
6704
- leaseTtlSec: profile.leaseTtlSec,
6705
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
6706
- maxBatchSize: profile.maxBatchSize,
6707
- maxTurns: profile.maxTurns,
6708
- maxBashTimeouts: profile.maxBashTimeouts,
6709
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
6710
- } });
6711
- const sandbox = {
6712
- config: profile.sandboxConfig,
6713
- rootDir: profile.mountPath,
6714
- path: profile.source
6715
- };
6716
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
6717
- const slotIdentity = {
6718
- agentName: common.agent,
6719
- runtimeProfileId: profile.id,
6720
- runtimeInstanceId
6721
- };
6722
- const executionPlans = createExecutionPlanCache({
6723
- stateDirs,
6724
- slotIdentity,
6725
- warmSessionTtlSec: common.warmSessionTtlSec,
6726
- workspacePolicy: {
6727
- defaultWorkspaceMode: profile.defaultWorkspaceMode,
6728
- allowedWorkspaceModes: profile.allowedWorkspaceModes
6729
- },
6730
- slotRegistry,
6731
- runtimeSessionStore,
6732
- sourceAttemptResolver
6733
- });
6734
- runtimes.set(profile.id, {
6735
- common,
6736
- profile,
6737
- sandbox,
6738
- stateDirs,
6739
- slotIdentity,
6740
- executionPlans,
6741
- preparedRuntime: preparedRuntimes.get(profile.id)
6742
- });
6743
- }
6744
- const firstRuntime = runtimes.get(profiles[0].id);
6745
- if (!firstRuntime) throw new Error("No runtime profiles resolved");
6746
+ if (!runtimes.has(profiles[0].id)) throw new Error("No runtime profiles resolved");
6746
6747
  const otelShutdown = await initWorkerOtel({
6747
6748
  serviceName: opts.serviceName,
6748
6749
  agent: ctx.agent,
6749
6750
  endpoint: cfg.otelEndpoint,
6750
6751
  resourceAttributes: {
6751
6752
  "moltnet.team.id": teamId,
6752
- "moltnet.agent.name": baseCommon.agent,
6753
+ "moltnet.agent.name": identity.agent,
6753
6754
  "moltnet.credential.source": ctx.credentialSource,
6754
6755
  "moltnet.runtime_profile.count": String(profiles.length),
6755
6756
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
6756
6757
  }
6757
6758
  });
6758
- const { logger, shutdown: shutdownLogger } = createRootLogger({
6759
- name: `agent-daemon.${opts.modeLabel}`,
6760
- level: cfg.logLevel || (baseCommon.debug ? "debug" : "info")
6761
- });
6762
- const rootLogger = logger.child({
6763
- mode: opts.modeLabel,
6764
- agent: baseCommon.agent,
6765
- teamId,
6766
- runtimeProfileIds: profiles.map((p) => p.id),
6767
- runtimeProfileNames: profiles.map((p) => p.name)
6768
- });
6769
6759
  for (const { profile, error } of skippedProfileBoundaries) rootLogger.warn({
6770
6760
  runtimeProfileId: profile.id,
6771
6761
  runtimeProfileName: profile.name,
@@ -6821,13 +6811,10 @@ async function runPolling(opts) {
6821
6811
  topK: profile.topK,
6822
6812
  maxOutputTokens: profile.maxOutputTokens,
6823
6813
  sandbox: runtime.sandbox.path,
6824
- leaseTtlSec: runtime.common.leaseTtlSec,
6825
- heartbeatIntervalMs: runtime.common.heartbeatIntervalMs,
6826
- maxTurns: runtime.common.maxTurns,
6827
- maxBashTimeouts: runtime.common.maxBashTimeouts,
6828
- warmSessionTtlSec: runtime.common.warmSessionTtlSec,
6829
- profileSessionTtlSec: profile.sessionTtlSec,
6830
- profileWorkspaceTtlSec: profile.workspaceTtlSec,
6814
+ heartbeatIntervalMs: operations.heartbeatIntervalMs,
6815
+ maxTurns: profile.maxTurns,
6816
+ maxBashTimeouts: profile.maxBashTimeouts,
6817
+ warmRetentionSec: operations.warmRetentionSec,
6831
6818
  defaultWorkspaceMode: profile.defaultWorkspaceMode,
6832
6819
  allowedWorkspaceModes: profile.allowedWorkspaceModes
6833
6820
  };
@@ -6852,7 +6839,7 @@ async function runPolling(opts) {
6852
6839
  runtimeSlotStore: slotRegistry,
6853
6840
  taskReader: ctx.agent.tasks
6854
6841
  }, {
6855
- agentName: selected.common.agent,
6842
+ agentName: identity.agent,
6856
6843
  mainWorktree: resolveMainWorktree$1(selected.sandbox.rootDir),
6857
6844
  runtimeInstanceId,
6858
6845
  runtimeProfileId: profile.id,
@@ -6885,12 +6872,8 @@ async function runPolling(opts) {
6885
6872
  teamId,
6886
6873
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
6887
6874
  correlationId: values["correlation-id"],
6888
- profiles: profiles.map((profile) => ({
6889
- profileId: profile.id,
6890
- leaseTtlSec: requireRuntime(runtimes, profile.id).common.leaseTtlSec
6891
- })),
6875
+ profiles: profiles.map((profile) => ({ profileId: profile.id })),
6892
6876
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
6893
- leaseTtlSec: firstRuntime.common.leaseTtlSec,
6894
6877
  listLimit,
6895
6878
  pollIntervalMs,
6896
6879
  maxPollIntervalMs,
@@ -6898,7 +6881,7 @@ async function runPolling(opts) {
6898
6881
  stopWhenEmpty: opts.stopWhenEmpty,
6899
6882
  waitForFirstTaskMs: waitForFirstTaskSec * 1e3,
6900
6883
  waitAfterTaskMs: waitAfterTaskSec * 1e3,
6901
- debug: baseCommon.debug,
6884
+ debug: identity.debug,
6902
6885
  traceIdlePolling: cfg.traceIdlePolling,
6903
6886
  logger: rootLogger,
6904
6887
  executorFingerprints: Object.fromEntries(profiles.map((profile) => [profile.id, requireRuntime(runtimes, profile.id).preparedRuntime.attestor.fingerprint])),
@@ -6907,14 +6890,11 @@ async function runPolling(opts) {
6907
6890
  sourceAttemptResolver
6908
6891
  }),
6909
6892
  makeReporter: (claimedTask) => {
6910
- const selected = runtimeForClaimedTask(runtimes, claimedTask);
6911
6893
  return new ApiTaskReporter({
6912
6894
  tasks: ctx.agent.tasks,
6913
6895
  teamId: claimedTask.task.teamId,
6914
- leaseTtlSec: selected.common.leaseTtlSec,
6915
- heartbeatIntervalMs: selected.common.heartbeatIntervalMs,
6916
- maxBatchSize: selected.common.maxBatchSize,
6917
- flushIntervalMs: selected.common.flushIntervalMs
6896
+ heartbeatIntervalMs: operations.heartbeatIntervalMs,
6897
+ logger: rootLogger
6918
6898
  });
6919
6899
  },
6920
6900
  onTaskFinished: async (output, claimedTask) => {
@@ -6962,7 +6942,7 @@ async function runPolling(opts) {
6962
6942
  },
6963
6943
  executeTask: async (claimedTask, reporter) => {
6964
6944
  const selected = runtimeForClaimedTask(runtimes, claimedTask);
6965
- const { common, executionPlans, profile, sandbox, slotIdentity, stateDirs } = selected;
6945
+ const { executionPlans, profile, sandbox, slotIdentity, stateDirs } = selected;
6966
6946
  if (runtimeCredentialConfig) await observeGovernancePlanSafely({
6967
6947
  config: runtimeCredentialConfig,
6968
6948
  profile,
@@ -7076,10 +7056,11 @@ async function runPolling(opts) {
7076
7056
  worktreeBranch: executionPlan.worktreeBranch,
7077
7057
  workspaceKind: executionPlan.workspaceKind,
7078
7058
  lastTaskId: claimedTask.task.id,
7079
- lastAttemptN: claimedTask.attemptN
7059
+ lastAttemptN: claimedTask.attemptN,
7060
+ warmRetentionSec: operations.warmRetentionSec
7080
7061
  });
7081
7062
  const rawExecuteTask = selected.preparedRuntime.createTaskExecutor({
7082
- agentName: common.agent,
7063
+ agentName: identity.agent,
7083
7064
  moltnetAgent: ctx.agent,
7084
7065
  agentIdentity,
7085
7066
  hostCapabilitySigner,
@@ -7109,8 +7090,8 @@ async function runPolling(opts) {
7109
7090
  makeExecutionPlan: (task) => executionPlans.getOrCreate(task),
7110
7091
  makeOnTurnEvent: makeTurnEventHandlerFactory(taskLogger),
7111
7092
  toolPolicyLogger: taskLogger,
7112
- maxTurns: common.maxTurns,
7113
- maxBashTimeouts: common.maxBashTimeouts
7093
+ maxTurns: profile.maxTurns,
7094
+ maxBashTimeouts: profile.maxBashTimeouts
7114
7095
  });
7115
7096
  try {
7116
7097
  active = {
@@ -7131,7 +7112,7 @@ async function runPolling(opts) {
7131
7112
  } finally {
7132
7113
  active = null;
7133
7114
  executionPlans.delete(claimedTask);
7134
- if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
7115
+ if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null, operations.warmRetentionSec);
7135
7116
  }
7136
7117
  }
7137
7118
  });
@@ -7207,7 +7188,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7207
7188
  const { values } = parseArgs({
7208
7189
  args: argv,
7209
7190
  options: {
7210
- ...commonOptionDefs(),
7191
+ ...runtimeCommandOptionDefs(),
7211
7192
  "task-id": {
7212
7193
  type: "string",
7213
7194
  short: "t"
@@ -7228,9 +7209,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7228
7209
  console.error(ONCE_HELP);
7229
7210
  return 1;
7230
7211
  }
7231
- let opts;
7212
+ let commandOptions;
7232
7213
  try {
7233
- opts = parseCommonOptions(values);
7214
+ commandOptions = parseRuntimeCommandOptions(values);
7234
7215
  } catch (err) {
7235
7216
  if (err instanceof MissingRequiredOptionError) {
7236
7217
  console.error(`${err.message}\n`);
@@ -7239,6 +7220,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7239
7220
  }
7240
7221
  throw err;
7241
7222
  }
7223
+ const { identity, operations } = commandOptions;
7242
7224
  if (values.sandbox) {
7243
7225
  console.error("Cannot use --sandbox. Remote runtime profiles define sandbox policy.");
7244
7226
  return 1;
@@ -7249,12 +7231,11 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7249
7231
  bindings: cfg.credentialBindings
7250
7232
  };
7251
7233
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
7252
- const initialOpts = opts;
7253
7234
  const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
7254
7235
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
7255
7236
  let gate = "resolve_agent_context";
7256
7237
  try {
7257
- const resolvedContext = await resolveAgentContext(initialOpts.agent, {
7238
+ const resolvedContext = await resolveAgentContext(identity.agent, {
7258
7239
  agentRootDir: explicitAgentRootDir,
7259
7240
  credentialSource: cfg.credentialSource,
7260
7241
  envApiUrl: cfg.apiUrl
@@ -7281,7 +7262,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7281
7262
  });
7282
7263
  gate = "resolve_agent_identity";
7283
7264
  const agentIdentity = await resolveDaemonAgentIdentity({
7284
- agentName: initialOpts.agent,
7265
+ agentName: identity.agent,
7285
7266
  whoami,
7286
7267
  credentialSource: cfg.credentialSource,
7287
7268
  agentDir: resolvedContext.agentDir,
@@ -7300,9 +7281,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7300
7281
  } catch (error) {
7301
7282
  await logDaemonStartupFailure({
7302
7283
  serviceName: "agent-daemon.once",
7303
- level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
7284
+ level: cfg.logLevel || (identity.debug ? "debug" : "info"),
7304
7285
  gate,
7305
- agent: initialOpts.agent,
7286
+ agent: identity.agent,
7306
7287
  credentialSource: cfg.credentialSource,
7307
7288
  error
7308
7289
  });
@@ -7316,34 +7297,23 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7316
7297
  teamId: values.team,
7317
7298
  cwd: daemonRootDir
7318
7299
  });
7319
- assertRuntimeAdapterSupportsProfile(runtimeAdapter, profile);
7320
- const preparedRuntime = attestPreparedRuntime(await runtimeAdapter.prepare({ profile }), signingPrivateKey);
7321
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, {
7322
- tools: preparedRuntime.tools,
7323
- executables: preparedRuntime.executables
7300
+ const { logger, shutdown: shutdownLogger } = createRootLogger({
7301
+ name: "agent-daemon.once",
7302
+ level: cfg.logLevel || (identity.debug ? "debug" : "info")
7324
7303
  });
7325
- assertGuestEnvironmentBoundary({
7326
- forwardEnv: profile.requiredEnv,
7327
- sandboxEnv: profile.sandboxConfig.env
7304
+ const rootLogger = logger.child({
7305
+ mode: "once",
7306
+ agent: identity.agent,
7307
+ provider: profile.provider,
7308
+ model: profile.model,
7309
+ thinkingLevel: profile.thinkingLevel,
7310
+ temperature: profile.temperature,
7311
+ topP: profile.topP,
7312
+ topK: profile.topK,
7313
+ maxOutputTokens: profile.maxOutputTokens,
7314
+ runtimeProfileId: profile.id,
7315
+ runtimeProfileName: profile.name
7328
7316
  });
7329
- await ctx.agent.tasks.registerExecutorManifest(await preparedRuntime.attestor.registration());
7330
- opts = parseCommonOptions(values, { runtimeDefaults: {
7331
- leaseTtlSec: profile.leaseTtlSec,
7332
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
7333
- maxBatchSize: profile.maxBatchSize,
7334
- maxTurns: profile.maxTurns,
7335
- maxBashTimeouts: profile.maxBashTimeouts,
7336
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
7337
- } });
7338
- const sandbox = {
7339
- config: profile.sandboxConfig,
7340
- rootDir: profile.mountPath,
7341
- path: profile.source
7342
- };
7343
- const piAgentDir = await resolvePiAgentDir(cfg, sandbox.rootDir, [profile]);
7344
- process.once("exit", piAgentDir.cleanup);
7345
- activatePiCodingAgentDir(piAgentDir.path, piAgentDir.env);
7346
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
7347
7317
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
7348
7318
  const runtimeSessionStore = createApiRuntimeSessionStore({
7349
7319
  agent: ctx.agent,
@@ -7351,30 +7321,29 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7351
7321
  });
7352
7322
  const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
7353
7323
  const runtimeInstanceId = createRuntimeInstanceId();
7354
- const slotIdentity = {
7355
- agentName: opts.agent,
7356
- runtimeProfileId: profile.id,
7357
- runtimeInstanceId
7358
- };
7359
- const executionPlans = createExecutionPlanCache({
7360
- stateDirs,
7361
- slotIdentity,
7362
- warmSessionTtlSec: opts.warmSessionTtlSec,
7363
- workspacePolicy: {
7364
- defaultWorkspaceMode: profile.defaultWorkspaceMode,
7365
- allowedWorkspaceModes: profile.allowedWorkspaceModes
7366
- },
7324
+ const { executionPlans, preparedRuntime, sandbox, slotIdentity, stateDirs } = await prepareRuntimeProfile({
7325
+ agent: ctx.agent,
7326
+ agentName: identity.agent,
7327
+ profile,
7328
+ prerequisiteEnv: cfg.profilePrerequisiteEnv,
7329
+ runtimeAdapter,
7330
+ runtimeInstanceId,
7331
+ signingPrivateKey,
7367
7332
  slotRegistry,
7368
7333
  runtimeSessionStore,
7369
- sourceAttemptResolver
7334
+ sourceAttemptResolver,
7335
+ warmRetentionSec: operations.warmRetentionSec
7370
7336
  });
7337
+ const piAgentDir = await resolvePiAgentDir(cfg, sandbox.rootDir, [profile]);
7338
+ process.once("exit", piAgentDir.cleanup);
7339
+ activatePiCodingAgentDir(piAgentDir.path, piAgentDir.env);
7371
7340
  const otelShutdown = await initWorkerOtel({
7372
7341
  serviceName: "moltnet.agent-daemon.once",
7373
7342
  agent: ctx.agent,
7374
7343
  endpoint: cfg.otelEndpoint,
7375
7344
  resourceAttributes: {
7376
7345
  "moltnet.task.id": taskId,
7377
- "moltnet.agent.name": opts.agent,
7346
+ "moltnet.agent.name": identity.agent,
7378
7347
  "moltnet.credential.source": ctx.credentialSource,
7379
7348
  "moltnet.llm.provider": profile.provider,
7380
7349
  "moltnet.llm.model": profile.model,
@@ -7386,34 +7355,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7386
7355
  "moltnet.runtime_profile.id": profile.id
7387
7356
  }
7388
7357
  });
7389
- const { logger, shutdown: shutdownLogger } = createRootLogger({
7390
- name: "agent-daemon.once",
7391
- level: cfg.logLevel || (opts.debug ? "debug" : "info")
7392
- });
7393
- const rootLogger = logger.child({
7394
- mode: "once",
7395
- agent: opts.agent,
7396
- provider: profile.provider,
7397
- model: profile.model,
7398
- thinkingLevel: profile.thinkingLevel,
7399
- temperature: profile.temperature,
7400
- topP: profile.topP,
7401
- topK: profile.topK,
7402
- maxOutputTokens: profile.maxOutputTokens,
7403
- runtimeProfileId: profile.id,
7404
- runtimeProfileName: profile.name
7405
- });
7406
7358
  rootLogger.info({
7407
7359
  sandbox: sandbox.path,
7408
7360
  taskId,
7409
- leaseTtlSec: opts.leaseTtlSec,
7410
- heartbeatIntervalMs: opts.heartbeatIntervalMs,
7411
- maxTurns: opts.maxTurns,
7412
- maxBashTimeouts: opts.maxBashTimeouts,
7413
- warmSessionTtlSec: opts.warmSessionTtlSec,
7361
+ heartbeatIntervalMs: operations.heartbeatIntervalMs,
7362
+ maxTurns: profile.maxTurns,
7363
+ maxBashTimeouts: profile.maxBashTimeouts,
7364
+ warmRetentionSec: operations.warmRetentionSec,
7414
7365
  profileId: profile.id,
7415
- profileSessionTtlSec: profile.sessionTtlSec,
7416
- profileWorkspaceTtlSec: profile.workspaceTtlSec,
7417
7366
  piAgentDir: piAgentDir.path,
7418
7367
  piAgentDirSource: piAgentDir.source
7419
7368
  }, "agent-daemon.starting");
@@ -7425,7 +7374,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7425
7374
  runtimeSlotStore: slotRegistry,
7426
7375
  taskReader: ctx.agent.tasks
7427
7376
  }, {
7428
- agentName: opts.agent,
7377
+ agentName: identity.agent,
7429
7378
  mainWorktree: resolveMainWorktree(sandbox.rootDir),
7430
7379
  runtimeInstanceId,
7431
7380
  runtimeProfileId: profile.id,
@@ -7471,7 +7420,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7471
7420
  });
7472
7421
  try {
7473
7422
  const rawExecuteTask = preparedRuntime.createTaskExecutor({
7474
- agentName: opts.agent,
7423
+ agentName: identity.agent,
7475
7424
  moltnetAgent: ctx.agent,
7476
7425
  agentIdentity,
7477
7426
  hostCapabilitySigner,
@@ -7501,8 +7450,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7501
7450
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
7502
7451
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
7503
7452
  toolPolicyLogger: rootLogger,
7504
- maxTurns: opts.maxTurns,
7505
- maxBashTimeouts: opts.maxBashTimeouts
7453
+ maxTurns: profile.maxTurns,
7454
+ maxBashTimeouts: profile.maxBashTimeouts
7506
7455
  });
7507
7456
  const executeTask = async (claimedTask, reporter) => {
7508
7457
  if (runtimeCredentialConfig) await observeGovernancePlanSafely({
@@ -7559,7 +7508,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7559
7508
  worktreeBranch: executionPlan.worktreeBranch,
7560
7509
  workspaceKind: executionPlan.workspaceKind,
7561
7510
  lastTaskId: claimedTask.task.id,
7562
- lastAttemptN: claimedTask.attemptN
7511
+ lastAttemptN: claimedTask.attemptN,
7512
+ warmRetentionSec: operations.warmRetentionSec
7563
7513
  });
7564
7514
  activeAttemptN = claimedTask.attemptN;
7565
7515
  try {
@@ -7577,7 +7527,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7577
7527
  } finally {
7578
7528
  activeAttemptN = null;
7579
7529
  executionPlans.delete(claimedTask);
7580
- if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
7530
+ if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null, operations.warmRetentionSec);
7581
7531
  }
7582
7532
  };
7583
7533
  const writeCorrelationAnchors = makePrBodyAnchorWriter({
@@ -7590,17 +7540,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7590
7540
  agent: ctx.agent,
7591
7541
  taskId,
7592
7542
  teamId: profile.teamId,
7593
- leaseTtlSec: opts.leaseTtlSec,
7594
7543
  profileId: profile.id,
7595
7544
  executorFingerprint: preparedRuntime.attestor.fingerprint
7596
7545
  }),
7597
7546
  makeReporter: () => new ApiTaskReporter({
7598
7547
  tasks: ctx.agent.tasks,
7599
7548
  teamId: profile.teamId,
7600
- leaseTtlSec: opts.leaseTtlSec,
7601
- heartbeatIntervalMs: opts.heartbeatIntervalMs,
7602
- maxBatchSize: opts.maxBatchSize,
7603
- flushIntervalMs: opts.flushIntervalMs
7549
+ heartbeatIntervalMs: operations.heartbeatIntervalMs,
7550
+ logger: rootLogger
7604
7551
  }),
7605
7552
  onTaskFinished: async (output, claimedTask) => {
7606
7553
  const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
@@ -7867,14 +7814,41 @@ var AgentServerModelDiscoveryError = class extends Error {
7867
7814
  this.statusCode = statusCode;
7868
7815
  }
7869
7816
  };
7817
+ /**
7818
+ * The Ollama capability that means a model accepts image input. Ollama also
7819
+ * reports `completion`, `tools`, `thinking` and `embedding`; none of those map
7820
+ * onto a Pi input modality, so only this one is read.
7821
+ */
7822
+ var OLLAMA_VISION_CAPABILITY = "vision";
7823
+ /**
7824
+ * Read Ollama's `capabilities` array, when the endpoint supplies one.
7825
+ *
7826
+ * Returns `undefined` when the field is absent — which is meaningfully
7827
+ * different from "present and without vision". A local Ollama returns
7828
+ * capabilities from `/api/tags`; Ollama Cloud does not, and needs a per-model
7829
+ * `/api/show` probe. Only an explicit absence should trigger that probe.
7830
+ */
7831
+ function readOllamaModalities(value) {
7832
+ if (!isRecord(value) || !Array.isArray(value["capabilities"])) return void 0;
7833
+ return value["capabilities"].includes(OLLAMA_VISION_CAPABILITY) ? ["text", "image"] : [];
7834
+ }
7870
7835
  var ModelDiscoveryCollector = class {
7871
- models = /* @__PURE__ */ new Set();
7836
+ /**
7837
+ * Model id → declared input modalities. `undefined` means the id is known but
7838
+ * its capabilities are not, so it is still a probe candidate; `[]` means the
7839
+ * source answered and the model is text-only.
7840
+ */
7841
+ models = /* @__PURE__ */ new Map();
7842
+ record(id, input) {
7843
+ if (input === void 0 && this.models.has(id)) return;
7844
+ this.models.set(id, input);
7845
+ }
7872
7846
  addOpenAiResponse(value) {
7873
7847
  if (!isRecord(value) || !Array.isArray(value["data"])) return;
7874
7848
  for (const candidate of value["data"]) {
7875
7849
  if (!isRecord(candidate)) continue;
7876
7850
  const id = candidate["id"];
7877
- if (typeof id === "string" && id.length > 0) this.models.add(id);
7851
+ if (typeof id === "string" && id.length > 0) this.record(id, void 0);
7878
7852
  }
7879
7853
  }
7880
7854
  addOllamaResponse(value) {
@@ -7882,16 +7856,28 @@ var ModelDiscoveryCollector = class {
7882
7856
  for (const candidate of value["models"]) {
7883
7857
  if (!isRecord(candidate)) continue;
7884
7858
  const name = candidate["name"];
7885
- if (typeof name === "string" && name.length > 0) this.models.add(name);
7859
+ if (typeof name === "string" && name.length > 0) this.record(name, readOllamaModalities(candidate));
7886
7860
  }
7887
7861
  }
7862
+ /** Attach modalities learned after collection, e.g. from an `/api/show` probe. */
7863
+ setModalities(id, input) {
7864
+ if (this.models.has(id)) this.models.set(id, input);
7865
+ }
7888
7866
  get size() {
7889
7867
  return this.models.size;
7890
7868
  }
7891
7869
  result(providerId, failures) {
7892
7870
  if (this.models.size === 0) throw discoveryFailure(providerId, failures);
7871
+ const ids = [...this.models.keys()].sort().slice(0, 500);
7893
7872
  return {
7894
- models: [...this.models].sort().slice(0, 500),
7873
+ models: ids.map((id) => {
7874
+ const input = this.models.get(id);
7875
+ return input && input.length > 0 ? {
7876
+ id,
7877
+ input: [...input]
7878
+ } : { id };
7879
+ }),
7880
+ unresolved: ids.filter((id) => this.models.get(id) === void 0),
7895
7881
  discoveredCount: this.models.size
7896
7882
  };
7897
7883
  }
@@ -7952,6 +7938,12 @@ function safeErrorToken(value) {
7952
7938
  //#endregion
7953
7939
  //#region src/lib/provider-configuration.ts
7954
7940
  var DEFAULT_PROVIDER_API = "openai-completions";
7941
+ /**
7942
+ * Concurrent `/api/show` probes. Small on purpose: this runs against a
7943
+ * third-party endpoint on an operator's behalf, and discovery is interactive,
7944
+ * so the cap favours being a polite client over shaving a second.
7945
+ */
7946
+ var MODALITY_PROBE_CONCURRENCY = 5;
7955
7947
  var ProviderConfigurationError = class extends Error {
7956
7948
  name = "ProviderConfigurationError";
7957
7949
  constructor(code, message, statusCode, options) {
@@ -7989,7 +7981,7 @@ var ProviderConfigurationService = class {
7989
7981
  api: input.api ?? previous?.api ?? DEFAULT_PROVIDER_API,
7990
7982
  baseUrl,
7991
7983
  envName: assertProviderEnvName(providerId, input.envName ?? previous?.envName ?? providerEnvName(providerId)),
7992
- models: [...input.models ?? previous?.models ?? []],
7984
+ models: (input.models ?? previous?.models ?? []).map(copyProviderModel),
7993
7985
  ...!input.clearApiKey && previous?.apiKeyRef ? { apiKeyRef: previous.apiKeyRef } : {}
7994
7986
  };
7995
7987
  const key = `pi-provider/${providerId}`;
@@ -8090,13 +8082,65 @@ var ProviderConfigurationService = class {
8090
8082
  providerId,
8091
8083
  returnedCount: 500
8092
8084
  }, "Provider model discovery result was truncated");
8093
- if (options.save) await this.set(providerId, { models: result.models }, options);
8085
+ if (isOllamaProvider(providerId, parsed) && result.unresolved.length > 0) await this.resolveOllamaModalities({
8086
+ collector,
8087
+ headers,
8088
+ ids: result.unresolved,
8089
+ origin: parsed.origin,
8090
+ providerId,
8091
+ signal: options.signal
8092
+ });
8093
+ const resolved = collector.result(providerId, failures);
8094
+ const declared = new Map(provider.models.map((model) => [model.id, model.input]));
8095
+ const models = resolved.models.map((model) => {
8096
+ const override = declared.get(model.id);
8097
+ return override && override.length > 0 ? {
8098
+ id: model.id,
8099
+ input: [...override]
8100
+ } : model;
8101
+ });
8102
+ const detected = models.filter((model) => model.input?.includes("image") && !declared.get(model.id)?.includes("image"));
8103
+ if (detected.length > 0) this.logger.info({
8104
+ code: "agent_server_provider_discovery_modalities_detected",
8105
+ models: detected.map((model) => model.id),
8106
+ providerId
8107
+ }, "Provider models reported image input support");
8108
+ if (options.save) await this.set(providerId, { models }, options);
8094
8109
  this.logger.info({
8095
8110
  code: "agent_server_provider_discovery_completed",
8096
- modelCount: result.models.length,
8111
+ modelCount: models.length,
8097
8112
  providerId
8098
8113
  }, "Provider model discovery completed");
8099
- return { models: result.models };
8114
+ return { models };
8115
+ }
8116
+ /**
8117
+ * Fill in modalities Ollama Cloud's `/api/tags` omits, one `/api/show` per
8118
+ * still-unknown model.
8119
+ *
8120
+ * Failures here are deliberately not pushed into the discovery `failures`
8121
+ * array: that array decides the error code of a *failed* discovery, so a
8122
+ * probe rejection must not relabel an otherwise-successful one. A model whose
8123
+ * probe fails simply stays text-only.
8124
+ */
8125
+ async resolveOllamaModalities(input) {
8126
+ const url = `${input.origin}/api/show`;
8127
+ for (let index = 0; index < input.ids.length; index += MODALITY_PROBE_CONCURRENCY) {
8128
+ if (input.signal?.aborted) throw new ProviderConfigurationError("operation_aborted", `provider "${input.providerId}" discovery was cancelled`, 408, { cause: input.signal.reason });
8129
+ const batch = input.ids.slice(index, index + MODALITY_PROBE_CONCURRENCY);
8130
+ await Promise.all(batch.map(async (id) => {
8131
+ const modalities = readOllamaModalities(await this.requestDiscoveryEndpoint({
8132
+ body: { model: id },
8133
+ endpoint: "ollama_show",
8134
+ failures: [],
8135
+ headers: input.headers,
8136
+ method: "POST",
8137
+ providerId: input.providerId,
8138
+ signal: input.signal,
8139
+ url
8140
+ }));
8141
+ if (modalities) input.collector.setModalities(id, modalities);
8142
+ }));
8143
+ }
8100
8144
  }
8101
8145
  async resolveApiKey(providerId, provider) {
8102
8146
  if (!provider.apiKeyRef) return void 0;
@@ -8117,7 +8161,14 @@ var ProviderConfigurationService = class {
8117
8161
  let response;
8118
8162
  try {
8119
8163
  response = await this.fetchImpl(input.url, {
8120
- headers: input.headers,
8164
+ ...input.body ? {
8165
+ body: JSON.stringify(input.body),
8166
+ method: input.method
8167
+ } : {},
8168
+ headers: input.body ? {
8169
+ ...input.headers,
8170
+ "content-type": "application/json"
8171
+ } : input.headers,
8121
8172
  redirect: "error",
8122
8173
  signal: input.signal ? AbortSignal.any([input.signal, timeout]) : timeout
8123
8174
  });
@@ -8184,7 +8235,7 @@ function providerView(provider) {
8184
8235
  api: provider.api,
8185
8236
  baseUrl: provider.baseUrl,
8186
8237
  envName: provider.envName,
8187
- models: [...provider.models],
8238
+ models: provider.models.map(copyProviderModel),
8188
8239
  hasApiKey: Boolean(provider.apiKeyRef)
8189
8240
  };
8190
8241
  }
@@ -8206,6 +8257,31 @@ function providerAbortSource(reason) {
8206
8257
  }
8207
8258
  //#endregion
8208
8259
  //#region src/cli/providers.ts
8260
+ var MODEL_MODALITIES$1 = PI_MODEL_MODALITIES;
8261
+ /**
8262
+ * Build the model list from `--model <id>` (text-only) and
8263
+ * `--model-input <id>=text,image` (declares modalities). Model ids contain
8264
+ * colons, so `=` separates the id from its modality list. A `--model-input`
8265
+ * entry also declares the model, and overrides a bare `--model` for that id.
8266
+ */
8267
+ function parseModelArgs(models, modelInputs) {
8268
+ if (!models && !modelInputs) return void 0;
8269
+ const entries = /* @__PURE__ */ new Map();
8270
+ for (const id of models ?? []) entries.set(id, { id });
8271
+ for (const raw of modelInputs ?? []) {
8272
+ const separator = raw.indexOf("=");
8273
+ if (separator <= 0) throw new ProviderCliError("invalid_arguments", `--model-input expects <model-id>=<modality>[,<modality>], received "${raw}"`);
8274
+ const id = raw.slice(0, separator);
8275
+ const input = raw.slice(separator + 1).split(",").map((value) => value.trim()).filter((value) => value.length > 0);
8276
+ if (input.length === 0) throw new ProviderCliError("invalid_arguments", `--model-input for "${id}" declared no modality`);
8277
+ for (const modality of input) if (!MODEL_MODALITIES$1.includes(modality)) throw new ProviderCliError("invalid_arguments", `--model-input for "${id}" has unknown modality "${modality}"; expected ${MODEL_MODALITIES$1.join(" or ")}`);
8278
+ entries.set(id, {
8279
+ id,
8280
+ input
8281
+ });
8282
+ }
8283
+ return [...entries.values()];
8284
+ }
8209
8285
  async function runProviders(argv, dependencies = {}) {
8210
8286
  if (isHelpFlag(argv) || argv.length === 0) {
8211
8287
  (dependencies.stdout ?? console.log)(PROVIDERS_HELP);
@@ -8279,6 +8355,10 @@ function parseProviderArgs(command, args) {
8279
8355
  type: "string",
8280
8356
  multiple: true
8281
8357
  },
8358
+ "model-input": {
8359
+ type: "string",
8360
+ multiple: true
8361
+ },
8282
8362
  "clear-models": { type: "boolean" },
8283
8363
  "api-key-stdin": { type: "boolean" },
8284
8364
  "clear-api-key": { type: "boolean" }
@@ -8288,6 +8368,7 @@ function parseProviderArgs(command, args) {
8288
8368
  });
8289
8369
  requirePositionals(positionals, 1, "providers set <id>");
8290
8370
  if (values.model && values["clear-models"]) throw new ProviderCliError("invalid_arguments", "--model and --clear-models cannot be used together");
8371
+ if (values["model-input"] && values["clear-models"]) throw new ProviderCliError("invalid_arguments", "--model-input and --clear-models cannot be used together");
8291
8372
  if (values["api-key-stdin"] && values["clear-api-key"]) throw new ProviderCliError("invalid_arguments", "--api-key-stdin and --clear-api-key cannot be used together");
8292
8373
  return {
8293
8374
  command,
@@ -8295,7 +8376,7 @@ function parseProviderArgs(command, args) {
8295
8376
  providerId: positionals[0],
8296
8377
  baseUrl: values["base-url"],
8297
8378
  api: values.api,
8298
- models: values["clear-models"] ? [] : values.model,
8379
+ models: values["clear-models"] ? [] : parseModelArgs(values.model, values["model-input"]),
8299
8380
  apiKeyStdin: values["api-key-stdin"] ?? false,
8300
8381
  clearApiKey: values["clear-api-key"] ?? false
8301
8382
  };
@@ -8450,7 +8531,7 @@ async function discoverProvider(context, parsed) {
8450
8531
  signal: context.signal
8451
8532
  });
8452
8533
  if (parsed.json) context.stdout(JSON.stringify(result));
8453
- else for (const model of result.models) context.stdout(model);
8534
+ else for (const model of result.models) context.stdout(model.input && model.input.length > 0 ? `${model.id}\t${model.input.join(",")}` : model.id);
8454
8535
  return 0;
8455
8536
  }
8456
8537
  async function removeProvider(context, parsed) {
@@ -9842,6 +9923,7 @@ var RunManager = class {
9842
9923
  extraArgs: ["--agent-root", agentRoot]
9843
9924
  };
9844
9925
  })();
9926
+ const runtimeSettings = this.options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS;
9845
9927
  const args = [
9846
9928
  ...runtimeModule ? ["--runtime", runtimeModule] : [],
9847
9929
  spec.mode,
@@ -9852,6 +9934,10 @@ var RunManager = class {
9852
9934
  ...spec.profiles.flatMap((profile) => ["--profile", profile]),
9853
9935
  "--task-types",
9854
9936
  spec.taskTypes.join(","),
9937
+ "--heartbeat-interval-ms",
9938
+ String(runtimeSettings.heartbeatIntervalMs),
9939
+ "--warm-retention-sec",
9940
+ String(runtimeSettings.warmRetentionSec),
9855
9941
  ...target.extraArgs
9856
9942
  ];
9857
9943
  if (activation.source === "managed") {
@@ -10442,6 +10528,13 @@ function writeRegistry(path, entries) {
10442
10528
  //#region src/lib/agent-server/protocol.ts
10443
10529
  var DateTime = Type.String({ format: "date-time" });
10444
10530
  var StringList = Type.Array(Type.String());
10531
+ /** A model the provider offers, with the input modalities it accepts. */
10532
+ var ProviderModelSchema = Type.Object({
10533
+ id: Type.String(),
10534
+ input: Type.Optional(Type.Array(Type.Union(PI_MODEL_MODALITIES.map((modality) => Type.Literal(modality))), { minItems: 1 }))
10535
+ });
10536
+ /** One shape on the wire, for both requests and responses. */
10537
+ var ProviderModelList = Type.Array(ProviderModelSchema);
10445
10538
  function schemaRef(schema) {
10446
10539
  const id = schema.$id;
10447
10540
  if (typeof id !== "string" || id.length === 0) throw new Error("Agent Server protocol schemas must have an identifier");
@@ -10474,7 +10567,7 @@ var AgentServerProviderSchema = Type.Object({
10474
10567
  api: Type.String(),
10475
10568
  baseUrl: Type.String({ format: "uri" }),
10476
10569
  envName: Type.String(),
10477
- models: StringList,
10570
+ models: ProviderModelList,
10478
10571
  hasApiKey: Type.Boolean()
10479
10572
  }, { $id: "AgentServerProvider" });
10480
10573
  var AgentServerRunRecordSchema = Type.Object({
@@ -10522,7 +10615,11 @@ var AgentServerStatusSchema = Type.Object({
10522
10615
  identities: Type.Array(schemaRef(AgentServerIdentitySchema)),
10523
10616
  selectedIdentity: Type.Optional(Type.String()),
10524
10617
  providers: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
10525
- runs: Type.Array(schemaRef(AgentServerRunSchema))
10618
+ runs: Type.Array(schemaRef(AgentServerRunSchema)),
10619
+ runtimeSettings: Type.Object({
10620
+ heartbeatIntervalMs: Type.Integer({ minimum: 0 }),
10621
+ warmRetentionSec: Type.Integer({ minimum: 0 })
10622
+ })
10526
10623
  }, { $id: "AgentServerStatus" });
10527
10624
  var PairingStartedSchema = Type.Object({
10528
10625
  pairingId: Type.String(),
@@ -10547,10 +10644,10 @@ var PutProviderSchema = Type.Object({
10547
10644
  api: Type.String(),
10548
10645
  baseUrl: Type.String({ format: "uri" }),
10549
10646
  envName: Type.String(),
10550
- models: StringList,
10647
+ models: ProviderModelList,
10551
10648
  apiKey: Type.Optional(Type.String())
10552
10649
  });
10553
- var DiscoverModelsSchema = Type.Object({ models: StringList }, { $id: "DiscoveredModels" });
10650
+ var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
10554
10651
  var StartRunSchema = Type.Object({
10555
10652
  agent: Type.String(),
10556
10653
  teamId: Type.String(),
@@ -10865,6 +10962,27 @@ function stringArray(body, field, options = {}) {
10865
10962
  if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new AgentServerHttpError(400, "invalid_body", `"${field}" must be ${options.allowEmpty ? "a" : "a non-empty"} string array`);
10866
10963
  return value;
10867
10964
  }
10965
+ var MODEL_MODALITIES = new Set(PI_MODEL_MODALITIES);
10966
+ /** Parse the provider `models` field: `{ id, input? }` entries only. */
10967
+ function modelArray(body, field) {
10968
+ const value = body[field];
10969
+ const invalid = (detail) => {
10970
+ throw new AgentServerHttpError(400, "invalid_body", detail);
10971
+ };
10972
+ if (!Array.isArray(value)) return invalid(`"${field}" must be an array of { id, input? } entries`);
10973
+ return value.map((item) => {
10974
+ if (typeof item !== "object" || item === null || Array.isArray(item)) return invalid(`"${field}" entries must be an { id, input? } object`);
10975
+ const entry = item;
10976
+ const id = entry.id;
10977
+ if (typeof id !== "string" || id.length === 0) return invalid(`"${field}" entries must carry a non-empty "id"`);
10978
+ if (entry.input === void 0) return { id };
10979
+ if (!Array.isArray(entry.input) || entry.input.length === 0 || entry.input.some((modality) => typeof modality !== "string" || !MODEL_MODALITIES.has(modality))) return invalid(`"${field}" entry "${id}" must declare "input" as a non-empty array of "text" or "image"`);
10980
+ return {
10981
+ id,
10982
+ input: entry.input
10983
+ };
10984
+ });
10985
+ }
10868
10986
  function requestOperationSignal(request, shutdownSignal) {
10869
10987
  const disconnected = new AbortController();
10870
10988
  if (request.raw.aborted) disconnected.abort({ source: "request" });
@@ -10996,7 +11114,8 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
10996
11114
  identities: identityViews(store),
10997
11115
  ...selected ? { selectedIdentity: selected } : {},
10998
11116
  providers: options.providers.list(),
10999
- runs: runViews(runs)
11117
+ runs: runViews(runs),
11118
+ runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
11000
11119
  };
11001
11120
  });
11002
11121
  }
@@ -11084,7 +11203,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11084
11203
  api: requireString(body, "api"),
11085
11204
  baseUrl: requireString(body, "baseUrl"),
11086
11205
  envName: requireString(body, "envName"),
11087
- models: stringArray(body, "models", { allowEmpty: true }),
11206
+ models: modelArray(body, "models"),
11088
11207
  ...optionalString(body, "apiKey") ? { apiKey: optionalString(body, "apiKey") } : {}
11089
11208
  });
11090
11209
  return reply.code(200).send(entry);
@@ -11489,6 +11608,7 @@ async function runAgentServer(argv) {
11489
11608
  const trustRequested = argv[0] === "trust";
11490
11609
  const commandArgs = trustRequested ? argv.slice(1) : argv;
11491
11610
  const envConfig = loadAgentServerEnvConfig();
11611
+ if (trustRequested) return runTrustCommand(commandArgs, resolveAgentServerRoot({ root: envConfig.root }));
11492
11612
  const { values } = parseArgs({
11493
11613
  args: commandArgs,
11494
11614
  options: {
@@ -11496,7 +11616,9 @@ async function runAgentServer(argv) {
11496
11616
  "allowed-origins": { type: "string" },
11497
11617
  root: { type: "string" },
11498
11618
  "api-url": { type: "string" },
11499
- remove: { type: "boolean" }
11619
+ "heartbeat-interval-ms": { type: "string" },
11620
+ "warm-retention-sec": { type: "string" },
11621
+ supervised: { type: "boolean" }
11500
11622
  }
11501
11623
  });
11502
11624
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
@@ -11507,8 +11629,8 @@ async function runAgentServer(argv) {
11507
11629
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
11508
11630
  const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
11509
11631
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
11632
+ const runtimeSettings = parseLocalOperationalSettings(values);
11510
11633
  const store = new AgentServerStore(root).ensure();
11511
- if (trustRequested) return runTrustCommand(commandArgs, root);
11512
11634
  const { logger, shutdown: shutdownLogger } = createRootLogger({
11513
11635
  name: "agent-daemon.server",
11514
11636
  level: envConfig.logLevel || "info"
@@ -11540,7 +11662,8 @@ async function runAgentServer(argv) {
11540
11662
  externalSecretProviders,
11541
11663
  baseEnv: processEnvSnapshot(),
11542
11664
  logger,
11543
- runtimeRegistry: new RuntimeRegistry(store.root)
11665
+ runtimeRegistry: new RuntimeRegistry(store.root),
11666
+ runtimeSettings
11544
11667
  });
11545
11668
  const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
11546
11669
  const app = buildAgentServer({
@@ -11559,6 +11682,7 @@ async function runAgentServer(argv) {
11559
11682
  cert: tls.cert
11560
11683
  } } : {},
11561
11684
  defaultApiUrl,
11685
+ runtimeSettings,
11562
11686
  ...envConfig.activeIdentity ? { activeIdentity: envConfig.activeIdentity } : {},
11563
11687
  version: "dev",
11564
11688
  logger,
@@ -11573,7 +11697,7 @@ async function runAgentServer(argv) {
11573
11697
  console.error(`config root: ${root}`);
11574
11698
  console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
11575
11699
  console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
11576
- return await waitForAgentServerShutdown(runs, app, shutdownController);
11700
+ return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
11577
11701
  } catch (cause) {
11578
11702
  await app.close().catch(() => void 0);
11579
11703
  throw cause;
@@ -11594,19 +11718,80 @@ async function runAgentServer(argv) {
11594
11718
  await shutdownLogger();
11595
11719
  }
11596
11720
  }
11597
- async function runTrustCommand(argv, root) {
11598
- if (!isMacos()) {
11599
- console.error("Local HTTPS trust setup is currently supported on macOS only.");
11600
- return 1;
11601
- }
11602
- if (argv.includes("--remove")) {
11603
- await removeLocalCa(root);
11604
- console.error("Removed the MoltNet local CA from your login keychain.");
11721
+ async function runTrustCommand(argv, defaultRoot) {
11722
+ try {
11723
+ const { values } = parseArgs({
11724
+ args: argv,
11725
+ options: {
11726
+ root: { type: "string" },
11727
+ remove: { type: "boolean" },
11728
+ status: { type: "boolean" },
11729
+ yes: { type: "boolean" },
11730
+ json: { type: "boolean" }
11731
+ }
11732
+ });
11733
+ const root = values.root ?? defaultRoot;
11734
+ const statusRequested = Boolean(values.status);
11735
+ const removeRequested = Boolean(values.remove);
11736
+ const yes = Boolean(values.yes);
11737
+ const json = Boolean(values.json);
11738
+ if (statusRequested && (removeRequested || yes)) {
11739
+ console.error("Usage: moltnet-agent server trust --status [--json]");
11740
+ return 1;
11741
+ }
11742
+ if (!isMacos()) {
11743
+ if (json) {
11744
+ printTrustStatus({
11745
+ supported: false,
11746
+ trusted: false,
11747
+ fingerprint: null
11748
+ });
11749
+ return 0;
11750
+ }
11751
+ console.error("Local HTTPS trust setup is currently supported on macOS only.");
11752
+ return 1;
11753
+ }
11754
+ const material = await ensureLocalTlsMaterial(root);
11755
+ if (statusRequested) {
11756
+ const trusted = await isLocalCaTrusted(root);
11757
+ if (json) printTrustStatus({
11758
+ supported: true,
11759
+ trusted,
11760
+ fingerprint: material.fingerprint
11761
+ });
11762
+ else console.log(trusted ? `MoltNet local CA ${material.fingerprint} is trusted.` : `MoltNet local CA ${material.fingerprint} is not trusted.`);
11763
+ return 0;
11764
+ }
11765
+ if (json && !yes) {
11766
+ console.error("Machine-readable trust changes require --yes after native app consent.");
11767
+ return 1;
11768
+ }
11769
+ if (removeRequested) {
11770
+ await removeLocalCa(root);
11771
+ if (json) printTrustStatus({
11772
+ supported: true,
11773
+ trusted: false,
11774
+ fingerprint: material.fingerprint
11775
+ });
11776
+ else console.log("Removed the MoltNet local CA from your login keychain.");
11777
+ return 0;
11778
+ }
11779
+ if (yes) await trustLocalCa(root);
11780
+ else await ensureTrustedLocalTls(root);
11781
+ if (json) printTrustStatus({
11782
+ supported: true,
11783
+ trusted: await isLocalCaTrusted(root),
11784
+ fingerprint: material.fingerprint
11785
+ });
11786
+ else console.log("MoltNet local HTTPS trust is ready for this macOS user.");
11605
11787
  return 0;
11788
+ } catch (cause) {
11789
+ console.error(`Agent Server trust command failed: ${cause instanceof Error ? cause.message : String(cause)}`);
11790
+ return 1;
11606
11791
  }
11607
- await ensureTrustedLocalTls(root);
11608
- console.error("MoltNet local HTTPS trust is ready for this macOS user.");
11609
- return 0;
11792
+ }
11793
+ function printTrustStatus(status) {
11794
+ console.log(JSON.stringify(status));
11610
11795
  }
11611
11796
  async function ensureTrustedLocalTls(root) {
11612
11797
  const material = await ensureLocalTlsMaterial(root);
@@ -11625,12 +11810,13 @@ async function ensureTrustedLocalTls(root) {
11625
11810
  await trustLocalCa(root);
11626
11811
  return material;
11627
11812
  }
11628
- function waitForAgentServerShutdown(runs, app, shutdownController) {
11813
+ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
11629
11814
  return new Promise((resolvePromise) => {
11630
11815
  let shuttingDown = false;
11631
- const shutdown = () => {
11816
+ const shutdown = (source) => {
11632
11817
  if (shuttingDown) return;
11633
11818
  shuttingDown = true;
11819
+ if (source === "stdin") console.error("shutting down: stdin EOF");
11634
11820
  shutdownController.abort({ source: "shutdown" });
11635
11821
  (async () => {
11636
11822
  app.server.closeAllConnections();
@@ -11656,16 +11842,31 @@ function waitForAgentServerShutdown(runs, app, shutdownController) {
11656
11842
  const failures = results.filter((result) => result.status === "rejected");
11657
11843
  for (const failure of failures) console.error(`shutdown cleanup failed: ${failure.reason.message}`);
11658
11844
  handlers.dispose();
11845
+ stdinGuard.dispose();
11659
11846
  const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
11660
11847
  resolvePromise(failures.length > 0 ? 1 : exitCode);
11661
11848
  })();
11662
11849
  };
11663
11850
  const handlers = installShutdownSignalHandlers({
11664
11851
  logDrain: () => console.error("shutting down: stopping runs…"),
11665
- drain: shutdown
11852
+ drain: () => shutdown()
11853
+ });
11854
+ const stdinGuard = installSupervisedStdinGuard({
11855
+ enabled: supervised,
11856
+ shutdown: () => shutdown("stdin")
11666
11857
  });
11667
11858
  });
11668
11859
  }
11860
+ /** Makes the supervising process's stdin pipe part of the server lifecycle. */
11861
+ function installSupervisedStdinGuard(options) {
11862
+ if (!options.enabled) return { dispose: () => void 0 };
11863
+ const input = options.input ?? process.stdin;
11864
+ const onEnd = () => options.shutdown();
11865
+ input.once("end", onEnd);
11866
+ input.resume();
11867
+ if (input.readableEnded) queueMicrotask(onEnd);
11868
+ return { dispose: () => input.off("end", onEnd) };
11869
+ }
11669
11870
  //#endregion
11670
11871
  //#region src/lib/runtime-session-sync.ts
11671
11872
  async function syncRuntimeSessions(deps, input) {
@@ -11782,7 +11983,7 @@ async function runSyncSessions(argv) {
11782
11983
  const { values } = parseArgs({
11783
11984
  args: argv,
11784
11985
  options: {
11785
- ...commonOptionDefs(),
11986
+ ...identityOptionDefs(),
11786
11987
  team: { type: "string" },
11787
11988
  "runtime-profile-id": { type: "string" },
11788
11989
  state: { type: "string" },
@@ -11795,9 +11996,9 @@ async function runSyncSessions(argv) {
11795
11996
  console.error(SYNC_SESSIONS_HELP);
11796
11997
  return 1;
11797
11998
  }
11798
- let opts;
11999
+ let identity;
11799
12000
  try {
11800
- opts = parseCommonOptions(values);
12001
+ identity = parseIdentityProcessOptions(values);
11801
12002
  } catch (err) {
11802
12003
  if (err instanceof MissingRequiredOptionError) {
11803
12004
  console.error(`${err.message}\n`);
@@ -11811,7 +12012,7 @@ async function runSyncSessions(argv) {
11811
12012
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
11812
12013
  const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
11813
12014
  const cfg = loadConfig();
11814
- const ctx = await resolveAgentContext(opts.agent, {
12015
+ const ctx = await resolveAgentContext(identity.agent, {
11815
12016
  credentialSource: cfg.credentialSource,
11816
12017
  envApiUrl: cfg.apiUrl,
11817
12018
  agentRootDir: explicitAgentRootDir
@@ -11826,7 +12027,7 @@ async function runSyncSessions(argv) {
11826
12027
  runtimeSlotStore: createApiRuntimeSlotStore({ agent: ctx.agent }),
11827
12028
  taskReader: ctx.agent.tasks
11828
12029
  }, {
11829
- agentName: opts.agent,
12030
+ agentName: identity.agent,
11830
12031
  dryRun: values["dry-run"] === true,
11831
12032
  limit,
11832
12033
  runtimeProfileId: values["runtime-profile-id"],
@@ -12003,7 +12204,7 @@ async function writeCache(cache) {
12003
12204
  }
12004
12205
  //#endregion
12005
12206
  //#region src/version.ts
12006
- var DAEMON_VERSION = "0.57.0";
12207
+ var DAEMON_VERSION = "0.58.0";
12007
12208
  //#endregion
12008
12209
  //#region src/cli.ts
12009
12210
  async function runAgentDaemonCli(options) {