@tangle-network/agent-runtime 0.343.2 → 0.343.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{activation-BMjalBNv.js → activation-C5rFWXGj.js} +2 -2
  2. package/dist/{activation-BMjalBNv.js.map → activation-C5rFWXGj.js.map} +1 -1
  3. package/dist/agent.js +3 -3
  4. package/dist/{coordination-manager-DIC19cgD.js → coordination-manager-T98ZbsEt.js} +3 -3
  5. package/dist/{coordination-manager-DIC19cgD.js.map → coordination-manager-T98ZbsEt.js.map} +1 -1
  6. package/dist/{delegate-BWXq1BeR.js → delegate-CjdtLf1v.js} +2 -2
  7. package/dist/{delegate-BWXq1BeR.js.map → delegate-CjdtLf1v.js.map} +1 -1
  8. package/dist/{durable-file-CA0kr1Pi.js → durable-file-DLcrzrjL.js} +12 -12
  9. package/dist/durable-file-DLcrzrjL.js.map +1 -0
  10. package/dist/durable.d.ts +13 -9
  11. package/dist/durable.js +46 -70
  12. package/dist/durable.js.map +1 -1
  13. package/dist/{graph-B89lApkU.js → graph-kgzTaNMA.js} +4 -4
  14. package/dist/{graph-B89lApkU.js.map → graph-kgzTaNMA.js.map} +1 -1
  15. package/dist/{improve-CII-AhGN.js → improve-DevAd83a.js} +6 -6
  16. package/dist/{improve-CII-AhGN.js.map → improve-DevAd83a.js.map} +1 -1
  17. package/dist/index.js +7 -7
  18. package/dist/intelligence.js +3 -3
  19. package/dist/kernel.js +10 -10
  20. package/dist/{loop-runner-Bbk9It72.js → loop-runner-CyUoVMkv.js} +2 -2
  21. package/dist/{loop-runner-Bbk9It72.js.map → loop-runner-CyUoVMkv.js.map} +1 -1
  22. package/dist/loop-runner-bin.js +1 -1
  23. package/dist/mcp/bin.js +3 -3
  24. package/dist/mcp/index.js +4 -4
  25. package/dist/{provision-supervisor-it-VCGyZ.js → provision-supervisor-CysVhSqV.js} +5 -5
  26. package/dist/{provision-supervisor-it-VCGyZ.js.map → provision-supervisor-CysVhSqV.js.map} +1 -1
  27. package/dist/{run-cancellation-DSpwvFmQ.js → run-cancellation-CEDiqfhC.js} +2 -2
  28. package/dist/{run-cancellation-DSpwvFmQ.js.map → run-cancellation-CEDiqfhC.js.map} +1 -1
  29. package/dist/{runtime-fKVK1rVb.js → runtime-CefFtXC2.js} +13 -13
  30. package/dist/{runtime-fKVK1rVb.js.map → runtime-CefFtXC2.js.map} +1 -1
  31. package/dist/{search-executor-Ymr-tAKJ.js → search-executor-t05_9Wwk.js} +2 -2
  32. package/dist/{search-executor-Ymr-tAKJ.js.map → search-executor-t05_9Wwk.js.map} +1 -1
  33. package/dist/{server-BngfDubR.js → server-DjrVWzH2.js} +3 -3
  34. package/dist/{server-BngfDubR.js.map → server-DjrVWzH2.js.map} +1 -1
  35. package/dist/{structural-rollout-BmKAIWWR.js → structural-rollout-CpJknIaL.js} +2 -2
  36. package/dist/{structural-rollout-BmKAIWWR.js.map → structural-rollout-CpJknIaL.js.map} +1 -1
  37. package/dist/{supervise-DyCUu8Io.js → supervise-BuVVC7Bl.js} +5 -5
  38. package/dist/{supervise-DyCUu8Io.js.map → supervise-BuVVC7Bl.js.map} +1 -1
  39. package/dist/{supervisor-gDn4C1o5.js → supervisor-D3M9ab1v.js} +3 -3
  40. package/dist/{supervisor-gDn4C1o5.js.map → supervisor-D3M9ab1v.js.map} +1 -1
  41. package/dist/testing.js +11 -11
  42. package/dist/{top-snapshot-OjHdhai1.js → top-snapshot-BGYI6G6K.js} +2 -2
  43. package/dist/{top-snapshot-OjHdhai1.js.map → top-snapshot-BGYI6G6K.js.map} +1 -1
  44. package/dist/tui/bin.js +2 -2
  45. package/dist/tui/index.js +2 -2
  46. package/package.json +6 -5
  47. package/dist/durable-file-CA0kr1Pi.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { i as ConfigError } from "./errors-DodWX-cb.js";
2
- import { t as supervise } from "./supervise-DyCUu8Io.js";
2
+ import { t as supervise } from "./supervise-BuVVC7Bl.js";
3
3
  import { agentProfileSchema } from "@tangle-network/agent-interface";
4
4
  import { computeFindingId, makeFinding } from "@tangle-network/agent-eval";
5
5
  //#region src/runtime/supervise/authoring.ts
