@runtypelabs/sdk 4.10.0 → 4.11.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.
package/dist/index.cjs CHANGED
@@ -38,6 +38,8 @@ __export(index_exports, {
38
38
  ClientTokensEndpoint: () => ClientTokensEndpoint,
39
39
  ContextTemplatesEndpoint: () => ContextTemplatesEndpoint,
40
40
  ConversationsEndpoint: () => ConversationsEndpoint,
41
+ DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS: () => DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS,
42
+ DEFAULT_STALL_STOP_AFTER: () => DEFAULT_STALL_STOP_AFTER,
41
43
  DispatchEndpoint: () => DispatchEndpoint,
42
44
  EvalBuilder: () => EvalBuilder,
43
45
  EvalEndpoint: () => EvalEndpoint,
@@ -75,25 +77,34 @@ __export(index_exports, {
75
77
  UsersEndpoint: () => UsersEndpoint,
76
78
  applyGeneratedRuntimeToolProposalToDispatchRequest: () => applyGeneratedRuntimeToolProposalToDispatchRequest,
77
79
  attachRuntimeToolsToDispatchRequest: () => attachRuntimeToolsToDispatchRequest,
80
+ buildEmptySessionNudge: () => buildEmptySessionNudge,
78
81
  buildGeneratedRuntimeToolGateOutput: () => buildGeneratedRuntimeToolGateOutput,
79
82
  buildLedgerOffloadReference: () => buildLedgerOffloadReference,
83
+ buildPolicyGuidance: () => buildPolicyGuidance,
80
84
  buildSendViewOffloadMarker: () => buildSendViewOffloadMarker,
85
+ compileWorkflowConfig: () => compileWorkflowConfig,
81
86
  computeAgentContentHash: () => computeAgentContentHash,
82
87
  computeFlowContentHash: () => computeFlowContentHash,
83
88
  createClient: () => createClient,
84
89
  createExternalTool: () => createExternalTool,
85
90
  defaultWorkflow: () => defaultWorkflow,
91
+ defaultWorkflowConfig: () => defaultWorkflowConfig,
86
92
  defineAgent: () => defineAgent,
87
93
  defineFlow: () => defineFlow,
94
+ definePlaybook: () => definePlaybook,
88
95
  deployWorkflow: () => deployWorkflow,
96
+ ensureDefaultWorkflowHooks: () => ensureDefaultWorkflowHooks,
89
97
  evaluateGeneratedRuntimeToolProposal: () => evaluateGeneratedRuntimeToolProposal,
90
98
  extractDeclaredToolResultChars: () => extractDeclaredToolResultChars,
91
99
  gameWorkflow: () => gameWorkflow,
92
100
  getDefaultPlanPath: () => getDefaultPlanPath,
93
101
  getLikelySupportingCandidatePaths: () => getLikelySupportingCandidatePaths,
102
+ interpolateWorkflowTemplate: () => interpolateWorkflowTemplate,
94
103
  isDiscoveryToolName: () => isDiscoveryToolName,
95
104
  isMarathonArtifactPath: () => isMarathonArtifactPath,
96
105
  isPreservationSensitiveTask: () => isPreservationSensitiveTask,
106
+ isWorkflowHookRef: () => isWorkflowHookRef,
107
+ listWorkflowHooks: () => listWorkflowHooks,
97
108
  normalizeAgentDefinition: () => normalizeAgentDefinition,
98
109
  normalizeCandidatePath: () => normalizeCandidatePath,
99
110
  parseFinalBuffer: () => parseFinalBuffer,
@@ -101,8 +112,14 @@ __export(index_exports, {
101
112
  parseOffloadedOutputId: () => parseOffloadedOutputId,
102
113
  parseSSEChunk: () => parseSSEChunk,
103
114
  processStream: () => processStream,
115
+ registerWorkflowHook: () => registerWorkflowHook,
116
+ resolveStallStopAfter: () => resolveStallStopAfter,
117
+ resolveWorkflowHook: () => resolveWorkflowHook,
104
118
  sanitizeTaskSlug: () => sanitizeTaskSlug,
105
- streamEvents: () => streamEvents
119
+ shouldInjectEmptySessionNudge: () => shouldInjectEmptySessionNudge,
120
+ shouldRequestModelEscalation: () => shouldRequestModelEscalation,
121
+ streamEvents: () => streamEvents,
122
+ unregisterWorkflowHook: () => unregisterWorkflowHook
106
123
  });
107
124
  module.exports = __toCommonJS(index_exports);
108
125
 
@@ -4418,6 +4435,261 @@ function sanitizeTaskSlug(taskName) {
4418
4435
  return taskName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
4419
4436
  }
4420
4437
 
4438
+ // src/workflows/hook-registry.ts
4439
+ var BUILTIN_NAMESPACE = "builtin";
4440
+ var HOOK_REF_PATTERN = /^[a-z0-9_-]+:[a-z0-9_-]+$/;
4441
+ function isWorkflowHookRef(value) {
4442
+ return typeof value === "string" && HOOK_REF_PATTERN.test(value);
4443
+ }
4444
+ var registry = /* @__PURE__ */ new Map();
4445
+ function registerWorkflowHook(name, entry) {
4446
+ if (!isWorkflowHookRef(name)) {
4447
+ throw new Error(
4448
+ `Invalid workflow hook name "${name}": must be "<namespace>:<id>" using lowercase letters, digits, "-" or "_" (e.g. "acme:my-completion").`
4449
+ );
4450
+ }
4451
+ if (name.startsWith(`${BUILTIN_NAMESPACE}:`)) {
4452
+ throw new Error(
4453
+ `Cannot register "${name}": the "builtin:" namespace is reserved. Register under your own namespace and reference it from the workflow config instead.`
4454
+ );
4455
+ }
4456
+ registry.set(name, entry);
4457
+ }
4458
+ function registerBuiltinWorkflowHook(name, entry) {
4459
+ if (!name.startsWith(`${BUILTIN_NAMESPACE}:`) || !isWorkflowHookRef(name)) {
4460
+ throw new Error(`Builtin workflow hooks must be named "builtin:<id>" (got "${name}").`);
4461
+ }
4462
+ if (registry.has(name)) return;
4463
+ registry.set(name, entry);
4464
+ }
4465
+ function resolveWorkflowHook(name, expectedKind) {
4466
+ const entry = registry.get(name);
4467
+ if (!entry) {
4468
+ const known = listWorkflowHooks().filter((hook) => hook.kind === expectedKind).map((hook) => hook.name);
4469
+ throw new Error(
4470
+ `Unknown workflow hook "${name}". ` + (known.length > 0 ? `Registered '${expectedKind}' hooks: ${known.join(", ")}.` : `No '${expectedKind}' hooks are registered.`) + " Custom hooks must be registered (e.g. via a playbook plugin) before the workflow is compiled."
4471
+ );
4472
+ }
4473
+ if (entry.kind !== expectedKind) {
4474
+ throw new Error(
4475
+ `Workflow hook "${name}" is registered as '${entry.kind}' but referenced from a '${expectedKind}' slot.`
4476
+ );
4477
+ }
4478
+ return entry.fn;
4479
+ }
4480
+ function listWorkflowHooks() {
4481
+ return [...registry.entries()].map(([name, entry]) => ({ name, kind: entry.kind }));
4482
+ }
4483
+ function unregisterWorkflowHook(name) {
4484
+ if (name.startsWith(`${BUILTIN_NAMESPACE}:`)) return false;
4485
+ return registry.delete(name);
4486
+ }
4487
+
4488
+ // src/workflows/workflow-config.ts
4489
+ var DISCOVERY_TOOLS = /* @__PURE__ */ new Set([
4490
+ "search_repo",
4491
+ "glob_files",
4492
+ "tree_directory",
4493
+ "list_directory"
4494
+ ]);
4495
+ var DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS = 2;
4496
+ function definePlaybook(playbook) {
4497
+ return playbook;
4498
+ }
4499
+ function interpolateWorkflowTemplate(template, state) {
4500
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
4501
+ const value = state[key];
4502
+ if (value === void 0 || value === null) return `{{${key}}}`;
4503
+ return String(value);
4504
+ });
4505
+ }
4506
+ function buildIsComplete(criteria, configName, milestoneName) {
4507
+ if (!criteria) return () => false;
4508
+ switch (criteria.type) {
4509
+ case "evidence":
4510
+ return (ctx) => {
4511
+ const minFiles = criteria.minReadFiles ?? 1;
4512
+ return (ctx.state.recentReadPaths?.length ?? 0) >= minFiles;
4513
+ };
4514
+ case "sessions": {
4515
+ let baselineSessionCount;
4516
+ return (ctx) => {
4517
+ const minSessions = criteria.minSessions ?? 1;
4518
+ if (baselineSessionCount === void 0) {
4519
+ baselineSessionCount = ctx.state.sessions.length;
4520
+ }
4521
+ return ctx.state.sessions.length - baselineSessionCount >= minSessions;
4522
+ };
4523
+ }
4524
+ case "planWritten":
4525
+ return (ctx) => {
4526
+ return ctx.trace.planWritten;
4527
+ };
4528
+ case "never":
4529
+ return () => false;
4530
+ default: {
4531
+ if (isWorkflowHookRef(criteria.type)) {
4532
+ return resolveWorkflowHook(criteria.type, "completion");
4533
+ }
4534
+ throw new Error(
4535
+ `Workflow config '${configName}': milestone '${milestoneName}' has unknown completionCriteria.type "${criteria.type}" (expected evidence | sessions | planWritten | never, or a 'completion' hook reference).`
4536
+ );
4537
+ }
4538
+ }
4539
+ }
4540
+ function buildPolicyIntercept(policy, configName, deps) {
4541
+ if (!policy.blockedTools?.length && !policy.blockDiscoveryTools && !policy.allowedReadGlobs?.length && !policy.allowedWriteGlobs?.length && !policy.requirePlanBeforeWrite) {
4542
+ return void 0;
4543
+ }
4544
+ const blockedSet = new Set(
4545
+ (policy.blockedTools ?? []).map((t) => t.trim()).filter(Boolean)
4546
+ );
4547
+ const readGlobs = policy.allowedReadGlobs ?? [];
4548
+ const writeGlobs = policy.allowedWriteGlobs ?? [];
4549
+ const matchPathGlobs = deps.matchPathGlobs;
4550
+ if ((readGlobs.length > 0 || writeGlobs.length > 0) && !matchPathGlobs) {
4551
+ throw new Error(
4552
+ `Workflow config '${configName}': policy uses allowedReadGlobs/allowedWriteGlobs but no glob matcher was provided to compileWorkflowConfig (pass deps.matchPathGlobs).`
4553
+ );
4554
+ }
4555
+ return (toolName, args, ctx) => {
4556
+ if (blockedSet.has(toolName)) {
4557
+ return `Blocked by playbook policy: ${toolName} is not allowed for this task.`;
4558
+ }
4559
+ if (policy.blockDiscoveryTools && DISCOVERY_TOOLS.has(toolName)) {
4560
+ return `Blocked by playbook policy: discovery tools are disabled for this task.`;
4561
+ }
4562
+ const pathArg = typeof args.path === "string" && args.path.trim() ? ctx.normalizePath(String(args.path)) : void 0;
4563
+ if (pathArg) {
4564
+ const isWrite = toolName === "write_file" || toolName === "restore_file_checkpoint";
4565
+ const isRead = toolName === "read_file";
4566
+ if (isRead && readGlobs.length > 0) {
4567
+ const allowed = matchPathGlobs(pathArg, readGlobs);
4568
+ if (!allowed) {
4569
+ return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed read globs: ${readGlobs.join(", ")}`;
4570
+ }
4571
+ }
4572
+ if (isWrite && writeGlobs.length > 0) {
4573
+ const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4574
+ if (planPath && pathArg === planPath) {
4575
+ } else {
4576
+ const allowed = matchPathGlobs(pathArg, writeGlobs);
4577
+ if (!allowed) {
4578
+ return `Blocked by playbook policy: ${toolName} path "${pathArg}" is outside allowed write globs: ${writeGlobs.join(", ")}`;
4579
+ }
4580
+ }
4581
+ }
4582
+ if (isWrite && policy.requirePlanBeforeWrite && !ctx.state.planWritten && !ctx.trace.planWritten) {
4583
+ const planPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4584
+ if (!planPath || pathArg !== planPath) {
4585
+ return `Blocked by playbook policy: write the plan before creating other files.`;
4586
+ }
4587
+ }
4588
+ }
4589
+ return void 0;
4590
+ };
4591
+ }
4592
+ function buildPolicyGuidance(policy) {
4593
+ if (!policy) return [];
4594
+ const lines = [];
4595
+ if (policy.requirePlanBeforeWrite) {
4596
+ lines.push(
4597
+ "Policy: write the plan file before any other file. Once the plan is written, other writes are allowed in the same turn."
4598
+ );
4599
+ }
4600
+ if (policy.allowedWriteGlobs?.length) {
4601
+ lines.push(
4602
+ `Policy: file writes are only allowed for paths matching: ${policy.allowedWriteGlobs.join(", ")} (the plan file is always allowed).`
4603
+ );
4604
+ }
4605
+ if (policy.outputRoot) {
4606
+ lines.push(`Policy: create new files under "${policy.outputRoot.replace(/\/$/, "")}/".`);
4607
+ }
4608
+ if (policy.allowedReadGlobs?.length) {
4609
+ lines.push(
4610
+ `Policy: file reads are only allowed for paths matching: ${policy.allowedReadGlobs.join(", ")}.`
4611
+ );
4612
+ }
4613
+ if (policy.blockDiscoveryTools) {
4614
+ lines.push(
4615
+ "Policy: broad discovery tools (search_repo, glob_files, tree_directory, list_directory) are disabled for this task."
4616
+ );
4617
+ }
4618
+ if (policy.blockedTools?.length) {
4619
+ lines.push(`Policy: these tools are disabled for this task: ${policy.blockedTools.join(", ")}.`);
4620
+ }
4621
+ return lines;
4622
+ }
4623
+ function resolveSlotHook(value, kind) {
4624
+ if (value === void 0) return void 0;
4625
+ if (typeof value === "function") return value;
4626
+ return resolveWorkflowHook(value, kind);
4627
+ }
4628
+ function compileMilestone(milestone, config, policyIntercept, policyGuidance) {
4629
+ const buildInstructions = typeof milestone.instructions === "function" ? milestone.instructions : isWorkflowHookRef(milestone.instructions) ? resolveWorkflowHook(milestone.instructions, "instructions") : (state) => {
4630
+ const header = `--- Workflow Phase: ${milestone.name} ---`;
4631
+ const desc = milestone.description ? `
4632
+ ${milestone.description}` : "";
4633
+ const instructions = interpolateWorkflowTemplate(
4634
+ milestone.instructions,
4635
+ state
4636
+ );
4637
+ return `${header}${desc}
4638
+ ${instructions}`;
4639
+ };
4640
+ const guidanceHook = typeof milestone.toolGuidance === "function" ? milestone.toolGuidance : milestone.toolGuidance !== void 0 && isWorkflowHookRef(milestone.toolGuidance) ? resolveWorkflowHook(milestone.toolGuidance, "toolGuidance") : void 0;
4641
+ const buildToolGuidance = (state) => {
4642
+ const base = guidanceHook ? guidanceHook(state) : milestone.toolGuidance ?? [];
4643
+ return policyGuidance.length > 0 ? [...base, ...policyGuidance] : base;
4644
+ };
4645
+ const customIntercept = resolveSlotHook(milestone.intercept, "intercept");
4646
+ const interceptToolCall = policyIntercept && customIntercept ? (toolName, args, ctx) => policyIntercept(toolName, args, ctx) ?? customIntercept(toolName, args, ctx) : policyIntercept ?? customIntercept;
4647
+ const transitionHook = typeof milestone.transitionSummary === "function" ? milestone.transitionSummary : milestone.transitionSummary !== void 0 && isWorkflowHookRef(milestone.transitionSummary) ? resolveWorkflowHook(milestone.transitionSummary, "transitionSummary") : void 0;
4648
+ const buildTransitionSummary = milestone.transitionSummary === void 0 ? void 0 : transitionHook ?? ((state, nextPhaseName) => interpolateWorkflowTemplate(milestone.transitionSummary, state).replace(
4649
+ /\{\{nextPhase\}\}/g,
4650
+ nextPhaseName
4651
+ ));
4652
+ const recoveryHook = typeof milestone.recovery === "function" ? milestone.recovery : milestone.recovery !== void 0 && isWorkflowHookRef(milestone.recovery) ? resolveWorkflowHook(milestone.recovery, "recovery") : void 0;
4653
+ const buildRecoveryMessage = milestone.recovery === void 0 ? void 0 : recoveryHook ?? ((state) => {
4654
+ const inline = milestone.recovery;
4655
+ const threshold = inline.afterEmptySessions ?? DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS;
4656
+ if ((state.consecutiveEmptySessions ?? 0) < threshold) return void 0;
4657
+ return interpolateWorkflowTemplate(inline.message, state);
4658
+ });
4659
+ const canAcceptCompletion = milestone.canAcceptCompletion === void 0 ? void 0 : typeof milestone.canAcceptCompletion === "function" ? milestone.canAcceptCompletion : isWorkflowHookRef(milestone.canAcceptCompletion) ? resolveWorkflowHook(milestone.canAcceptCompletion, "acceptCompletion") : () => milestone.canAcceptCompletion;
4660
+ const isComplete = typeof milestone.completionCriteria === "function" ? milestone.completionCriteria : buildIsComplete(milestone.completionCriteria, config.name, milestone.name);
4661
+ return {
4662
+ name: milestone.name,
4663
+ description: milestone.description,
4664
+ buildInstructions,
4665
+ buildToolGuidance,
4666
+ isComplete,
4667
+ ...interceptToolCall ? { interceptToolCall } : {},
4668
+ ...buildTransitionSummary ? { buildTransitionSummary } : {},
4669
+ ...buildRecoveryMessage ? { buildRecoveryMessage } : {},
4670
+ ...milestone.forceEndTurn ? { shouldForceEndTurn: resolveSlotHook(milestone.forceEndTurn, "forceEndTurn") } : {},
4671
+ ...canAcceptCompletion ? { canAcceptCompletion } : {}
4672
+ };
4673
+ }
4674
+ function compileWorkflowConfig(config, deps = {}) {
4675
+ const policyIntercept = config.policy ? buildPolicyIntercept(config.policy, config.name, deps) : void 0;
4676
+ const policyGuidance = buildPolicyGuidance(config.policy);
4677
+ const phases = config.milestones.map(
4678
+ (milestone) => compileMilestone(milestone, config, policyIntercept, policyGuidance)
4679
+ );
4680
+ const classifyVariant4 = resolveSlotHook(config.classifyVariant, "classify");
4681
+ const generateBootstrapContext2 = resolveSlotHook(config.bootstrap, "bootstrap");
4682
+ const buildCandidateBlock2 = resolveSlotHook(config.candidateBlock, "candidateBlock");
4683
+ return {
4684
+ name: config.name,
4685
+ phases,
4686
+ ...config.stallPolicy ? { stallPolicy: config.stallPolicy } : {},
4687
+ ...classifyVariant4 ? { classifyVariant: classifyVariant4 } : {},
4688
+ ...generateBootstrapContext2 ? { generateBootstrapContext: generateBootstrapContext2 } : {},
4689
+ ...buildCandidateBlock2 ? { buildCandidateBlock: buildCandidateBlock2 } : {}
4690
+ };
4691
+ }
4692
+
4421
4693
  // src/workflows/default-workflow.ts
4422
4694
  function isExternalTask(state) {
4423
4695
  return state.workflowVariant === "external";
@@ -4567,6 +4839,45 @@ function summarizeTextBlock(value, maxLines = 4) {
4567
4839
  if (!text) return "";
4568
4840
  return text.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, maxLines).join(" | ").slice(0, 240);
4569
4841
  }
4842
+ function interceptProductWriteTarget(toolName, normalizedPathArg, ctx, guardLabel) {
4843
+ const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4844
+ const normalizedBestCandidatePath = ctx.state.bestCandidatePath ? ctx.normalizePath(ctx.state.bestCandidatePath) : void 0;
4845
+ if (!ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4846
+ const allowedWriteTargets = new Set(
4847
+ [
4848
+ normalizedPlanPath,
4849
+ normalizedBestCandidatePath,
4850
+ ...(ctx.state.recentReadPaths || []).map((readPath) => ctx.normalizePath(readPath)),
4851
+ ...ctx.trace.readPaths.map((readPath) => ctx.normalizePath(readPath))
4852
+ ].filter((value) => Boolean(value))
4853
+ );
4854
+ if (!allowedWriteTargets.has(normalizedPathArg)) {
4855
+ return [
4856
+ `Blocked by marathon ${guardLabel}: ${toolName} is limited to the confirmed target, the plan file, or files already discovered/read for this task.`,
4857
+ `Do not create scratch files like "${normalizedPathArg}".`,
4858
+ normalizedBestCandidatePath ? `Edit "${normalizedBestCandidatePath}" or another previously discovered repo file instead.` : "Read the current target file before writing."
4859
+ ].join(" ");
4860
+ }
4861
+ }
4862
+ if (ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4863
+ const outputRoot = ctx.state.outputRoot ? ctx.state.outputRoot.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "") || void 0 : void 0;
4864
+ if (!outputRoot) {
4865
+ return [
4866
+ `Blocked by marathon ${guardLabel}: creation tasks require outputRoot. Writes outside the plan are not allowed.`,
4867
+ `Plan path: "${normalizedPlanPath}". Create files only under the configured output root.`
4868
+ ].join(" ");
4869
+ }
4870
+ const rootPrefix = outputRoot + "/";
4871
+ const isUnderRoot = normalizedPathArg === outputRoot || normalizedPathArg.startsWith(rootPrefix);
4872
+ if (!isUnderRoot) {
4873
+ return [
4874
+ `Blocked by marathon ${guardLabel}: ${toolName} must target the plan or paths under outputRoot "${outputRoot}/".`,
4875
+ `"${normalizedPathArg}" is outside the allowed output root.`
4876
+ ].join(" ");
4877
+ }
4878
+ }
4879
+ return void 0;
4880
+ }
4570
4881
  var researchPhase = {
4571
4882
  name: "research",
4572
4883
  description: "Inspect the repo and identify the correct target file",
@@ -4651,11 +4962,15 @@ var researchPhase = {
4651
4962
  const normalizedPathArg2 = typeof _args.path === "string" && _args.path.trim() ? ctx.normalizePath(String(_args.path)) : void 0;
4652
4963
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4653
4964
  if (normalizedPathArg2 && normalizedPlanPath && normalizedPathArg2 !== normalizedPlanPath) {
4654
- return [
4655
- `Blocked by marathon research guard: ${toolName} cannot create product files during the research phase.`,
4656
- "Complete research first, then the system will advance you to planning.",
4657
- `You may write the plan to "${normalizedPlanPath}" once research is complete.`
4658
- ].join(" ");
4965
+ const planWritten = ctx.trace.planWritten || Boolean(ctx.state.planWritten);
4966
+ if (!planWritten) {
4967
+ return [
4968
+ `Blocked by marathon research guard: ${toolName} cannot create product files during the research phase.`,
4969
+ "Complete research first, then the system will advance you to planning.",
4970
+ `You may write the plan to "${normalizedPlanPath}" once research is complete.`
4971
+ ].join(" ");
4972
+ }
4973
+ return interceptProductWriteTarget(toolName, normalizedPathArg2, ctx, "research guard");
4659
4974
  }
4660
4975
  }
4661
4976
  return void 0;
@@ -4778,19 +5093,24 @@ var planningPhase = {
4778
5093
  "Research is complete. Write the implementation plan for building this from scratch.",
4779
5094
  `Write the plan markdown to exactly: ${planPath}`,
4780
5095
  "List the files you will create, their locations, purpose, and any dependencies to install.",
5096
+ ...state.outputRoot ? [
5097
+ `All new files must be created under "${state.outputRoot}" \u2014 writes outside that directory are blocked, so plan every file location inside it.`
5098
+ ] : [],
4781
5099
  'Include a "Verification steps" section listing the concrete checks you will run before TASK_COMPLETE.',
4782
- "If the plan already exists, update that same plan file instead of creating a different one."
5100
+ "If the plan already exists, update that same plan file instead of creating a different one.",
5101
+ "Once the plan is written, you may begin creating the planned files in the same turn."
4783
5102
  ].join("\n");
4784
5103
  }
4785
5104
  return [
4786
5105
  "--- Workflow Phase: Planning ---",
4787
5106
  "Research is complete. Your current job is to write the implementation plan before any product-file edits.",
4788
5107
  `Write the plan markdown to exactly: ${planPath}`,
4789
- "Do NOT edit the target product file yet.",
5108
+ "Do NOT edit the target product file before the plan exists.",
4790
5109
  "The plan should summarize UX findings, explain why the current best candidate is the right file, and list concrete execution steps.",
4791
5110
  'The plan must include a "Preserve existing functionality" section that lists current behaviors, linked files, integrations, and constraints that must keep working.',
4792
5111
  'The plan must include a "Verification steps" section listing the concrete checks you will run before TASK_COMPLETE.',
4793
- "If the plan already exists, update that same plan file instead of creating a different one."
5112
+ "If the plan already exists, update that same plan file instead of creating a different one.",
5113
+ "Once the plan is written, you may begin editing the target file in the same turn."
4794
5114
  ].join("\n");
4795
5115
  },
4796
5116
  buildToolGuidance(state) {
@@ -4818,10 +5138,14 @@ var planningPhase = {
4818
5138
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
4819
5139
  const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
4820
5140
  if (isWriteLikeTool && normalizedPathArg && normalizedPlanPath && normalizedPathArg !== normalizedPlanPath) {
4821
- return [
4822
- `Blocked by marathon planning guard: ${toolName} must target the exact plan path during planning.`,
4823
- `Write the plan to "${normalizedPlanPath}" before editing any product files.`
4824
- ].join(" ");
5141
+ const planWritten = ctx.trace.planWritten || Boolean(ctx.state.planWritten);
5142
+ if (!planWritten) {
5143
+ return [
5144
+ `Blocked by marathon planning guard: ${toolName} must target the exact plan path during planning.`,
5145
+ `Write the plan to "${normalizedPlanPath}" before editing any product files.`
5146
+ ].join(" ");
5147
+ }
5148
+ return interceptProductWriteTarget(toolName, normalizedPathArg, ctx, "planning guard");
4825
5149
  }
4826
5150
  return void 0;
4827
5151
  },
@@ -4881,6 +5205,9 @@ var executionPhase = {
4881
5205
  },
4882
5206
  buildToolGuidance(state) {
4883
5207
  return [
5208
+ ...state.isCreationTask && state.outputRoot ? [
5209
+ `Creation guard: create new files under "${state.outputRoot}". Writes outside it are blocked \u2014 the plan file is the only exception.`
5210
+ ] : [],
4884
5211
  ...state.bestCandidatePath ? [
4885
5212
  `Execution-phase guard: broad discovery tools (search_repo, glob_files, tree_directory, list_directory) are locked while executing against "${state.bestCandidatePath}".`
4886
5213
  ] : [
@@ -4922,40 +5249,13 @@ var executionPhase = {
4922
5249
  `After that, you may update "${normalizedPlanPath}" with progress.`
4923
5250
  ].join(" ");
4924
5251
  }
4925
- if (!ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4926
- const allowedWriteTargets = new Set(
4927
- [
4928
- normalizedPlanPath,
4929
- normalizedBestCandidatePath,
4930
- ...(ctx.state.recentReadPaths || []).map((readPath) => ctx.normalizePath(readPath)),
4931
- ...ctx.trace.readPaths.map((readPath) => ctx.normalizePath(readPath))
4932
- ].filter((value) => Boolean(value))
4933
- );
4934
- if (!allowedWriteTargets.has(normalizedPathArg)) {
4935
- return [
4936
- `Blocked by marathon execution guard: ${toolName} is limited to the confirmed target, the plan file, or files already discovered/read for this task.`,
4937
- `Do not create scratch files like "${normalizedPathArg}".`,
4938
- normalizedBestCandidatePath ? `Edit "${normalizedBestCandidatePath}" or another previously discovered repo file instead.` : "Read the current target file before writing."
4939
- ].join(" ");
4940
- }
4941
- }
4942
- if (ctx.state.isCreationTask && normalizedPathArg && normalizedPathArg !== normalizedPlanPath) {
4943
- const outputRoot = ctx.state.outputRoot ? ctx.state.outputRoot.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "") || void 0 : void 0;
4944
- if (!outputRoot) {
4945
- return [
4946
- `Blocked by marathon execution guard: creation tasks require outputRoot. Writes outside the plan are not allowed.`,
4947
- `Plan path: "${normalizedPlanPath}". Create files only under the configured output root.`
4948
- ].join(" ");
4949
- }
4950
- const rootPrefix = outputRoot + "/";
4951
- const isUnderRoot = normalizedPathArg === outputRoot || normalizedPathArg.startsWith(rootPrefix);
4952
- if (!isUnderRoot) {
4953
- return [
4954
- `Blocked by marathon execution guard: ${toolName} must target the plan or paths under outputRoot "${outputRoot}/".`,
4955
- `"${normalizedPathArg}" is outside the allowed output root.`
4956
- ].join(" ");
4957
- }
4958
- }
5252
+ const writeTargetBlock = interceptProductWriteTarget(
5253
+ toolName,
5254
+ normalizedPathArg,
5255
+ ctx,
5256
+ "execution guard"
5257
+ );
5258
+ if (writeTargetBlock) return writeTargetBlock;
4959
5259
  }
4960
5260
  return void 0;
4961
5261
  },
@@ -5188,13 +5488,163 @@ function buildCandidateBlock(state) {
5188
5488
  ...state.bestCandidateReason ? [`Why: ${state.bestCandidateReason}`] : []
5189
5489
  ].join("\n");
5190
5490
  }
5191
- var defaultWorkflow = {
5491
+ var builtinHooksRegistered = false;
5492
+ function ensureDefaultWorkflowHooks() {
5493
+ if (builtinHooksRegistered) return;
5494
+ builtinHooksRegistered = true;
5495
+ registerBuiltinWorkflowHook("builtin:classify-task-variant", {
5496
+ kind: "classify",
5497
+ fn: classifyVariant
5498
+ });
5499
+ registerBuiltinWorkflowHook("builtin:repo-bootstrap-discovery", {
5500
+ kind: "bootstrap",
5501
+ fn: generateBootstrapContext
5502
+ });
5503
+ registerBuiltinWorkflowHook("builtin:best-candidate-block", {
5504
+ kind: "candidateBlock",
5505
+ fn: buildCandidateBlock
5506
+ });
5507
+ registerBuiltinWorkflowHook("builtin:research-instructions", {
5508
+ kind: "instructions",
5509
+ fn: researchPhase.buildInstructions
5510
+ });
5511
+ registerBuiltinWorkflowHook("builtin:research-tool-guidance", {
5512
+ kind: "toolGuidance",
5513
+ fn: researchPhase.buildToolGuidance
5514
+ });
5515
+ registerBuiltinWorkflowHook("builtin:research-complete", {
5516
+ kind: "completion",
5517
+ fn: researchPhase.isComplete
5518
+ });
5519
+ registerBuiltinWorkflowHook("builtin:research-transition-summary", {
5520
+ kind: "transitionSummary",
5521
+ fn: researchPhase.buildTransitionSummary
5522
+ });
5523
+ registerBuiltinWorkflowHook("builtin:research-guard", {
5524
+ kind: "intercept",
5525
+ fn: researchPhase.interceptToolCall
5526
+ });
5527
+ registerBuiltinWorkflowHook("builtin:research-recovery", {
5528
+ kind: "recovery",
5529
+ fn: researchPhase.buildRecoveryMessage
5530
+ });
5531
+ registerBuiltinWorkflowHook("builtin:research-force-end-turn", {
5532
+ kind: "forceEndTurn",
5533
+ fn: researchPhase.shouldForceEndTurn
5534
+ });
5535
+ registerBuiltinWorkflowHook("builtin:research-accept-completion", {
5536
+ kind: "acceptCompletion",
5537
+ fn: researchPhase.canAcceptCompletion
5538
+ });
5539
+ registerBuiltinWorkflowHook("builtin:planning-instructions", {
5540
+ kind: "instructions",
5541
+ fn: planningPhase.buildInstructions
5542
+ });
5543
+ registerBuiltinWorkflowHook("builtin:planning-tool-guidance", {
5544
+ kind: "toolGuidance",
5545
+ fn: planningPhase.buildToolGuidance
5546
+ });
5547
+ registerBuiltinWorkflowHook("builtin:planning-complete", {
5548
+ kind: "completion",
5549
+ fn: planningPhase.isComplete
5550
+ });
5551
+ registerBuiltinWorkflowHook("builtin:planning-transition-summary", {
5552
+ kind: "transitionSummary",
5553
+ fn: planningPhase.buildTransitionSummary
5554
+ });
5555
+ registerBuiltinWorkflowHook("builtin:planning-guard", {
5556
+ kind: "intercept",
5557
+ fn: planningPhase.interceptToolCall
5558
+ });
5559
+ registerBuiltinWorkflowHook("builtin:planning-recovery", {
5560
+ kind: "recovery",
5561
+ fn: planningPhase.buildRecoveryMessage
5562
+ });
5563
+ registerBuiltinWorkflowHook("builtin:planning-force-end-turn", {
5564
+ kind: "forceEndTurn",
5565
+ fn: planningPhase.shouldForceEndTurn
5566
+ });
5567
+ registerBuiltinWorkflowHook("builtin:execution-instructions", {
5568
+ kind: "instructions",
5569
+ fn: executionPhase.buildInstructions
5570
+ });
5571
+ registerBuiltinWorkflowHook("builtin:execution-tool-guidance", {
5572
+ kind: "toolGuidance",
5573
+ fn: executionPhase.buildToolGuidance
5574
+ });
5575
+ registerBuiltinWorkflowHook("builtin:execution-guard", {
5576
+ kind: "intercept",
5577
+ fn: executionPhase.interceptToolCall
5578
+ });
5579
+ registerBuiltinWorkflowHook("builtin:execution-recovery", {
5580
+ kind: "recovery",
5581
+ fn: executionPhase.buildRecoveryMessage
5582
+ });
5583
+ registerBuiltinWorkflowHook("builtin:execution-force-end-turn", {
5584
+ kind: "forceEndTurn",
5585
+ fn: executionPhase.shouldForceEndTurn
5586
+ });
5587
+ registerBuiltinWorkflowHook("builtin:execution-accept-completion", {
5588
+ kind: "acceptCompletion",
5589
+ fn: executionPhase.canAcceptCompletion
5590
+ });
5591
+ }
5592
+ var defaultWorkflowConfig = {
5192
5593
  name: "default",
5193
- phases: [researchPhase, planningPhase, executionPhase],
5194
- classifyVariant,
5195
- generateBootstrapContext,
5196
- buildCandidateBlock
5594
+ // Empty-session escalation. The counter only counts tool actions, so
5595
+ // narration-only sessions ("I'll create the files now" with no tool calls)
5596
+ // escalate here even though the phase recovery conditions keyed on
5597
+ // hadTextOutput skip them: nudge after the first actionless session, signal
5598
+ // model escalation after the second (a no-op unless the caller configured a
5599
+ // fallback model), and stop as 'stalled' after the third — the same total
5600
+ // session budget as before stallPolicy existed.
5601
+ stallPolicy: { nudgeAfter: 1, escalateModelAfter: 2, stopAfter: 3 },
5602
+ classifyVariant: "builtin:classify-task-variant",
5603
+ bootstrap: "builtin:repo-bootstrap-discovery",
5604
+ candidateBlock: "builtin:best-candidate-block",
5605
+ milestones: [
5606
+ {
5607
+ name: "research",
5608
+ description: "Inspect the repo and identify the correct target file",
5609
+ instructions: "builtin:research-instructions",
5610
+ toolGuidance: "builtin:research-tool-guidance",
5611
+ completionCriteria: { type: "builtin:research-complete" },
5612
+ intercept: "builtin:research-guard",
5613
+ transitionSummary: "builtin:research-transition-summary",
5614
+ recovery: "builtin:research-recovery",
5615
+ forceEndTurn: "builtin:research-force-end-turn",
5616
+ canAcceptCompletion: "builtin:research-accept-completion"
5617
+ },
5618
+ {
5619
+ name: "planning",
5620
+ description: "Write the implementation plan before editing product files",
5621
+ instructions: "builtin:planning-instructions",
5622
+ toolGuidance: "builtin:planning-tool-guidance",
5623
+ completionCriteria: { type: "builtin:planning-complete" },
5624
+ intercept: "builtin:planning-guard",
5625
+ transitionSummary: "builtin:planning-transition-summary",
5626
+ recovery: "builtin:planning-recovery",
5627
+ forceEndTurn: "builtin:planning-force-end-turn"
5628
+ // canAcceptCompletion intentionally absent: the hand-written planning
5629
+ // phase never defined it, and the SDK accepts completion when the slot
5630
+ // is undefined. Keep parity.
5631
+ },
5632
+ {
5633
+ name: "execution",
5634
+ description: "Execute the plan by editing target files",
5635
+ instructions: "builtin:execution-instructions",
5636
+ toolGuidance: "builtin:execution-tool-guidance",
5637
+ // Execution never auto-advances; completion is agent-driven via TASK_COMPLETE
5638
+ completionCriteria: { type: "never" },
5639
+ intercept: "builtin:execution-guard",
5640
+ recovery: "builtin:execution-recovery",
5641
+ forceEndTurn: "builtin:execution-force-end-turn",
5642
+ canAcceptCompletion: "builtin:execution-accept-completion"
5643
+ }
5644
+ ]
5197
5645
  };
5646
+ ensureDefaultWorkflowHooks();
5647
+ var defaultWorkflow = compileWorkflowConfig(defaultWorkflowConfig);
5198
5648
 
5199
5649
  // src/workflows/deploy-workflow.ts
5200
5650
  var scaffoldPhase = {
@@ -5606,6 +6056,34 @@ var gameWorkflow = {
5606
6056
  }
5607
6057
  };
5608
6058
 
6059
+ // src/workflows/stall-policy.ts
6060
+ var DEFAULT_STALL_STOP_AFTER = 3;
6061
+ function isPositiveInteger(value) {
6062
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
6063
+ }
6064
+ function resolveStallStopAfter(policy) {
6065
+ return isPositiveInteger(policy?.stopAfter) ? policy.stopAfter : DEFAULT_STALL_STOP_AFTER;
6066
+ }
6067
+ function shouldRequestModelEscalation(policy, consecutiveEmptySessions) {
6068
+ const threshold = policy?.escalateModelAfter;
6069
+ if (!isPositiveInteger(threshold)) return false;
6070
+ return consecutiveEmptySessions === threshold;
6071
+ }
6072
+ function shouldInjectEmptySessionNudge(policy, consecutiveEmptySessions) {
6073
+ const threshold = policy?.nudgeAfter;
6074
+ if (!isPositiveInteger(threshold)) return false;
6075
+ return consecutiveEmptySessions >= threshold;
6076
+ }
6077
+ function buildEmptySessionNudge(consecutiveEmptySessions) {
6078
+ const sessionPhrase = consecutiveEmptySessions === 1 ? "Your previous session ended" : `Your previous ${consecutiveEmptySessions} sessions ended`;
6079
+ return [
6080
+ "Recovery instruction:",
6081
+ `${sessionPhrase} without a single tool call. Describing what you plan to do does nothing \u2014 only tool calls make progress.`,
6082
+ "Your next response MUST include at least one tool call (for example write_file, edit_file, read_file, or run_check) that advances the task.",
6083
+ "If a previous tool call was blocked, re-read the block message and satisfy its requirement instead of ending the turn."
6084
+ ].join("\n");
6085
+ }
6086
+
5609
6087
  // src/endpoints.ts
5610
6088
  var FlowsEndpoint = class {
5611
6089
  constructor(client) {
@@ -8152,8 +8630,11 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8152
8630
  }
8153
8631
  buildStuckTurnRecoveryMessage(state, workflow) {
8154
8632
  const currentPhase = workflow.phases.find((p) => p.name === state.workflowPhase);
8155
- if (currentPhase?.buildRecoveryMessage) {
8156
- return currentPhase.buildRecoveryMessage(state);
8633
+ const phaseMessage = currentPhase?.buildRecoveryMessage?.(state);
8634
+ if (phaseMessage) return phaseMessage;
8635
+ const emptySessions = state.consecutiveEmptySessions || 0;
8636
+ if (shouldInjectEmptySessionNudge(workflow.stallPolicy, emptySessions)) {
8637
+ return buildEmptySessionNudge(emptySessions);
8157
8638
  }
8158
8639
  return void 0;
8159
8640
  }
@@ -8279,6 +8760,10 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8279
8760
  state.originalMessage = options.message;
8280
8761
  }
8281
8762
  const queuedSteeringMessages = options.getQueuedUserMessages?.() ?? [];
8763
+ if (queuedSteeringMessages.length > 0) {
8764
+ state.consecutiveEmptySessions = 0;
8765
+ state.stallEscalationRequested = void 0;
8766
+ }
8282
8767
  const preparedSession = await this.prepareSessionContext(
8283
8768
  options.message,
8284
8769
  state,
@@ -8519,11 +9004,15 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8519
9004
  } else {
8520
9005
  state.lastCompletionRejectionReason = void 0;
8521
9006
  }
8522
- const sessionHadActions = sessionTrace.wroteFiles || sessionTrace.readFiles || sessionTrace.discoveryPerformed || sessionTrace.verificationAttempted;
9007
+ const sessionHadActions = sessionTrace.wroteFiles || sessionTrace.readFiles || sessionTrace.discoveryPerformed || sessionTrace.verificationAttempted || options.hasQueuedUserMessages?.() === true;
8523
9008
  if (sessionHadActions) {
8524
9009
  state.consecutiveEmptySessions = 0;
9010
+ state.stallEscalationRequested = void 0;
8525
9011
  } else {
8526
9012
  state.consecutiveEmptySessions = (state.consecutiveEmptySessions || 0) + 1;
9013
+ if (shouldRequestModelEscalation(workflow.stallPolicy, state.consecutiveEmptySessions)) {
9014
+ state.stallEscalationRequested = true;
9015
+ }
8527
9016
  }
8528
9017
  if (sessionResult.stopReason === "complete" && !detectedTaskCompletion) {
8529
9018
  const currentPhase = workflow.phases.find((p) => p.name === state.workflowPhase);
@@ -8552,7 +9041,7 @@ var _AgentsEndpoint = class _AgentsEndpoint {
8552
9041
  state.status = "budget_exceeded";
8553
9042
  } else if (acceptedTaskCompletion) {
8554
9043
  state.status = "complete";
8555
- } else if ((state.consecutiveEmptySessions || 0) >= 3) {
9044
+ } else if ((state.consecutiveEmptySessions || 0) >= resolveStallStopAfter(workflow.stallPolicy)) {
8556
9045
  state.status = "stalled";
8557
9046
  } else if (maxCost && state.totalCost >= maxCost) {
8558
9047
  state.status = "budget_exceeded";
@@ -11102,6 +11591,8 @@ var STEP_TYPE_TO_METHOD = {
11102
11591
  ClientTokensEndpoint,
11103
11592
  ContextTemplatesEndpoint,
11104
11593
  ConversationsEndpoint,
11594
+ DEFAULT_RECOVERY_AFTER_EMPTY_SESSIONS,
11595
+ DEFAULT_STALL_STOP_AFTER,
11105
11596
  DispatchEndpoint,
11106
11597
  EvalBuilder,
11107
11598
  EvalEndpoint,
@@ -11139,25 +11630,34 @@ var STEP_TYPE_TO_METHOD = {
11139
11630
  UsersEndpoint,
11140
11631
  applyGeneratedRuntimeToolProposalToDispatchRequest,
11141
11632
  attachRuntimeToolsToDispatchRequest,
11633
+ buildEmptySessionNudge,
11142
11634
  buildGeneratedRuntimeToolGateOutput,
11143
11635
  buildLedgerOffloadReference,
11636
+ buildPolicyGuidance,
11144
11637
  buildSendViewOffloadMarker,
11638
+ compileWorkflowConfig,
11145
11639
  computeAgentContentHash,
11146
11640
  computeFlowContentHash,
11147
11641
  createClient,
11148
11642
  createExternalTool,
11149
11643
  defaultWorkflow,
11644
+ defaultWorkflowConfig,
11150
11645
  defineAgent,
11151
11646
  defineFlow,
11647
+ definePlaybook,
11152
11648
  deployWorkflow,
11649
+ ensureDefaultWorkflowHooks,
11153
11650
  evaluateGeneratedRuntimeToolProposal,
11154
11651
  extractDeclaredToolResultChars,
11155
11652
  gameWorkflow,
11156
11653
  getDefaultPlanPath,
11157
11654
  getLikelySupportingCandidatePaths,
11655
+ interpolateWorkflowTemplate,
11158
11656
  isDiscoveryToolName,
11159
11657
  isMarathonArtifactPath,
11160
11658
  isPreservationSensitiveTask,
11659
+ isWorkflowHookRef,
11660
+ listWorkflowHooks,
11161
11661
  normalizeAgentDefinition,
11162
11662
  normalizeCandidatePath,
11163
11663
  parseFinalBuffer,
@@ -11165,6 +11665,12 @@ var STEP_TYPE_TO_METHOD = {
11165
11665
  parseOffloadedOutputId,
11166
11666
  parseSSEChunk,
11167
11667
  processStream,
11668
+ registerWorkflowHook,
11669
+ resolveStallStopAfter,
11670
+ resolveWorkflowHook,
11168
11671
  sanitizeTaskSlug,
11169
- streamEvents
11672
+ shouldInjectEmptySessionNudge,
11673
+ shouldRequestModelEscalation,
11674
+ streamEvents,
11675
+ unregisterWorkflowHook
11170
11676
  });