@@ -207,4 +207,4 @@ async function delegate(intent, opts) {
207
207
  //#endregion
208
208
  export { defaultProfileRichnessThresholds as a, assessAuthoredProfile as i, delegate as n, profileRichnessFinding as o, asAuthoredProfile as r, supervisorInstructions as s, defaultDelegateBudget as t };
209
209
 
210
- //# sourceMappingURL=delegate-BWXq1BeR.js.map
210
+ //# sourceMappingURL=delegate-CjdtLf1v.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"delegate-BWXq1BeR.js","names":[],"sources":["../src/runtime/supervise/authoring.ts","../src/runtime/supervise/delegate.ts"],"sourcesContent":["/**\n *\n * The supervisor's intelligence is AUTHORING the agents it spawns — not pressing buttons.\n *\n * Every agent here is three things: instructions (system prompt), tools, and a model — its\n * `AgentProfile`. The supervisor's job is to WRITE those profiles: read the task, decompose it,\n * and for each sub-task author a tailored worker recipe. `supervisorInstructions` is the how-to the\n * supervisor reads; canonical Runtime executors materialize the resulting profile.\n *\n * The skill is the single OPTIMIZABLE surface: edit it → the supervisor designs better agents.\n * That is the self-improvement lever (the prompt/skill lever), not the execution plumbing.\n *\n * @experimental\n */\n\nimport { type AnalystFinding, computeFindingId, makeFinding } from '@tangle-network/agent-eval'\nimport {\n type AgentProfile,\n type AgentProfilePrompt,\n agentProfileSchema,\n} from '@tangle-network/agent-interface'\n\n/** What the supervisor AUTHORS per sub-task: one complete canonical profile whose name and\n * task-specific system prompt are present. Every other `AgentProfile` axis is preserved exactly. */\nexport type AuthoredProfile = AgentProfile & {\n readonly name: string\n readonly prompt: AgentProfilePrompt & { readonly systemPrompt: string }\n}\n\n/** Narrow an untyped `spawn_worker` profile argument to an `AuthoredProfile`, or null if the\n * supervisor failed to author one (empty/placeholder profile — a skill violation worth catching). */\nexport function asAuthoredProfile(raw: unknown): AuthoredProfile | null {\n const parsed = agentProfileSchema.safeParse(raw)\n if (!parsed.success) return null\n const systemPrompt = parsed.data.prompt?.systemPrompt\n if (typeof systemPrompt !== 'string' || systemPrompt.trim().length === 0) return null\n return {\n ...parsed.data,\n name:\n typeof parsed.data.name === 'string' && parsed.data.name.length > 0\n ? parsed.data.name\n : 'worker',\n prompt: { ...parsed.data.prompt, systemPrompt },\n }\n}\n\n/** The supervisor skill: an explicit profile-authoring instruction, never an implicit Runtime\n * policy. Editing this text changes how a profile designs the descendants it spawns. */\nexport function supervisorInstructions(opts?: { goal?: string }): string {\n return [\n 'Your delegation craft is AUTHORING: a spawned worker is exactly as good as the profile you write.',\n '',\n 'For the task you are given:',\n '1. DECOMPOSE it into the smallest set of sub-tasks a single focused worker can each deliver.',\n '2. For EACH sub-task, AUTHOR a worker by calling spawn_worker with a COMPLETE `profile`:',\n ' • name and description: who this specialist is and why it exists.',\n ' • prompt.systemPrompt: rich instructions for THIS sub-task — exact output, process, evidence, and what \"done\" means.',\n ' • model.default, model.reasoningEffort, and harness: choose the execution system deliberately when the task benefits from it.',\n ' • tools, mcp, resources.skills/files/instructions, hooks, subagents, permissions, and modes: grant or attach every capability the worker needs; omit an axis only when it is intentionally unnecessary.',\n ' • tools.agent_runtime_coordination_spawn_worker: true ONLY when this child should author and drive descendants. Add only the other agent_runtime_coordination_<verb> tools it will call, such as observe_agent or steer_agent.',\n ' • A child with spawn_worker MUST carry this complete profile-authoring instruction as an immutable resources.skills entry with resources.failOnError: true, so it can author its own descendants from the same contract.',\n ' • metadata may describe the work, but it never grants recursion or selects a Runtime execution path.',\n ' NEVER spawn a worker with an empty profile. The quality of the worker IS the quality of the profile you write.',\n '3. End your turn while your workers run: Runtime wakes you with each settled worker. A settlement says valid:true only if the deployable check passed.',\n '4. If a worker did NOT deliver, AUTHOR A NEW profile whose prompt.systemPrompt names the SPECIFIC failure and how to fix it — never just retry the same profile.',\n \"5. read_journal to re-read YOUR OWN record before you decide the next move: every spawn you made, every settle, every question and answer, every steer, every analyst finding — oldest first, this node only, including what you did before a restart. Use it to see what you already tried instead of trying it again. It is paged: pass the returned nextRow as the next call's sinceRow, narrow with kinds, and raise limit/maxBytes only as far as you will actually read. A truncated:true page means a bound cut it short — keep paging before you conclude you have read everything.\",\n '6. AUTHOR YOUR OWN LENS when the questions you can already ask of a settled trace do not cover the failure you are chasing: define_analyst takes an id, a description, an area, the question in your own words, the instructions for answering it with trace evidence, and the smallest toolGroup that can answer it (model is the seat it runs on; omit it for the run default). It is DATA, never code. Then run_analyst it on any settled worker like a lens the run shipped with, and read the finding. list_analysts shows what you have. Define a lens when you need a different question asked — not a second copy of a question already on the menu.',\n '7. EVERY refusal you get back carries a `reason` naming the exact unmet condition. Read it and change that condition — a spawn refused for budget-exhausted needs a smaller budget or the refund of a settled worker, an invalid-profile needs the named field fixed, a submit_result refused because the check THREW is a broken check to report, not a result to resubmit. Never repeat a call that was refused without changing what it was refused for.',\n '8. ask_parent ONLY when you genuinely cannot decide, and then READ ITS OUTCOME. \"queued-for-parent\" means an inbox above you now holds the question. \"no-parent\" means no inbox above you is configured to receive it: the question is still on the run record for anyone watching, but nothing will route an answer back to you, so do not block. Decide it with answer_question, or answer_question with deferReason to record that it stays open, and carry on — a blocking question left undecided also refuses your stop.',\n '9. Once the work is delivered, end your turn with nothing left running.',\n ...(opts?.goal ? ['', `The goal: ${opts.goal}`] : []),\n ].join('\\n')\n}\n\n// ── Profile-richness gate ────────────────────────────────────────────────────\n//\n// The supervisor's product is the worker PROFILE it authors. The failure mode the existing\n// gates miss: `asAuthoredProfile` / `local-harness` only reject a FULLY EMPTY system prompt —\n// a two-sentence stub passes. `assessAuthoredProfile` OBSERVES the authored artifact (it reads\n// no judge verdict, so it steers cleanly past `assertTraceDerivedFindings`) and flags THIN:\n// a short/few-line system prompt, OR no tools, OR no skills, OR no MCP when the task needs one.\n// It emits a real `AnalystFinding` so it rides the SAME coordination bus the driver pulls via\n// receives in its next wake — the supervisor can self-correct and re-author richer.\n\n/** Thresholds below which a system prompt is treated as a thin stub. Tunable per call. */\nexport interface ProfileRichnessThresholds {\n /** A prompt shorter than this many characters is thin (default 600). */\n readonly minSystemPromptChars: number\n /** A prompt with fewer than this many non-blank lines is thin (default 6). */\n readonly minSystemPromptLines: number\n}\n\n/** Default thresholds for `ProfileRichnessThresholds` — 600 chars / 6 lines minimum system prompt. */\nexport const defaultProfileRichnessThresholds: ProfileRichnessThresholds = {\n minSystemPromptChars: 600,\n minSystemPromptLines: 6,\n}\n\n/** Per-field verdict on one authored profile — the raw material the bench renders + scores. */\nexport interface ProfileRichness {\n readonly name: string\n /** The resolved system prompt (canonical `prompt.systemPrompt`, the sandbox `prompt.system`\n * convention, or a bare-string prompt — whichever the author used). */\n readonly systemPrompt: string\n readonly systemPromptChars: number\n readonly systemPromptLines: number\n readonly sentenceCount: number\n readonly hasDescription: boolean\n readonly hasTools: boolean\n readonly hasSkills: boolean\n readonly hasMcp: boolean\n readonly hasSubagents: boolean\n /** 0..1 — fraction of richness signals present (prompt-depth + the four levers). */\n readonly richness: number\n /** True when the supervisor authored a stub instead of a real profile. */\n readonly thin: boolean\n /** The specific reasons it is thin (empty when rich) — used in the finding's action. */\n readonly reasons: string[]\n}\n\n/** Read the system prompt from any authored shape: canonical `prompt.systemPrompt`, the sandbox\n * `prompt.system` convention, or a bare-string `prompt`. */\nfunction resolveSystemPrompt(profile: AgentProfile): string {\n const pr = (profile as { prompt?: unknown }).prompt\n if (typeof pr === 'string') return pr\n if (pr && typeof pr === 'object') {\n const o = pr as { systemPrompt?: unknown; system?: unknown }\n if (typeof o.systemPrompt === 'string') return o.systemPrompt\n if (typeof o.system === 'string') return o.system\n }\n return ''\n}\n\n/** OBSERVE one authored `AgentProfile` and score its richness (no judge verdict is read). The task\n * context (`needsMcp`) lets a domain say \"this work needs a data/tool MCP\" so a missing MCP counts. */\nexport function assessAuthoredProfile(\n profile: AgentProfile,\n opts?: { needsMcp?: boolean; thresholds?: Partial<ProfileRichnessThresholds> },\n): ProfileRichness {\n const th = { ...defaultProfileRichnessThresholds, ...(opts?.thresholds ?? {}) }\n const systemPrompt = resolveSystemPrompt(profile)\n const trimmed = systemPrompt.trim()\n const systemPromptChars = trimmed.length\n const systemPromptLines = trimmed\n ? trimmed.split('\\n').filter((l) => l.trim().length > 0).length\n : 0\n const sentenceCount = trimmed\n ? (trimmed.match(/[.!?](\\s|$)/g) ?? []).length || (trimmed ? 1 : 0)\n : 0\n const hasDescription =\n typeof profile.description === 'string' && profile.description.trim().length > 0\n const tools = (profile as { tools?: Record<string, unknown> }).tools\n const hasTools = !!tools && Object.keys(tools).length > 0\n const skills = (profile.resources as { skills?: unknown[] } | undefined)?.skills\n const hasSkills = Array.isArray(skills) && skills.length > 0\n const mcp = (profile as { mcp?: Record<string, unknown> }).mcp\n const hasMcp = !!mcp && Object.keys(mcp).length > 0\n const subagents = (profile as { subagents?: Record<string, unknown> }).subagents\n const hasSubagents = !!subagents && Object.keys(subagents).length > 0\n\n const reasons: string[] = []\n const promptThin =\n systemPromptChars < th.minSystemPromptChars || systemPromptLines < th.minSystemPromptLines\n if (promptThin)\n reasons.push(\n `system prompt is thin (${systemPromptChars} chars, ${systemPromptLines} lines; need ≥${th.minSystemPromptChars} chars and ≥${th.minSystemPromptLines} lines)`,\n )\n if (!hasTools)\n reasons.push('no tools granted (a worker can only act through the tools you grant it)')\n if (!hasSkills) reasons.push('no skills attached (no reusable how-to notes injected)')\n if (opts?.needsMcp && !hasMcp) reasons.push('no MCP server, but the task needs data/tool access')\n\n // Richness = fraction of signals present. Prompt-depth is one signal; the four levers are the rest.\n const signals = [!promptThin, hasTools, hasSkills, hasDescription, opts?.needsMcp ? hasMcp : true]\n const richness = signals.filter(Boolean).length / signals.length\n // THIN ⟺ the prompt is a stub OR the worker has no levers at all (no tools AND no skills AND no mcp).\n const thin = promptThin || (!hasTools && !hasSkills && !hasMcp)\n\n return {\n name: profile.name ?? 'worker',\n systemPrompt,\n systemPromptChars,\n systemPromptLines,\n sentenceCount,\n hasDescription,\n hasTools,\n hasSkills,\n hasMcp,\n hasSubagents,\n richness,\n thin,\n reasons,\n }\n}\n\n/** Turn a {@link ProfileRichness} verdict into a bus-routable `AnalystFinding` (area `profile-quality`).\n * Severity scales with thinness; the recommended action names the MISSING lever so the supervisor can\n * re-author. `subject` = the worker name so per-worker findings diff cleanly across re-authors. */\nexport function profileRichnessFinding(\n richness: ProfileRichness,\n opts?: { analystId?: string; runId?: string },\n): AnalystFinding {\n const analyst_id = opts?.analystId ?? 'profile-richness'\n const subject = richness.name\n const claim = richness.thin\n ? `Worker \"${richness.name}\" was authored as a THIN profile: ${richness.reasons.join('; ')}.`\n : `Worker \"${richness.name}\" was authored as a rich profile (richness ${(richness.richness * 100).toFixed(0)}%).`\n const severity: AnalystFinding['severity'] = richness.thin\n ? richness.richness < 0.25\n ? 'high'\n : 'medium'\n : 'info'\n return makeFinding({\n analyst_id,\n severity,\n area: 'profile-quality',\n claim,\n subject,\n confidence: 0.9,\n evidence_refs: [\n {\n kind: 'metric',\n uri: `profile:${subject}`,\n excerpt: `chars=${richness.systemPromptChars} lines=${richness.systemPromptLines} tools=${richness.hasTools} skills=${richness.hasSkills} mcp=${richness.hasMcp} richness=${richness.richness.toFixed(2)}`,\n },\n ],\n ...(richness.thin\n ? { recommended_action: `Re-author \"${richness.name}\" with: ${richness.reasons.join('; ')}.` }\n : {}),\n id_basis: computeFindingId({\n analyst_id,\n area: 'profile-quality',\n subject,\n claim: `richness:${richness.thin ? 'thin' : 'rich'}`,\n }),\n })\n}\n","/**\n *\n * `delegate` — the one generic delegation verb. You hand it an INTENT (what you want done) and it\n * hands that intent to a default AUTHORING supervisor: a router-brained supervisor whose standing\n * instruction is `supervisorInstructions()` (the authoring-agent-profiles skill). The supervisor\n * DECOMPOSES the intent and AUTHORS the worker profile it needs per sub-task — there is NO hardcoded\n * coder/researcher profile here. That is the whole point: `delegate('fix the failing test', …)` and\n * `delegate('research X and cite sources', …)` route through the SAME front door; the supervisor\n * writes a code-shaped or research-shaped worker on its own.\n *\n * It is a thin wrapper over `supervise()` — the one front door — so the conserved-budget pool, the\n * completion oracle (`deliverable`), the coordination toolbox, and equal-compute accounting all come\n * for free; nothing is hand-rolled. The result is `supervise()`'s `SupervisedResult` returned\n * UNCHANGED, so its `spentTotal` (`{ iterations, tokens, usd, ms }`) rides straight back to the\n * caller on BOTH paths — a `winner` carries the delivered worker's spend, a `no-winner` carries the\n * spend incurred before it failed. That cost channel means a `delegate()` caller always learns what\n * the delegation actually spent.\n *\n * @experimental\n */\n\nimport { ConfigError } from '../../errors'\nimport type { RouterTransportConfig } from '../router-http-transport'\nimport type { DeliverableSpec } from './completion-gate'\nimport type { ExecutorConfig } from './runtime'\nimport { supervise } from './supervise'\nimport type { SupervisorProfile } from './supervisor-agent'\nimport type { Budget, SupervisedResult } from './types'\n\n/** The conserved pool a `delegate()` call applies when the caller does not pass its own `budget`.\n * A modest token ceiling + a small iteration ceiling — generous enough for a few-worker decompose,\n * bounded enough that an unsupervised intent cannot run away. Callers override via `opts.budget`. */\nexport const defaultDelegateBudget: Budget = { maxIterations: 50, maxTokens: 200_000 }\n\n/** Inputs to {@link delegate}. The intent is the first positional arg; everything here is optional\n * with explicit execution identity, so the common call names one exact supervisor profile. */\nexport interface DelegateOptions<Out = unknown> {\n /** The completion oracle (settled ⟺ delivered) the authored workers settle against. Strongly\n * recommended — without it the supervisor trusts a worker's self-report. For a code intent,\n * `patchDelivered()` is the canonical example; for a free-form answer, a content check. */\n readonly deliverable?: DeliverableSpec<Out>\n /** WHERE the authored workers run — the worker-execution backend (`router-tools` / `sandbox` /\n * `cli-worktree` / …). The supervisor authors the worker PROFILE; this is the substrate it runs\n * on. Provide this OR `makeWorkerAgent`-style wiring through `supervise()` is unavailable. */\n readonly backend?: ExecutorConfig\n /** The conserved compute pool for the whole delegation. Defaults to {@link defaultDelegateBudget}. */\n readonly budget?: Budget\n /** Exact executable authoring supervisor. Model, prompt, harness, and provider live here. */\n readonly supervisorProfile: SupervisorProfile\n /** Router endpoint/auth for a `cli-base` supervisor; contains no behavioral settings. */\n readonly router: RouterTransportConfig\n /** Restrict the run to this subset of models (forwarded to `supervise()`). */\n readonly allowedModels?: readonly string[]\n readonly runId?: string\n}\n\n/**\n * Delegate an INTENT to a default authoring supervisor and return its `SupervisedResult` unchanged.\n *\n * The supervisor authors + spawns whatever worker the intent needs over the conserved-budget pool;\n * `result.spentTotal` reports what the whole delegation actually cost. A `winner` result carries the\n * authored worker's delivered output; a `no-winner` result names why (never a fabricated success).\n */\nexport async function delegate<Out = unknown>(\n intent: string,\n opts: DelegateOptions<Out>,\n): Promise<SupervisedResult<Out>> {\n if (typeof intent !== 'string' || intent.trim().length === 0) {\n throw new ConfigError('delegate: `intent` must be a non-empty string')\n }\n return supervise(opts.supervisorProfile, intent, {\n budget: opts.budget ?? defaultDelegateBudget,\n ...(opts.backend ? { backend: opts.backend } : {}),\n ...(opts.deliverable ? { deliverable: opts.deliverable as DeliverableSpec<unknown> } : {}),\n router: opts.router,\n ...(opts.allowedModels ? { allowedModels: opts.allowedModels } : {}),\n ...(opts.runId ? { runId: opts.runId } : {}),\n }) as Promise<SupervisedResult<Out>>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"delegate-CjdtLf1v.js","names":[],"sources":["../src/runtime/supervise/authoring.ts","../src/runtime/supervise/delegate.ts"],"sourcesContent":["/**\n *\n * The supervisor's intelligence is AUTHORING the agents it spawns — not pressing buttons.\n *\n * Every agent here is three things: instructions (system prompt), tools, and a model — its\n * `AgentProfile`. The supervisor's job is to WRITE those profiles: read the task, decompose it,\n * and for each sub-task author a tailored worker recipe. `supervisorInstructions` is the how-to the\n * supervisor reads; canonical Runtime executors materialize the resulting profile.\n *\n * The skill is the single OPTIMIZABLE surface: edit it → the supervisor designs better agents.\n * That is the self-improvement lever (the prompt/skill lever), not the execution plumbing.\n *\n * @experimental\n */\n\nimport { type AnalystFinding, computeFindingId, makeFinding } from '@tangle-network/agent-eval'\nimport {\n type AgentProfile,\n type AgentProfilePrompt,\n agentProfileSchema,\n} from '@tangle-network/agent-interface'\n\n/** What the supervisor AUTHORS per sub-task: one complete canonical profile whose name and\n * task-specific system prompt are present. Every other `AgentProfile` axis is preserved exactly. */\nexport type AuthoredProfile = AgentProfile & {\n readonly name: string\n readonly prompt: AgentProfilePrompt & { readonly systemPrompt: string }\n}\n\n/** Narrow an untyped `spawn_worker` profile argument to an `AuthoredProfile`, or null if the\n * supervisor failed to author one (empty/placeholder profile — a skill violation worth catching). */\nexport function asAuthoredProfile(raw: unknown): AuthoredProfile | null {\n const parsed = agentProfileSchema.safeParse(raw)\n if (!parsed.success) return null\n const systemPrompt = parsed.data.prompt?.systemPrompt\n if (typeof systemPrompt !== 'string' || systemPrompt.trim().length === 0) return null\n return {\n ...parsed.data,\n name:\n typeof parsed.data.name === 'string' && parsed.data.name.length > 0\n ? parsed.data.name\n : 'worker',\n prompt: { ...parsed.data.prompt, systemPrompt },\n }\n}\n\n/** The supervisor skill: an explicit profile-authoring instruction, never an implicit Runtime\n * policy. Editing this text changes how a profile designs the descendants it spawns. */\nexport function supervisorInstructions(opts?: { goal?: string }): string {\n return [\n 'Your delegation craft is AUTHORING: a spawned worker is exactly as good as the profile you write.',\n '',\n 'For the task you are given:',\n '1. DECOMPOSE it into the smallest set of sub-tasks a single focused worker can each deliver.',\n '2. For EACH sub-task, AUTHOR a worker by calling spawn_worker with a COMPLETE `profile`:',\n ' • name and description: who this specialist is and why it exists.',\n ' • prompt.systemPrompt: rich instructions for THIS sub-task — exact output, process, evidence, and what \"done\" means.',\n ' • model.default, model.reasoningEffort, and harness: choose the execution system deliberately when the task benefits from it.',\n ' • tools, mcp, resources.skills/files/instructions, hooks, subagents, permissions, and modes: grant or attach every capability the worker needs; omit an axis only when it is intentionally unnecessary.',\n ' • tools.agent_runtime_coordination_spawn_worker: true ONLY when this child should author and drive descendants. Add only the other agent_runtime_coordination_<verb> tools it will call, such as observe_agent or steer_agent.',\n ' • A child with spawn_worker MUST carry this complete profile-authoring instruction as an immutable resources.skills entry with resources.failOnError: true, so it can author its own descendants from the same contract.',\n ' • metadata may describe the work, but it never grants recursion or selects a Runtime execution path.',\n ' NEVER spawn a worker with an empty profile. The quality of the worker IS the quality of the profile you write.',\n '3. End your turn while your workers run: Runtime wakes you with each settled worker. A settlement says valid:true only if the deployable check passed.',\n '4. If a worker did NOT deliver, AUTHOR A NEW profile whose prompt.systemPrompt names the SPECIFIC failure and how to fix it — never just retry the same profile.',\n \"5. read_journal to re-read YOUR OWN record before you decide the next move: every spawn you made, every settle, every question and answer, every steer, every analyst finding — oldest first, this node only, including what you did before a restart. Use it to see what you already tried instead of trying it again. It is paged: pass the returned nextRow as the next call's sinceRow, narrow with kinds, and raise limit/maxBytes only as far as you will actually read. A truncated:true page means a bound cut it short — keep paging before you conclude you have read everything.\",\n '6. AUTHOR YOUR OWN LENS when the questions you can already ask of a settled trace do not cover the failure you are chasing: define_analyst takes an id, a description, an area, the question in your own words, the instructions for answering it with trace evidence, and the smallest toolGroup that can answer it (model is the seat it runs on; omit it for the run default). It is DATA, never code. Then run_analyst it on any settled worker like a lens the run shipped with, and read the finding. list_analysts shows what you have. Define a lens when you need a different question asked — not a second copy of a question already on the menu.',\n '7. EVERY refusal you get back carries a `reason` naming the exact unmet condition. Read it and change that condition — a spawn refused for budget-exhausted needs a smaller budget or the refund of a settled worker, an invalid-profile needs the named field fixed, a submit_result refused because the check THREW is a broken check to report, not a result to resubmit. Never repeat a call that was refused without changing what it was refused for.',\n '8. ask_parent ONLY when you genuinely cannot decide, and then READ ITS OUTCOME. \"queued-for-parent\" means an inbox above you now holds the question. \"no-parent\" means no inbox above you is configured to receive it: the question is still on the run record for anyone watching, but nothing will route an answer back to you, so do not block. Decide it with answer_question, or answer_question with deferReason to record that it stays open, and carry on — a blocking question left undecided also refuses your stop.',\n '9. Once the work is delivered, end your turn with nothing left running.',\n ...(opts?.goal ? ['', `The goal: ${opts.goal}`] : []),\n ].join('\\n')\n}\n\n// ── Profile-richness gate ────────────────────────────────────────────────────\n//\n// The supervisor's product is the worker PROFILE it authors. The failure mode the existing\n// gates miss: `asAuthoredProfile` / `local-harness` only reject a FULLY EMPTY system prompt —\n// a two-sentence stub passes. `assessAuthoredProfile` OBSERVES the authored artifact (it reads\n// no judge verdict, so it steers cleanly past `assertTraceDerivedFindings`) and flags THIN:\n// a short/few-line system prompt, OR no tools, OR no skills, OR no MCP when the task needs one.\n// It emits a real `AnalystFinding` so it rides the SAME coordination bus the driver pulls via\n// receives in its next wake — the supervisor can self-correct and re-author richer.\n\n/** Thresholds below which a system prompt is treated as a thin stub. Tunable per call. */\nexport interface ProfileRichnessThresholds {\n /** A prompt shorter than this many characters is thin (default 600). */\n readonly minSystemPromptChars: number\n /** A prompt with fewer than this many non-blank lines is thin (default 6). */\n readonly minSystemPromptLines: number\n}\n\n/** Default thresholds for `ProfileRichnessThresholds` — 600 chars / 6 lines minimum system prompt. */\nexport const defaultProfileRichnessThresholds: ProfileRichnessThresholds = {\n minSystemPromptChars: 600,\n minSystemPromptLines: 6,\n}\n\n/** Per-field verdict on one authored profile — the raw material the bench renders + scores. */\nexport interface ProfileRichness {\n readonly name: string\n /** The resolved system prompt (canonical `prompt.systemPrompt`, the sandbox `prompt.system`\n * convention, or a bare-string prompt — whichever the author used). */\n readonly systemPrompt: string\n readonly systemPromptChars: number\n readonly systemPromptLines: number\n readonly sentenceCount: number\n readonly hasDescription: boolean\n readonly hasTools: boolean\n readonly hasSkills: boolean\n readonly hasMcp: boolean\n readonly hasSubagents: boolean\n /** 0..1 — fraction of richness signals present (prompt-depth + the four levers). */\n readonly richness: number\n /** True when the supervisor authored a stub instead of a real profile. */\n readonly thin: boolean\n /** The specific reasons it is thin (empty when rich) — used in the finding's action. */\n readonly reasons: string[]\n}\n\n/** Read the system prompt from any authored shape: canonical `prompt.systemPrompt`, the sandbox\n * `prompt.system` convention, or a bare-string `prompt`. */\nfunction resolveSystemPrompt(profile: AgentProfile): string {\n const pr = (profile as { prompt?: unknown }).prompt\n if (typeof pr === 'string') return pr\n if (pr && typeof pr === 'object') {\n const o = pr as { systemPrompt?: unknown; system?: unknown }\n if (typeof o.systemPrompt === 'string') return o.systemPrompt\n if (typeof o.system === 'string') return o.system\n }\n return ''\n}\n\n/** OBSERVE one authored `AgentProfile` and score its richness (no judge verdict is read). The task\n * context (`needsMcp`) lets a domain say \"this work needs a data/tool MCP\" so a missing MCP counts. */\nexport function assessAuthoredProfile(\n profile: AgentProfile,\n opts?: { needsMcp?: boolean; thresholds?: Partial<ProfileRichnessThresholds> },\n): ProfileRichness {\n const th = { ...defaultProfileRichnessThresholds, ...(opts?.thresholds ?? {}) }\n const systemPrompt = resolveSystemPrompt(profile)\n const trimmed = systemPrompt.trim()\n const systemPromptChars = trimmed.length\n const systemPromptLines = trimmed\n ? trimmed.split('\\n').filter((l) => l.trim().length > 0).length\n : 0\n const sentenceCount = trimmed\n ? (trimmed.match(/[.!?](\\s|$)/g) ?? []).length || (trimmed ? 1 : 0)\n : 0\n const hasDescription =\n typeof profile.description === 'string' && profile.description.trim().length > 0\n const tools = (profile as { tools?: Record<string, unknown> }).tools\n const hasTools = !!tools && Object.keys(tools).length > 0\n const skills = (profile.resources as { skills?: unknown[] } | undefined)?.skills\n const hasSkills = Array.isArray(skills) && skills.length > 0\n const mcp = (profile as { mcp?: Record<string, unknown> }).mcp\n const hasMcp = !!mcp && Object.keys(mcp).length > 0\n const subagents = (profile as { subagents?: Record<string, unknown> }).subagents\n const hasSubagents = !!subagents && Object.keys(subagents).length > 0\n\n const reasons: string[] = []\n const promptThin =\n systemPromptChars < th.minSystemPromptChars || systemPromptLines < th.minSystemPromptLines\n if (promptThin)\n reasons.push(\n `system prompt is thin (${systemPromptChars} chars, ${systemPromptLines} lines; need ≥${th.minSystemPromptChars} chars and ≥${th.minSystemPromptLines} lines)`,\n )\n if (!hasTools)\n reasons.push('no tools granted (a worker can only act through the tools you grant it)')\n if (!hasSkills) reasons.push('no skills attached (no reusable how-to notes injected)')\n if (opts?.needsMcp && !hasMcp) reasons.push('no MCP server, but the task needs data/tool access')\n\n // Richness = fraction of signals present. Prompt-depth is one signal; the four levers are the rest.\n const signals = [!promptThin, hasTools, hasSkills, hasDescription, opts?.needsMcp ? hasMcp : true]\n const richness = signals.filter(Boolean).length / signals.length\n // THIN ⟺ the prompt is a stub OR the worker has no levers at all (no tools AND no skills AND no mcp).\n const thin = promptThin || (!hasTools && !hasSkills && !hasMcp)\n\n return {\n name: profile.name ?? 'worker',\n systemPrompt,\n systemPromptChars,\n systemPromptLines,\n sentenceCount,\n hasDescription,\n hasTools,\n hasSkills,\n hasMcp,\n hasSubagents,\n richness,\n thin,\n reasons,\n }\n}\n\n/** Turn a {@link ProfileRichness} verdict into a bus-routable `AnalystFinding` (area `profile-quality`).\n * Severity scales with thinness; the recommended action names the MISSING lever so the supervisor can\n * re-author. `subject` = the worker name so per-worker findings diff cleanly across re-authors. */\nexport function profileRichnessFinding(\n richness: ProfileRichness,\n opts?: { analystId?: string; runId?: string },\n): AnalystFinding {\n const analyst_id = opts?.analystId ?? 'profile-richness'\n const subject = richness.name\n const claim = richness.thin\n ? `Worker \"${richness.name}\" was authored as a THIN profile: ${richness.reasons.join('; ')}.`\n : `Worker \"${richness.name}\" was authored as a rich profile (richness ${(richness.richness * 100).toFixed(0)}%).`\n const severity: AnalystFinding['severity'] = richness.thin\n ? richness.richness < 0.25\n ? 'high'\n : 'medium'\n : 'info'\n return makeFinding({\n analyst_id,\n severity,\n area: 'profile-quality',\n claim,\n subject,\n confidence: 0.9,\n evidence_refs: [\n {\n kind: 'metric',\n uri: `profile:${subject}`,\n excerpt: `chars=${richness.systemPromptChars} lines=${richness.systemPromptLines} tools=${richness.hasTools} skills=${richness.hasSkills} mcp=${richness.hasMcp} richness=${richness.richness.toFixed(2)}`,\n },\n ],\n ...(richness.thin\n ? { recommended_action: `Re-author \"${richness.name}\" with: ${richness.reasons.join('; ')}.` }\n : {}),\n id_basis: computeFindingId({\n analyst_id,\n area: 'profile-quality',\n subject,\n claim: `richness:${richness.thin ? 'thin' : 'rich'}`,\n }),\n })\n}\n","/**\n *\n * `delegate` — the one generic delegation verb. You hand it an INTENT (what you want done) and it\n * hands that intent to a default AUTHORING supervisor: a router-brained supervisor whose standing\n * instruction is `supervisorInstructions()` (the authoring-agent-profiles skill). The supervisor\n * DECOMPOSES the intent and AUTHORS the worker profile it needs per sub-task — there is NO hardcoded\n * coder/researcher profile here. That is the whole point: `delegate('fix the failing test', …)` and\n * `delegate('research X and cite sources', …)` route through the SAME front door; the supervisor\n * writes a code-shaped or research-shaped worker on its own.\n *\n * It is a thin wrapper over `supervise()` — the one front door — so the conserved-budget pool, the\n * completion oracle (`deliverable`), the coordination toolbox, and equal-compute accounting all come\n * for free; nothing is hand-rolled. The result is `supervise()`'s `SupervisedResult` returned\n * UNCHANGED, so its `spentTotal` (`{ iterations, tokens, usd, ms }`) rides straight back to the\n * caller on BOTH paths — a `winner` carries the delivered worker's spend, a `no-winner` carries the\n * spend incurred before it failed. That cost channel means a `delegate()` caller always learns what\n * the delegation actually spent.\n *\n * @experimental\n */\n\nimport { ConfigError } from '../../errors'\nimport type { RouterTransportConfig } from '../router-http-transport'\nimport type { DeliverableSpec } from './completion-gate'\nimport type { ExecutorConfig } from './runtime'\nimport { supervise } from './supervise'\nimport type { SupervisorProfile } from './supervisor-agent'\nimport type { Budget, SupervisedResult } from './types'\n\n/** The conserved pool a `delegate()` call applies when the caller does not pass its own `budget`.\n * A modest token ceiling + a small iteration ceiling — generous enough for a few-worker decompose,\n * bounded enough that an unsupervised intent cannot run away. Callers override via `opts.budget`. */\nexport const defaultDelegateBudget: Budget = { maxIterations: 50, maxTokens: 200_000 }\n\n/** Inputs to {@link delegate}. The intent is the first positional arg; everything here is optional\n * with explicit execution identity, so the common call names one exact supervisor profile. */\nexport interface DelegateOptions<Out = unknown> {\n /** The completion oracle (settled ⟺ delivered) the authored workers settle against. Strongly\n * recommended — without it the supervisor trusts a worker's self-report. For a code intent,\n * `patchDelivered()` is the canonical example; for a free-form answer, a content check. */\n readonly deliverable?: DeliverableSpec<Out>\n /** WHERE the authored workers run — the worker-execution backend (`router-tools` / `sandbox` /\n * `cli-worktree` / …). The supervisor authors the worker PROFILE; this is the substrate it runs\n * on. Provide this OR `makeWorkerAgent`-style wiring through `supervise()` is unavailable. */\n readonly backend?: ExecutorConfig\n /** The conserved compute pool for the whole delegation. Defaults to {@link defaultDelegateBudget}. */\n readonly budget?: Budget\n /** Exact executable authoring supervisor. Model, prompt, harness, and provider live here. */\n readonly supervisorProfile: SupervisorProfile\n /** Router endpoint/auth for a `cli-base` supervisor; contains no behavioral settings. */\n readonly router: RouterTransportConfig\n /** Restrict the run to this subset of models (forwarded to `supervise()`). */\n readonly allowedModels?: readonly string[]\n readonly runId?: string\n}\n\n/**\n * Delegate an INTENT to a default authoring supervisor and return its `SupervisedResult` unchanged.\n *\n * The supervisor authors + spawns whatever worker the intent needs over the conserved-budget pool;\n * `result.spentTotal` reports what the whole delegation actually cost. A `winner` result carries the\n * authored worker's delivered output; a `no-winner` result names why (never a fabricated success).\n */\nexport async function delegate<Out = unknown>(\n intent: string,\n opts: DelegateOptions<Out>,\n): Promise<SupervisedResult<Out>> {\n if (typeof intent !== 'string' || intent.trim().length === 0) {\n throw new ConfigError('delegate: `intent` must be a non-empty string')\n }\n return supervise(opts.supervisorProfile, intent, {\n budget: opts.budget ?? defaultDelegateBudget,\n ...(opts.backend ? { backend: opts.backend } : {}),\n ...(opts.deliverable ? { deliverable: opts.deliverable as DeliverableSpec<unknown> } : {}),\n router: opts.router,\n ...(opts.allowedModels ? { allowedModels: opts.allowedModels } : {}),\n ...(opts.runId ? { runId: opts.runId } : {}),\n }) as Promise<SupervisedResult<Out>>\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, openSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
+ import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  //#region \0rolldown/runtime.js
5
5
  var __defProp = Object.defineProperty;
@@ -35,7 +35,10 @@ var durable_file_exports = /* @__PURE__ */ __exportAll({
35
35
  syncDurableDirectory: () => syncDurableDirectory,
36
36
  writeAtomicDurableFile: () => writeAtomicDurableFile
37
37
  });
38
- /** Publish a new file without replacing a concurrent winner. */
38
+ /**
39
+ * Publish a new file without replacing a concurrent winner. The published entry is a hard link
40
+ * of the temporary file, so a content function sees the published file's inode.
41
+ */
39
42
  function publishExclusiveDurableFile(filePath, content, options = {}) {
40
43
  const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
41
44
  let operationFailed = false;
@@ -143,15 +146,12 @@ function writeAtomicDurableFile(filePath, content, options = {}) {
143
146
  if (cleanupError !== void 0) throw cleanupError;
144
147
  }
145
148
  function writeExclusiveDurableFile(filePath, content, options, exactMode = false) {
146
- if (options.mode === void 0) writeFileSync(filePath, content, {
147
- encoding: "utf8",
148
- flag: "wx"
149
- });
150
- else writeFileSync(filePath, content, {
151
- encoding: "utf8",
152
- flag: "wx",
153
- mode: options.mode
154
- });
149
+ const fd = openSync(filePath, "wx", options.mode ?? 438);
150
+ try {
151
+ writeFileSync(fd, typeof content === "string" ? content : content(fstatSync(fd, { bigint: true })), "utf8");
152
+ } finally {
153
+ closeSync(fd);
154
+ }
155
155
  syncDurableFile(filePath, exactMode ? options.mode : void 0);
156
156
  }
157
157
  function isAlreadyExistsError(error) {
@@ -163,4 +163,4 @@ function isNoEntryError(error) {
163
163
  //#endregion
164
164
  export { syncDurableDirectory as a, __exportAll as c, publishExclusiveDurableFile as i, assertNoSymlinkDescendant as n, writeAtomicDurableFile as o, durable_file_exports as r, sha256 as s, appendDurableFile as t };
165
165
 
166
- //# sourceMappingURL=durable-file-CA0kr1Pi.js.map
166
+ //# sourceMappingURL=durable-file-DLcrzrjL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-file-DLcrzrjL.js","names":["fsConstants"],"sources":["../src/content-digest.ts","../src/runtime/supervise/durable-file.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\n\nexport function sha256(value: string): string {\n return createHash('sha256').update(value).digest('hex')\n}\n","/**\n * Small filesystem durability primitives for Runtime-owned state.\n *\n * Callers must validate their path and parent directory before invoking these functions.\n * The exclusive writer protects a no-clobber publication race; the atomic writer replaces one\n * state file after its temporary contents reach stable storage.\n *\n * @internal\n */\n\nimport { randomUUID } from 'node:crypto'\nimport {\n type BigIntStats,\n closeSync,\n existsSync,\n fchmodSync,\n constants as fsConstants,\n fstatSync,\n fsyncSync,\n linkSync,\n lstatSync,\n openSync,\n renameSync,\n statSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n} from 'node:fs'\nimport { dirname, join, relative, resolve } from 'node:path'\n\ninterface DurableFileOptions {\n readonly mode?: number\n}\n\n/**\n * File content, or content computed from the new file's own stat when it must name the file's\n * inode (a lock record that later proves it was written for this very file).\n */\ntype DurableFileContent = string | ((file: BigIntStats) => string)\n\n/**\n * Publish a new file without replacing a concurrent winner. The published entry is a hard link\n * of the temporary file, so a content function sees the published file's inode.\n */\nexport function publishExclusiveDurableFile(\n filePath: string,\n content: DurableFileContent,\n options: DurableFileOptions = {},\n): boolean {\n const temporaryPath = `${filePath}.${randomUUID()}.tmp`\n let operationFailed = false\n let operationError: unknown\n let published = false\n try {\n writeExclusiveDurableFile(temporaryPath, content, options)\n try {\n linkSync(temporaryPath, filePath)\n syncDurableDirectory(dirname(filePath))\n published = true\n } catch (error) {\n if (!isAlreadyExistsError(error)) throw error\n }\n } catch (error) {\n operationFailed = true\n operationError = error\n }\n let cleanupError: unknown\n try {\n unlinkSync(temporaryPath)\n } catch (error) {\n if (!isNoEntryError(error)) cleanupError = error\n }\n if (operationFailed) throw operationError\n if (cleanupError !== undefined) throw cleanupError\n return published\n}\n\n/** Fsync a directory after publishing a durable file entry. */\nexport function syncDurableDirectory(directoryPath: string): void {\n const fd = openSync(directoryPath, 'r')\n try {\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/** Fsync one durable file after its contents reach stable storage. */\nfunction syncDurableFile(filePath: string, exactMode?: number): void {\n const fd = openSync(filePath, 'r')\n try {\n if (exactMode !== undefined && process.platform !== 'win32') fchmodSync(fd, exactMode)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/** Append one record with O_APPEND, then fsync the file and containing directory. */\nexport function appendDurableFile(\n filePath: string,\n content: string,\n options: DurableFileOptions = {},\n): void {\n const flags =\n fsConstants.O_APPEND |\n fsConstants.O_CREAT |\n fsConstants.O_WRONLY |\n (typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0)\n const fd = openSync(filePath, flags, options.mode ?? 0o600)\n try {\n if (process.platform !== 'win32') fchmodSync(fd, options.mode ?? 0o600)\n const expectedBytes = Buffer.byteLength(content, 'utf8')\n const writtenBytes = writeSync(fd, content, undefined, 'utf8')\n if (writtenBytes !== expectedBytes) {\n throw new Error(\n `durable append wrote ${writtenBytes} of ${expectedBytes} bytes to '${filePath}'`,\n )\n }\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n syncDurableDirectory(dirname(filePath))\n}\n\n/** Reject a path that escapes its run directory or traverses a symbolic link. */\nexport function assertNoSymlinkDescendant(root: string, target: string, label = 'path'): void {\n const base = resolve(root)\n const exact = resolve(target)\n const rel = relative(base, exact)\n if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\\\' : '/'}`)) {\n throw new Error(`${label} path escapes its run directory`)\n }\n if (existsSync(base) && lstatSync(base).isSymbolicLink()) {\n throw new Error(`${label} path contains a symbolic link: ${base}`)\n }\n let current = base\n for (const part of rel.split(/[\\\\/]/u).filter(Boolean)) {\n current = join(current, part)\n if (!existsSync(current)) continue\n if (lstatSync(current).isSymbolicLink()) {\n throw new Error(`${label} path contains a symbolic link: ${current}`)\n }\n }\n}\n\n/** Write, fsync, atomically replace, and fsync one Runtime-owned state file.\n * preserveMode retains ordinary access permissions despite the current umask; a new\n * file keeps normal creation semantics. Callers still own path validation and write exclusion. */\nexport function writeAtomicDurableFile(\n filePath: string,\n content: string,\n options: DurableFileOptions & { readonly preserveMode?: boolean } = {},\n): void {\n const temporaryPath = `${filePath}.${randomUUID()}.tmp`\n let operationFailed = false\n let operationError: unknown\n try {\n let mode = options.mode\n let exactMode = false\n if (options.preserveMode) {\n try {\n mode = statSync(filePath).mode & 0o777\n exactMode = true\n } catch (error) {\n if (!isNoEntryError(error)) throw error\n }\n }\n writeExclusiveDurableFile(temporaryPath, content, { mode }, exactMode)\n renameSync(temporaryPath, filePath)\n syncDurableDirectory(dirname(filePath))\n } catch (error) {\n operationFailed = true\n operationError = error\n }\n let cleanupError: unknown\n try {\n unlinkSync(temporaryPath)\n } catch (error) {\n if (!isNoEntryError(error)) cleanupError = error\n // A successful rename removes the temporary directory entry.\n }\n if (operationFailed) throw operationError\n if (cleanupError !== undefined) throw cleanupError\n}\n\nfunction writeExclusiveDurableFile(\n filePath: string,\n content: DurableFileContent,\n options: DurableFileOptions,\n exactMode = false,\n): void {\n const fd = openSync(filePath, 'wx', options.mode ?? 0o666)\n try {\n const text = typeof content === 'string' ? content : content(fstatSync(fd, { bigint: true }))\n writeFileSync(fd, text, 'utf8')\n } finally {\n closeSync(fd)\n }\n syncDurableFile(filePath, exactMode ? options.mode : undefined)\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error !== null &&\n typeof error === 'object' &&\n 'code' in error &&\n (error as { code?: unknown }).code === 'EEXIST'\n )\n}\n\nfunction isNoEntryError(error: unknown): boolean {\n return (\n error !== null &&\n typeof error === 'object' &&\n 'code' in error &&\n (error as { code?: unknown }).code === 'ENOENT'\n )\n}\n"],"mappings":"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"}
package/dist/durable.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { $l as PursuitVersions, Cu as FileObserverJournal, D as RetainedPendingCause, Dd as SelectSeat, Du as observerRecordDigest, Eu as ObserverRecordKind, Gf as CoordinationAuthentication, Hd as DeadlineCheckIn, Jf as CoordinationReadAccess, Jl as JudgedPursuitVersion, Km as ToolLoopCompactionOptions, Mv as DownMessageAuthorizationInput, Qf as CoordinationRejectReason, Ql as PursuitVersionStop, Rv as MapOnSettleRoute, Su as projectPursuit, Td as SeatSelectionInput, Tu as ObserverRecord, V as NodeId, Xl as NextPursuitVersionInput, Yf as CoordinationTransportOptions, Yl as NextPursuitVersion, Zf as CoordinationHttpAudit, Zl as PursuitVersionChain, _u as PursuitNodeUsage, au as SupervisePursuitError, bu as PursuitRunTotals, cd as AuthorizedSpawnContext, cu as superviseGraphPursuit, dd as WorkerSlots, df as ResolveDriveHarness, du as PursuitObserverDelivery, eu as SettledPursuitVersion, fu as PursuitCostProvenance, gu as PursuitNodeTiming, gv as EscalateQuestion, hu as PursuitNodeProjection, it as SupervisedResult, iu as SuperviseGraphPursuitOptions, jv as ContinuityMode, kv as AuthorizedDownMessage, lf as DriveHarnessOwnerContext, lu as supervisePursuit, mu as PursuitNodePlatform, nd as RUN_FORK_CORRELATION_KEYS, nf as readRootStream, nu as VersionVerdict, ou as SupervisePursuitOptions, ov as WorkerWatchOptions, pu as PursuitNodeCost, qf as CoordinationPublicAddress, rf as readRootStreamReceipt, rt as RootStreamReceipt, ru as assertPursuitVersions, sd as AuthorizedSpawn, su as SupervisedPursuitResult, td as PursuitFork, tf as RootStreamRecord, tu as VersionJudge, uu as RunReopen, vu as PursuitProjection, wd as SeatSelection, wu as ObserverJournal, wy as AnalyzeOnSettleRoute, xu as PursuitStatus, yf as ManagerWakePolicy, yu as PursuitRunProjection, yv as QuestionEscalationOutcome } from "./types-D0ZRqIJh.js";
2
2
  import { n as RuntimeDecisionKind, r as RuntimeDecisionPoint, t as RuntimeDecisionEvidenceRef } from "./runtime-hooks-nsqxSUFL.js";
3
+ import { LockIdentity } from "@tangle-network/agent-eval/lock-holder";
3
4
  //#region src/durable/chat-engine.d.ts
4
5
  /**
5
6
  * `handleChatTurn` is a framework-neutral chat-turn HTTP orchestrator.
@@ -139,11 +140,14 @@ interface RunDirectoryLockHolder {
139
140
  /** ISO instant the holder took the lock. */
140
141
  readonly startedAt: string;
141
142
  readonly runId: string;
142
- /**
143
- * The OS's start token for `pid` when the host reports one (`readProcessStart`). A holder
144
- * without one is judged by pid liveness alone, which cannot detect a reused pid.
145
- */
143
+ /** The OS's start token for `pid` when the host reports one. A holder without one is never proved dead. */
146
144
  readonly processStart?: string;
145
+ /** Hostname of the holder. Records written before it existed are never proved dead. */
146
+ readonly host?: string;
147
+ readonly bootId?: string;
148
+ readonly pidNamespace?: string;
149
+ /** Inode identity of the lock file this record was written into. */
150
+ readonly lock?: LockIdentity;
147
151
  }
148
152
  /** A held lock. `release()` removes the file; it is safe to call more than once. */
149
153
  interface RunDirectoryLock extends RunDirectoryLockHolder {
@@ -160,10 +164,10 @@ declare class RunDirectoryLockedError extends Error {
160
164
  * Take `runDir/supervise.lock`, or refuse.
161
165
  *
162
166
  * The file is published with its full content or not at all, so a contender never reads a
163
- * half-written holder. A lock whose holder is gone (its pid no longer exists, or the pid now
164
- * belongs to a process with a different start token) is stale and is removed under the mutation guard;
165
- * a pid this process may not signal (`EPERM`) is alive and refuses. An empty file names no
166
- * holder and is reclaimed. A file with unreadable content is left in place and refused:
167
+ * half-written holder. A lock whose holder is proved dead (its pid no longer exists, the pid now
168
+ * belongs to a process with a different start token, or the host rebooted) is stale and is removed
169
+ * under the mutation guard; a holder that is alive or cannot be proved dead refuses. An empty file
170
+ * names no holder and is reclaimed. A file with unreadable content is left in place and refused:
167
171
  * reclaiming it could evict a live holder written by something other than this module.
168
172
  */
169
173
  declare function acquireRunDirectoryLock(runDir: string, runId: string, now?: () => number): Promise<RunDirectoryLock>;
@@ -171,7 +175,7 @@ declare function acquireRunDirectoryLock(runDir: string, runId: string, now?: ()
171
175
  declare function readRunDirectoryLock(runDir: string): Promise<RunDirectoryLockHolder | undefined>;
172
176
  /** What {@link runDirectoryHolderIsLive} proved about a run directory's recorded holder. */
173
177
  interface RunDirectoryHolderLiveness {
174
- /** True only while the process that took the lock is still the process that holds the pid. */
178
+ /** False only when the holder that took the lock is proved dead; a holder that cannot be proved dead reads as live. */
175
179
  readonly live: boolean;
176
180
  /** The holder the lock file names. Absent when no lock file names one, which reads as not live. */
177
181
  readonly holder?: RunDirectoryLockHolder;
package/dist/durable.js CHANGED
@@ -1,23 +1,22 @@
1
- import { a as syncDurableDirectory, i as publishExclusiveDurableFile, o as writeAtomicDurableFile } from "./durable-file-CA0kr1Pi.js";
2
- import { Bi as assertValidBudget, C as FileSpawnJournal, Mi as contentAddress, Qr as authoredProfileDigest, _t as readCommittedJsonLines, ct as loadSpawnForest, hr as isFiniteNumber, ji as detachedSnapshot, mr as failedItems, mt as isNoEntError, pt as appendCommittedJsonLine, r as assertResumeLimits } from "./supervisor-gDn4C1o5.js";
1
+ import { a as syncDurableDirectory, i as publishExclusiveDurableFile, o as writeAtomicDurableFile } from "./durable-file-DLcrzrjL.js";
2
+ import { Bi as assertValidBudget, C as FileSpawnJournal, Mi as contentAddress, Qr as authoredProfileDigest, _t as readCommittedJsonLines, ct as loadSpawnForest, hr as isFiniteNumber, ji as detachedSnapshot, mr as failedItems, mt as isNoEntError, pt as appendCommittedJsonLine, r as assertResumeLimits } from "./supervisor-D3M9ab1v.js";
3
3
  import { f as RuntimeRunStateError, m as ValidationError } from "./errors-DodWX-cb.js";
4
4
  import { W as addSpend, X as errorMessage, ft as zeroSpend, q as cloneSpend } from "./sandbox-profile-compat-_fEtHPCj.js";
5
5
  import { a as withPursuitContext, t as composeRuntimeHooks } from "./runtime-hooks-tXpAarhW.js";
6
6
  import { a as applyExactAgentProfileDiff } from "./profile-6OLpbjvV.js";
7
- import { a as superviseRootProfile, ot as readRootStream, st as readRootStreamReceipt, t as supervise } from "./supervise-DyCUu8Io.js";
8
- import { a as runCancelRequestFile, o as runCancellationFile, v as supervisorRunDir, x as workerCancellationsDir } from "./run-cancellation-DSpwvFmQ.js";
9
- import { i as superviseAgentGraph } from "./graph-B89lApkU.js";
7
+ import { a as superviseRootProfile, ot as readRootStream, st as readRootStreamReceipt, t as supervise } from "./supervise-BuVVC7Bl.js";
8
+ import { a as runCancelRequestFile, o as runCancellationFile, v as supervisorRunDir, x as workerCancellationsDir } from "./run-cancellation-CEDiqfhC.js";
9
+ import { i as superviseAgentGraph } from "./graph-kgzTaNMA.js";
10
10
  import { agentProfileSchema, canonicalAgentProfileDigest, canonicalCandidateDigest, canonicalCandidateJson, sha256Bytes } from "@tangle-network/agent-interface";
11
11
  import { isRecord } from "@tangle-network/agent-core";
12
12
  import { createHash } from "node:crypto";
13
13
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync } from "node:fs";
14
14
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
15
- import { execFile } from "node:child_process";
16
- import { access, mkdir, readFile, realpath, rm, rmdir, unlink } from "node:fs/promises";
17
- import { promisify } from "node:util";
15
+ import { access, mkdir, open, readFile, realpath, rm, rmdir, unlink } from "node:fs/promises";
18
16
  import { SearchRecorder, developmentClaim, incumbent, openSearchLedger, parseSearchLedgerLine, runSearch, uniform } from "@tangle-network/agent-eval/campaign";
19
17
  import { pathToFileURL } from "node:url";
20
18
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
19
+ import { lockHolderVerdict, lockIdentity, parseLockHolderRecord, readLockHolderIdentity } from "@tangle-network/agent-eval/lock-holder";
21
20
  //#region src/durable/chat-engine.ts
22
21
  const encoder = new TextEncoder();
23
22
  function encodeLine(event) {
@@ -2044,28 +2043,37 @@ var RunDirectoryLockedError = class extends Error {
2044
2043
  * Take `runDir/supervise.lock`, or refuse.
2045
2044
  *
2046
2045
  * The file is published with its full content or not at all, so a contender never reads a
2047
- * half-written holder. A lock whose holder is gone (its pid no longer exists, or the pid now
2048
- * belongs to a process with a different start token) is stale and is removed under the mutation guard;
2049
- * a pid this process may not signal (`EPERM`) is alive and refuses. An empty file names no
2050
- * holder and is reclaimed. A file with unreadable content is left in place and refused:
2046
+ * half-written holder. A lock whose holder is proved dead (its pid no longer exists, the pid now
2047
+ * belongs to a process with a different start token, or the host rebooted) is stale and is removed
2048
+ * under the mutation guard; a holder that is alive or cannot be proved dead refuses. An empty file
2049
+ * names no holder and is reclaimed. A file with unreadable content is left in place and refused:
2051
2050
  * reclaiming it could evict a live holder written by something other than this module.
2052
2051
  */
2053
2052
  async function acquireRunDirectoryLock(runDir, runId, now = Date.now) {
2054
2053
  const dir = resolve(runDir);
2055
2054
  const path = resolve(dir, RUN_DIRECTORY_LOCK_FILE);
2056
2055
  await mkdir(dir, { recursive: true });
2057
- const processStart = await readProcessStart(process.pid);
2058
- const holder = Object.freeze({
2059
- pid: process.pid,
2056
+ const { pid, ...identity } = readLockHolderIdentity(process.pid);
2057
+ const owner = {
2058
+ pid,
2060
2059
  startedAt: new Date(now()).toISOString(),
2061
2060
  runId,
2062
- ...processStart === void 0 ? {} : { processStart }
2063
- });
2061
+ ...identity
2062
+ };
2064
2063
  return withMutationGuard(path, async () => {
2065
2064
  const existing = await readLockFile(path);
2066
- if (existing.state === "holder" && await holderIsLive(existing.holder)) throw new RunDirectoryLockedError(path, existing.holder);
2065
+ if (existing.state === "holder" && !holderIsDead(existing)) throw new RunDirectoryLockedError(path, existing.holder);
2067
2066
  if (existing.state !== "missing") await removeIfPresent(path);
2068
- if (!publishExclusiveDurableFile(path, `${JSON.stringify(holder)}\n`)) throw new Error(`supervisePursuit: could not take ${path}`);
2067
+ const written = {};
2068
+ const published = publishExclusiveDurableFile(path, (file) => {
2069
+ written.holder = Object.freeze({
2070
+ ...owner,
2071
+ lock: lockIdentity(file)
2072
+ });
2073
+ return `${JSON.stringify(written.holder)}\n`;
2074
+ });
2075
+ const holder = written.holder;
2076
+ if (!published || holder === void 0) throw new Error(`supervisePursuit: could not take ${path}`);
2069
2077
  let released = false;
2070
2078
  return Object.freeze({
2071
2079
  ...holder,
@@ -2102,47 +2110,21 @@ async function runDirectoryHolderIsLive(runDir) {
2102
2110
  const existing = await readLockFile(resolve(runDir, RUN_DIRECTORY_LOCK_FILE));
2103
2111
  if (existing.state !== "holder") return { live: false };
2104
2112
  return {
2105
- live: await holderIsLive(existing.holder),
2113
+ live: !holderIsDead(existing),
2106
2114
  holder: existing.holder
2107
2115
  };
2108
2116
  }
2109
- const execFileAsync = promisify(execFile);
2110
- /**
2111
- * The OS's start token for a live pid: on Linux the `starttime` field of `/proc/<pid>/stat`
2112
- * (clock ticks since boot), elsewhere the `ps` `lstart` column. The same process reports the
2113
- * same token for its whole life and a later holder of the pid reports a different one.
2114
- * `undefined` when the host cannot report one: the pid is gone, there is no `/proc` and no
2115
- * `ps`, or the platform is Windows.
2116
- */
2117
- async function readProcessStart(pid) {
2118
- if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return void 0;
2119
- if (process.platform === "linux") {
2120
- let stat;
2121
- try {
2122
- stat = await readFile(`/proc/${pid}/stat`, "utf8");
2123
- } catch {
2124
- return;
2125
- }
2126
- const startTime = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/u)[19];
2127
- return startTime !== void 0 && /^\d+$/u.test(startTime) ? startTime : void 0;
2128
- }
2129
- try {
2130
- const { stdout } = await execFileAsync("ps", [
2131
- "-o",
2132
- "lstart=",
2133
- "-p",
2134
- String(pid)
2135
- ]);
2136
- const token = stdout.trim();
2137
- return token.length === 0 ? void 0 : token;
2138
- } catch {
2139
- return;
2140
- }
2141
- }
2142
2117
  async function readLockFile(path) {
2143
2118
  let text;
2119
+ let current;
2144
2120
  try {
2145
- text = await readFile(path, "utf8");
2121
+ const file = await open(path, "r");
2122
+ try {
2123
+ current = lockIdentity(await file.stat({ bigint: true }));
2124
+ text = await file.readFile("utf8");
2125
+ } finally {
2126
+ await file.close();
2127
+ }
2146
2128
  } catch (error) {
2147
2129
  if (isNoEntError(error)) return { state: "missing" };
2148
2130
  throw error;
@@ -2157,30 +2139,24 @@ async function readLockFile(path) {
2157
2139
  const record = parsed;
2158
2140
  if (typeof record !== "object" || record === null || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0 || typeof record.startedAt !== "string" || typeof record.runId !== "string" || record.processStart !== void 0 && typeof record.processStart !== "string") throw new Error(`supervisePursuit: ${path} is not a readable lock file; remove it by hand`);
2159
2141
  const { pid, startedAt, runId, processStart } = record;
2142
+ const verdictRecord = parseLockHolderRecord(parsed);
2143
+ const { pid: _pid, ...identity } = verdictRecord ?? { pid };
2160
2144
  return {
2161
2145
  state: "holder",
2162
2146
  holder: Object.freeze({
2163
2147
  pid,
2164
2148
  startedAt,
2165
2149
  runId,
2166
- ...processStart === void 0 ? {} : { processStart }
2167
- })
2150
+ ...processStart === void 0 ? {} : { processStart },
2151
+ ...identity
2152
+ }),
2153
+ record: verdictRecord,
2154
+ current
2168
2155
  };
2169
2156
  }
2170
- async function holderIsLive(holder) {
2171
- if (!processExists(holder.pid)) return false;
2172
- if (holder.processStart === void 0) return true;
2173
- const current = await readProcessStart(holder.pid);
2174
- return current === void 0 || current === holder.processStart;
2175
- }
2176
- function processExists(pid) {
2177
- if (!Number.isInteger(pid) || pid <= 0) return false;
2178
- try {
2179
- process.kill(pid, 0);
2180
- return true;
2181
- } catch (error) {
2182
- return error.code !== "ESRCH";
2183
- }
2157
+ /** agent-eval's dead-owner rule: only a holder proved dead is stale; alive or unknown stays held. */
2158
+ function holderIsDead(existing) {
2159
+ return lockHolderVerdict(existing.record, existing.current).liveness === "dead";
2184
2160
  }
2185
2161
  /** Remove the lock only while it still names this holder; a reclaimed lock belongs to its new owner. */
2186
2162
  async function releaseHolder(path, holder) {