squad-kit 0.9.1 → 0.10.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/cli.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/cli.ts
9
- import { Command } from "commander";
9
+ import { Command, Option } from "commander";
10
10
 
11
11
  // src/ui/banner.ts
12
12
  import boxen from "boxen";
@@ -349,6 +349,20 @@ function mergePlanner(base, override) {
349
349
  validation: {
350
350
  enabled: override.validation?.enabled ?? base?.validation?.enabled ?? true,
351
351
  strict: override.validation?.strict ?? base?.validation?.strict ?? false
352
+ },
353
+ runtime: {
354
+ anthropic: override.runtime?.anthropic ?? base?.runtime?.anthropic ?? "agent-sdk"
355
+ },
356
+ providerOptions: {
357
+ anthropic: {
358
+ thinking: override.providerOptions?.anthropic?.thinking ?? base?.providerOptions?.anthropic?.thinking ?? "adaptive",
359
+ effort: override.providerOptions?.anthropic?.effort ?? base?.providerOptions?.anthropic?.effort ?? "medium",
360
+ effortByPhase: {
361
+ scout: override.providerOptions?.anthropic?.effortByPhase?.scout ?? base?.providerOptions?.anthropic?.effortByPhase?.scout ?? "minimal",
362
+ draft: override.providerOptions?.anthropic?.effortByPhase?.draft ?? base?.providerOptions?.anthropic?.effortByPhase?.draft ?? "medium"
363
+ },
364
+ thinkingBudget: override.providerOptions?.anthropic?.thinkingBudget ?? base?.providerOptions?.anthropic?.thinkingBudget
365
+ }
352
366
  }
353
367
  };
354
368
  if (merged.modelOverride) {
@@ -427,6 +441,12 @@ function parseConfig(raw, configFile) {
427
441
  "planner.budget.maxDurationSeconds must be > 0. Run `squad config set planner` to fix planner.budget values."
428
442
  );
429
443
  }
444
+ const ar = merged.planner.runtime?.anthropic;
445
+ if (ar !== void 0 && ar !== "agent-sdk" && ar !== "vercel") {
446
+ throw new Error(
447
+ "planner.runtime.anthropic must be agent-sdk or vercel. Run `squad config set planner` to fix it."
448
+ );
449
+ }
430
450
  }
431
451
  return merged;
432
452
  }
@@ -1025,6 +1045,14 @@ async function runConfigUnsetPlanner(opts = {}) {
1025
1045
  info("Re-enable any time with `squad config set planner`.");
1026
1046
  }
1027
1047
 
1048
+ // src/core/ci-env.ts
1049
+ function skipExternalProbesInAutomation() {
1050
+ const c = process.env.CI;
1051
+ if (c === "true" || c === "1") return true;
1052
+ if (process.env.GITHUB_ACTIONS) return true;
1053
+ return false;
1054
+ }
1055
+
1028
1056
  // src/commands/config/set-planner.ts
1029
1057
  function parseProvider(arg) {
1030
1058
  if (!["anthropic", "openai", "google"].includes(arg)) {
@@ -1153,20 +1181,27 @@ async function runConfigSetPlanner(opts = {}) {
1153
1181
  kv("model (execute)", execModel, 10);
1154
1182
  kv("credential", `${cred.source} (${sourceText})`, 10);
1155
1183
  }
1156
- if (process.env.CI === "true" || !cred) {
1184
+ if (skipExternalProbesInAutomation() || !cred) {
1157
1185
  printPlannerNextSteps(Boolean(cred));
1158
1186
  return;
1159
1187
  }
1160
- const listed = await fetchProviderModelIds(provider, cred.value);
1161
- if (!listed.ok) {
1162
- const st = listed.status;
1163
- if (st === 401 || st === 403) {
1164
- warning(`Could not list models (HTTP ${st}); check your key.`);
1188
+ try {
1189
+ const listed = await fetchProviderModelIds(provider, cred.value);
1190
+ if (!listed.ok) {
1191
+ const st = listed.status;
1192
+ if (st === 401 || st === 403) {
1193
+ warning(`Could not list models (HTTP ${st}); check your key.`);
1194
+ } else {
1195
+ warning(`Model list probe failed (HTTP ${st}): ${listed.body.slice(0, 120)}`);
1196
+ }
1165
1197
  } else {
1166
- warning(`Model list probe failed (HTTP ${st}): ${listed.body.slice(0, 120)}`);
1198
+ info("Credential check: models API responded OK for this key.");
1167
1199
  }
1168
- } else {
1169
- info("Credential check: models API responded OK for this key.");
1200
+ } catch (e) {
1201
+ const msg = e instanceof Error ? e.message : String(e);
1202
+ warning(
1203
+ `Could not reach provider model list (${msg.slice(0, 160)}). Key was saved; run \`squad doctor\` to verify.`
1204
+ );
1170
1205
  }
1171
1206
  printPlannerNextSteps(true);
1172
1207
  }
@@ -2010,7 +2045,7 @@ async function runConfigSetTracker(opts = {}) {
2010
2045
  if (next.tracker.project) {
2011
2046
  kv("project", next.tracker.project, 10);
2012
2047
  }
2013
- if (process.env.CI === "true") {
2048
+ if (skipExternalProbesInAutomation()) {
2014
2049
  printTrackerNextSteps(next.tracker.type);
2015
2050
  return;
2016
2051
  }
@@ -3296,17 +3331,32 @@ function noCandidateReason() {
3296
3331
  // src/planner/loop.ts
3297
3332
  import path19 from "path";
3298
3333
  import fs27 from "fs";
3299
- import { APICallError as APICallError2 } from "ai";
3334
+ import { APICallError as APICallError3 } from "ai";
3335
+
3336
+ // src/planner/tools/index.ts
3337
+ import { randomUUID } from "crypto";
3300
3338
 
3301
3339
  // src/planner/tools/read-file.ts
3302
3340
  import fs21 from "fs";
3303
3341
  import path13 from "path";
3304
3342
  import { tool } from "ai";
3305
3343
  import { z } from "zod";
3306
- var READ_FILE_TOOL_NAME = "read_file";
3307
3344
  var MAX_BYTES_PER_FULL_READ = 32e3;
3308
3345
  var MAX_LINES_PER_RANGED_READ = 400;
3309
3346
  var MAX_BYTES_FOR_SYNC_LINE_SPLIT = 2e6;
3347
+ var rangedReadFileSchema = z.object({
3348
+ path: z.string().min(1).describe("Repo-relative POSIX path."),
3349
+ offset: z.number().int().min(1).optional().describe("1-indexed line number to start reading at (use with limit)."),
3350
+ limit: z.number().int().min(1).max(MAX_LINES_PER_RANGED_READ).optional().describe(`Line count from offset (max ${MAX_LINES_PER_RANGED_READ}).`)
3351
+ }).refine(
3352
+ (o) => o.offset === void 0 && o.limit === void 0 || o.offset !== void 0 && o.limit !== void 0,
3353
+ { message: "Provide both offset and limit for a ranged read, or neither for a full read." }
3354
+ );
3355
+ var fullReadFileSchema = z.object({
3356
+ path: z.string().min(1).describe("Repo-relative POSIX path.")
3357
+ });
3358
+ var READ_FILE_TOOL_DESCRIPTION = "Read a UTF-8 text file from the project, returning its contents so you can plan against real code. Pass `offset` and `limit` together to read a line range (preferred when you know the region). Without them the whole file is read, capped at 32 KB. Paths must be relative to the project root. Binary files are refused. You have a bounded context budget; prefer small, targeted reads.";
3359
+ var READ_FILE_TOOL_DESCRIPTION_WHOLE_ONLY = "Read a UTF-8 text file from the project, returning its contents so you can plan against real code. Whole-file reads only; capped at 32 KB. Paths must be relative to the project root. Binary files are refused. You have a bounded context budget; prefer small, targeted reads.";
3310
3360
  function readFileTool(root, budget, input5) {
3311
3361
  const raw = typeof input5.path === "string" ? input5.path : "";
3312
3362
  if (!raw.length) {
@@ -3392,66 +3442,6 @@ function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
3392
3442
  budget.recordRead(bytes);
3393
3443
  return { content: out, isError: false };
3394
3444
  }
3395
- function readFileToolFactory(root, budget, hooks, opts) {
3396
- const ranged = opts?.ranged ?? true;
3397
- const params = ranged ? z.object({
3398
- path: z.string().min(1).describe("Repo-relative POSIX path."),
3399
- offset: z.number().int().min(1).optional().describe("1-indexed line number to start reading at (use with limit)."),
3400
- limit: z.number().int().min(1).max(MAX_LINES_PER_RANGED_READ).optional().describe(`Line count from offset (max ${MAX_LINES_PER_RANGED_READ}).`)
3401
- }).refine(
3402
- (o) => o.offset === void 0 && o.limit === void 0 || o.offset !== void 0 && o.limit !== void 0,
3403
- { message: "Provide both offset and limit for a ranged read, or neither for a full read." }
3404
- ) : z.object({
3405
- path: z.string().min(1).describe("Repo-relative POSIX path.")
3406
- });
3407
- return tool({
3408
- description: "Read a UTF-8 text file from the project, returning its contents so you can plan against real code. " + (ranged ? "Pass `offset` and `limit` together to read a line range (preferred when you know the region). Without them the whole file is read, capped at 32 KB. " : "Whole-file reads only; capped at 32 KB. ") + "Paths must be relative to the project root. Binary files are refused. You have a bounded context budget; prefer small, targeted reads.",
3409
- parameters: params,
3410
- execute: async (args, { toolCallId }) => {
3411
- const tc = {
3412
- id: toolCallId,
3413
- name: READ_FILE_TOOL_NAME,
3414
- input: { path: args.path, offset: args.offset, limit: args.limit }
3415
- };
3416
- const before = budget.snapshot().bytes;
3417
- let result = readFileTool(root, budget, args);
3418
- let after = budget.snapshot().bytes;
3419
- let bytesLoaded = after - before;
3420
- hooks.onToolCall?.(tc, bytesLoaded, after);
3421
- hooks.bus.emit({
3422
- kind: "tool_call",
3423
- runId: hooks.runId,
3424
- turn: hooks.turn,
3425
- toolCall: tc,
3426
- bytesLoaded,
3427
- totalBytes: after
3428
- });
3429
- if (readBudgetishError(result)) {
3430
- const decide = hooks.decideOnLimit;
3431
- if (decide) {
3432
- const d = await decide(hooks.getLimitCtx());
3433
- if (d === "cancel") {
3434
- throw new PlannerUserCancelledError(
3435
- hooks.getAccumulatedText(),
3436
- hooks.runId,
3437
- hooks.turn,
3438
- "budget_reads",
3439
- tc,
3440
- result
3441
- );
3442
- }
3443
- hooks.extendSessionLimits();
3444
- result = readFileTool(root, budget, args);
3445
- after = budget.snapshot().bytes;
3446
- bytesLoaded = after - before;
3447
- } else {
3448
- hooks.setBudgetExhausted(true);
3449
- }
3450
- }
3451
- return { content: result.content, isError: result.isError };
3452
- }
3453
- });
3454
- }
3455
3445
  var PlannerUserCancelledError = class _PlannerUserCancelledError extends Error {
3456
3446
  constructor(planText, runId, turn, incompleteKind, toolCall, pendingResult) {
3457
3447
  super("PlannerUserCancelledError");
@@ -3630,38 +3620,13 @@ function walkFlat(root, dir, ig, out, max, includeHidden) {
3630
3620
  var MAX_GREP_MATCHES = 200;
3631
3621
  var MAX_GREP_FILE_SIZE_BYTES = 1e6;
3632
3622
  var GREP_TOOL_NAME = "grep";
3633
- function grepToolFactory(root, budget, hooks) {
3634
- return tool2({
3635
- description: "Search the project for a literal string or regex pattern. Returns up to 200 matches across files, each shown as `path:line:match`. Use this BEFORE `read_file` to locate symbols or call sites; it is much cheaper than reading whole files.",
3636
- parameters: z2.object({
3637
- pattern: z2.string().min(1).describe("Literal string or regex (default literal)."),
3638
- regex: z2.boolean().optional().default(false),
3639
- path: z2.string().optional().describe("Optional sub-path to scope the search to (relative)."),
3640
- caseInsensitive: z2.boolean().optional().default(false)
3641
- }),
3642
- execute: async (args, { toolCallId }) => {
3643
- const tc = {
3644
- id: toolCallId,
3645
- name: GREP_TOOL_NAME,
3646
- input: { ...args }
3647
- };
3648
- const before = budget.snapshot().bytes;
3649
- const out = runGrep(root, budget, args);
3650
- const after = budget.snapshot().bytes;
3651
- const bytesLoaded = after - before;
3652
- hooks.onToolCall?.(tc, bytesLoaded, after);
3653
- hooks.bus.emit({
3654
- kind: "tool_call",
3655
- runId: hooks.runId,
3656
- turn: hooks.turn,
3657
- toolCall: tc,
3658
- bytesLoaded,
3659
- totalBytes: after
3660
- });
3661
- return out;
3662
- }
3663
- });
3664
- }
3623
+ var grepSchema = z2.object({
3624
+ pattern: z2.string().min(1).describe("Literal string or regex (default literal)."),
3625
+ regex: z2.boolean().optional().default(false),
3626
+ path: z2.string().optional().describe("Optional sub-path to scope the search to (relative)."),
3627
+ caseInsensitive: z2.boolean().optional().default(false)
3628
+ });
3629
+ var GREP_TOOL_DESCRIPTION = "Search the project for a literal string or regex pattern. Returns up to 200 matches across files, each shown as `path:line:match`. Use this BEFORE `read_file` to locate symbols or call sites; it is much cheaper than reading whole files.";
3665
3630
  function runGrep(root, budget, args) {
3666
3631
  const exec = budget.canExecuteTool();
3667
3632
  if (!exec.ok) {
@@ -3791,34 +3756,10 @@ import { tool as tool3 } from "ai";
3791
3756
  import { z as z3 } from "zod";
3792
3757
  var MAX_ENTRIES_PER_LIST = 200;
3793
3758
  var LIST_DIR_TOOL_NAME = "list_dir";
3794
- function listDirToolFactory(root, budget, hooks) {
3795
- return tool3({
3796
- description: "List the contents of a directory in the project (one level deep). Returns up to 200 entries with type (file/dir) and size. Cheap; use this to discover sibling files before reading them.",
3797
- parameters: z3.object({
3798
- path: z3.string().describe('Repo-relative directory path. Use "." for the project root.')
3799
- }),
3800
- execute: async (args, { toolCallId }) => {
3801
- const tc = {
3802
- id: toolCallId,
3803
- name: LIST_DIR_TOOL_NAME,
3804
- input: { path: args.path }
3805
- };
3806
- const before = budget.snapshot().bytes;
3807
- const out = runListDir(root, budget, args.path);
3808
- const after = budget.snapshot().bytes;
3809
- hooks.onToolCall?.(tc, after - before, after);
3810
- hooks.bus.emit({
3811
- kind: "tool_call",
3812
- runId: hooks.runId,
3813
- turn: hooks.turn,
3814
- toolCall: tc,
3815
- bytesLoaded: after - before,
3816
- totalBytes: after
3817
- });
3818
- return out;
3819
- }
3820
- });
3821
- }
3759
+ var listDirSchema = z3.object({
3760
+ path: z3.string().describe('Repo-relative directory path. Use "." for the project root.')
3761
+ });
3762
+ var LIST_DIR_TOOL_DESCRIPTION = "List the contents of a directory in the project (one level deep). Returns up to 200 entries with type (file/dir) and size. Cheap; use this to discover sibling files before reading them.";
3822
3763
  function runListDir(root, budget, rawPath) {
3823
3764
  const exec = budget.canExecuteTool();
3824
3765
  if (!exec.ok) {
@@ -3895,26 +3836,132 @@ ${lines.join("\n")}` : "(empty directory)";
3895
3836
  }
3896
3837
 
3897
3838
  // src/planner/tools/index.ts
3898
- function buildPlannerTools(opts) {
3839
+ import { tool as tool4 } from "ai";
3840
+ function buildPlannerToolDefinitions(opts) {
3899
3841
  const enabled = opts.enabled ?? { rangedRead: true, grep: true, listDir: true };
3900
- const gh = opts.grepHooks ?? {
3901
- runId: opts.readHooks.runId,
3902
- turn: opts.readHooks.turn,
3903
- bus: opts.readHooks.bus,
3904
- onToolCall: opts.readHooks.onToolCall
3905
- };
3906
- const lh = opts.listDirHooks ?? {
3907
- runId: opts.readHooks.runId,
3908
- turn: opts.readHooks.turn,
3909
- bus: opts.readHooks.bus,
3910
- onToolCall: opts.readHooks.onToolCall
3911
- };
3912
- const readOpts = { ranged: enabled.rangedRead ?? true };
3913
- return {
3914
- read_file: readFileToolFactory(opts.root, opts.budget, opts.readHooks, readOpts),
3915
- ...enabled.grep ? { grep: grepToolFactory(opts.root, opts.budget, gh) } : {},
3916
- ...enabled.listDir ? { list_dir: listDirToolFactory(opts.root, opts.budget, lh) } : {}
3917
- };
3842
+ const ranged = enabled.rangedRead ?? true;
3843
+ const defs = [];
3844
+ const readParams = ranged ? rangedReadFileSchema : fullReadFileSchema;
3845
+ const readDesc = ranged ? READ_FILE_TOOL_DESCRIPTION : READ_FILE_TOOL_DESCRIPTION_WHOLE_ONLY;
3846
+ defs.push({
3847
+ name: "read_file",
3848
+ description: readDesc,
3849
+ parameters: readParams,
3850
+ execute: async (args) => {
3851
+ const turn = opts.getTurn();
3852
+ const tc = {
3853
+ id: randomUUID(),
3854
+ name: "read_file",
3855
+ input: { path: args.path, offset: args.offset, limit: args.limit }
3856
+ };
3857
+ const before = opts.budget.snapshot().bytes;
3858
+ let result = readFileTool(opts.root, opts.budget, args);
3859
+ let after = opts.budget.snapshot().bytes;
3860
+ let bytesLoaded = after - before;
3861
+ opts.onToolCall?.(tc, bytesLoaded, after);
3862
+ opts.bus.emit({
3863
+ kind: "tool_call",
3864
+ runId: opts.runId,
3865
+ turn,
3866
+ toolCall: tc,
3867
+ bytesLoaded,
3868
+ totalBytes: after
3869
+ });
3870
+ if (readBudgetishError(result)) {
3871
+ const decide = opts.decideOnLimit;
3872
+ if (decide) {
3873
+ const d = await decide(opts.getLimitCtx());
3874
+ if (d === "cancel") {
3875
+ throw new PlannerUserCancelledError(opts.getAccumulatedText(), opts.runId, turn, "budget_reads", tc, result);
3876
+ }
3877
+ opts.extendSessionLimits();
3878
+ result = readFileTool(opts.root, opts.budget, args);
3879
+ after = opts.budget.snapshot().bytes;
3880
+ bytesLoaded = after - before;
3881
+ } else {
3882
+ opts.setBudgetExhausted(true);
3883
+ }
3884
+ }
3885
+ return { content: result.content, isError: result.isError };
3886
+ }
3887
+ });
3888
+ if (enabled.grep) {
3889
+ defs.push({
3890
+ name: GREP_TOOL_NAME,
3891
+ description: GREP_TOOL_DESCRIPTION,
3892
+ parameters: grepSchema,
3893
+ execute: async (args) => {
3894
+ const turn = opts.getTurn();
3895
+ const tc = {
3896
+ id: randomUUID(),
3897
+ name: GREP_TOOL_NAME,
3898
+ input: { ...args }
3899
+ };
3900
+ const before = opts.budget.snapshot().bytes;
3901
+ const out = runGrep(opts.root, opts.budget, {
3902
+ pattern: args.pattern,
3903
+ regex: args.regex ?? false,
3904
+ path: args.path,
3905
+ caseInsensitive: args.caseInsensitive ?? false
3906
+ });
3907
+ const after = opts.budget.snapshot().bytes;
3908
+ const bytesLoaded = after - before;
3909
+ opts.onToolCall?.(tc, bytesLoaded, after);
3910
+ opts.bus.emit({
3911
+ kind: "tool_call",
3912
+ runId: opts.runId,
3913
+ turn,
3914
+ toolCall: tc,
3915
+ bytesLoaded,
3916
+ totalBytes: after
3917
+ });
3918
+ return { content: out.content, isError: out.isError };
3919
+ }
3920
+ });
3921
+ }
3922
+ if (enabled.listDir) {
3923
+ defs.push({
3924
+ name: LIST_DIR_TOOL_NAME,
3925
+ description: LIST_DIR_TOOL_DESCRIPTION,
3926
+ parameters: listDirSchema,
3927
+ execute: async (args) => {
3928
+ const turn = opts.getTurn();
3929
+ const tc = {
3930
+ id: randomUUID(),
3931
+ name: LIST_DIR_TOOL_NAME,
3932
+ input: { path: args.path }
3933
+ };
3934
+ const before = opts.budget.snapshot().bytes;
3935
+ const out = runListDir(opts.root, opts.budget, args.path);
3936
+ const after = opts.budget.snapshot().bytes;
3937
+ opts.onToolCall?.(tc, after - before, after);
3938
+ opts.bus.emit({
3939
+ kind: "tool_call",
3940
+ runId: opts.runId,
3941
+ turn,
3942
+ toolCall: tc,
3943
+ bytesLoaded: after - before,
3944
+ totalBytes: after
3945
+ });
3946
+ return { content: out.content, isError: out.isError };
3947
+ }
3948
+ });
3949
+ }
3950
+ return defs;
3951
+ }
3952
+ function vercelToolsFromDefinitions(defs) {
3953
+ const out = {};
3954
+ for (const def of defs) {
3955
+ out[def.name] = tool4({
3956
+ description: def.description,
3957
+ parameters: def.parameters,
3958
+ execute: async (args) => {
3959
+ const r = await def.execute(args);
3960
+ return typeof r === "string" ? r : r;
3961
+ }
3962
+ });
3963
+ }
3964
+ return out;
3918
3965
  }
3919
3966
 
3920
3967
  // src/planner/provider-errors.ts
@@ -4004,7 +4051,87 @@ function rateLimitMessage(err2) {
4004
4051
  ].join("\n");
4005
4052
  }
4006
4053
 
4054
+ // src/core/runs.ts
4055
+ import fs25 from "fs/promises";
4056
+ import path17 from "path";
4057
+ import { randomBytes } from "crypto";
4058
+ var RING_SIZE = 20;
4059
+ function newRunId() {
4060
+ const ts = Date.now().toString(36).padStart(9, "0");
4061
+ const rnd = randomBytes(8).toString("hex");
4062
+ return `${ts}-${rnd}`;
4063
+ }
4064
+ async function appendRun(paths, rec) {
4065
+ const dir = path17.join(paths.squadDir, "runs");
4066
+ await fs25.mkdir(dir, { recursive: true });
4067
+ const file = path17.join(dir, `${rec.runId}.json`);
4068
+ await fs25.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
4069
+ await pruneOldRuns(dir);
4070
+ }
4071
+ async function pruneOldRuns(dir) {
4072
+ const entries = (await fs25.readdir(dir)).filter((n) => n.endsWith(".json"));
4073
+ if (entries.length <= RING_SIZE) return;
4074
+ entries.sort();
4075
+ const drop = entries.slice(0, entries.length - RING_SIZE);
4076
+ await Promise.all(drop.map((n) => fs25.rm(path17.join(dir, n), { force: true })));
4077
+ }
4078
+ async function listRuns(paths) {
4079
+ const dir = path17.join(paths.squadDir, "runs");
4080
+ let entries;
4081
+ try {
4082
+ entries = await fs25.readdir(dir);
4083
+ } catch {
4084
+ return [];
4085
+ }
4086
+ const files = entries.filter((n) => n.endsWith(".json")).sort().reverse();
4087
+ const out = [];
4088
+ for (const f of files) {
4089
+ try {
4090
+ const raw = await fs25.readFile(path17.join(dir, f), "utf8");
4091
+ const parsed = JSON.parse(raw);
4092
+ if (parsed.version === 1) out.push(parsed);
4093
+ } catch {
4094
+ }
4095
+ }
4096
+ return out;
4097
+ }
4098
+
4099
+ // src/planner/session-limits.ts
4100
+ var DEFAULT_PLANNER_MAX_ITERATIONS = 40;
4101
+ var PLANNER_MARKDOWN_CONTINUATION_USER = "Your previous reply was cut off because it hit the per-response output token limit for this request. Continue the Markdown plan from the exact cut-off only: write the remainder, do not repeat headings or paragraphs already sent. When the plan is complete, use end_turn without unnecessary tool calls.";
4102
+
4103
+ // src/planner/runtimes/vercel-runtime.ts
4104
+ import { APICallError, generateObject, NoObjectGeneratedError } from "ai";
4105
+
4106
+ // src/planner/providers/index.ts
4107
+ import { createAnthropic } from "@ai-sdk/anthropic";
4108
+ import { createOpenAI } from "@ai-sdk/openai";
4109
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
4110
+ function resolveModel(provider, modelId, apiKey) {
4111
+ switch (provider) {
4112
+ case "anthropic":
4113
+ return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
4114
+ case "openai":
4115
+ return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
4116
+ case "google":
4117
+ return { provider, model: createGoogleGenerativeAI({ apiKey })(modelId), modelId };
4118
+ }
4119
+ }
4120
+
4007
4121
  // src/planner/usage-map.ts
4122
+ function usageFromAgentSdkResult(usage) {
4123
+ const inTok = usage?.input_tokens ?? 0;
4124
+ const outTok = usage?.output_tokens ?? 0;
4125
+ const out = {
4126
+ inputTokens: inTok ?? 0,
4127
+ outputTokens: outTok ?? 0
4128
+ };
4129
+ const cc = usage?.cache_creation_input_tokens;
4130
+ const cr = usage?.cache_read_input_tokens;
4131
+ if (cc != null && cc > 0) out.cacheCreationTokens = cc;
4132
+ if (cr != null && cr > 0) out.cacheReadTokens = cr;
4133
+ return out;
4134
+ }
4008
4135
  function usageFromLanguageModelStep(usage, providerMetadata) {
4009
4136
  const out = {
4010
4137
  inputTokens: usage.promptTokens,
@@ -4135,74 +4262,558 @@ function turnsToCoreMessages(systemPrompt, turns, opts) {
4135
4262
  return messages;
4136
4263
  }
4137
4264
 
4138
- // src/core/runs.ts
4139
- import fs25 from "fs/promises";
4140
- import path17 from "path";
4141
- import { randomBytes } from "crypto";
4142
- var RING_SIZE = 20;
4143
- function newRunId() {
4144
- const ts = Date.now().toString(36).padStart(9, "0");
4145
- const rnd = randomBytes(8).toString("hex");
4146
- return `${ts}-${rnd}`;
4147
- }
4148
- async function appendRun(paths, rec) {
4149
- const dir = path17.join(paths.squadDir, "runs");
4150
- await fs25.mkdir(dir, { recursive: true });
4151
- const file = path17.join(dir, `${rec.runId}.json`);
4152
- await fs25.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
4153
- await pruneOldRuns(dir);
4265
+ // src/planner/runtimes/vercel-runtime.ts
4266
+ var MAX_RATE_LIMIT_RETRY_SEC = 90;
4267
+ function isAbortError(e) {
4268
+ return e instanceof Error && e.name === "AbortError";
4154
4269
  }
4155
- async function pruneOldRuns(dir) {
4156
- const entries = (await fs25.readdir(dir)).filter((n) => n.endsWith(".json"));
4157
- if (entries.length <= RING_SIZE) return;
4158
- entries.sort();
4159
- const drop = entries.slice(0, entries.length - RING_SIZE);
4160
- await Promise.all(drop.map((n) => fs25.rm(path17.join(dir, n), { force: true })));
4270
+ async function defaultSleep(ms) {
4271
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
4161
4272
  }
4162
- async function listRuns(paths) {
4163
- const dir = path17.join(paths.squadDir, "runs");
4164
- let entries;
4165
- try {
4166
- entries = await fs25.readdir(dir);
4167
- } catch {
4168
- return [];
4273
+ async function sleepWithAbort(ms, sleepFn, abort) {
4274
+ if (!abort) {
4275
+ await sleepFn(ms);
4276
+ return;
4169
4277
  }
4170
- const files = entries.filter((n) => n.endsWith(".json")).sort().reverse();
4171
- const out = [];
4172
- for (const f of files) {
4278
+ if (abort.aborted) throw new DOMException("Aborted", "AbortError");
4279
+ await Promise.race([
4280
+ sleepFn(ms),
4281
+ new Promise((_, reject) => {
4282
+ abort.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
4283
+ })
4284
+ ]);
4285
+ }
4286
+ function asToolInput(args) {
4287
+ if (args && typeof args === "object" && !Array.isArray(args)) return args;
4288
+ if (typeof args === "string") {
4173
4289
  try {
4174
- const raw = await fs25.readFile(path17.join(dir, f), "utf8");
4175
- const parsed = JSON.parse(raw);
4176
- if (parsed.version === 1) out.push(parsed);
4290
+ return JSON.parse(args);
4177
4291
  } catch {
4292
+ return {};
4178
4293
  }
4179
4294
  }
4180
- return out;
4295
+ return {};
4181
4296
  }
4182
-
4183
- // src/planner/session-limits.ts
4184
- var DEFAULT_PLANNER_MAX_ITERATIONS = 40;
4185
- var PLANNER_MARKDOWN_CONTINUATION_USER = "Your previous reply was cut off because it hit the per-response output token limit for this request. Continue the Markdown plan from the exact cut-off only: write the remainder, do not repeat headings or paragraphs already sent. When the plan is complete, use end_turn without unnecessary tool calls.";
4186
-
4187
- // src/planner/events.ts
4188
- var PlannerEventBus = class {
4189
- listeners = /* @__PURE__ */ new Set();
4190
- emit(e) {
4191
- for (const fn of this.listeners) {
4192
- try {
4193
- fn(e);
4194
- } catch {
4195
- }
4196
- }
4197
- }
4198
- subscribe(fn) {
4297
+ function normalizeToolExecutionResult(result) {
4298
+ if (typeof result === "string") {
4299
+ try {
4300
+ const j = JSON.parse(result);
4301
+ if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
4302
+ const o = j;
4303
+ return {
4304
+ content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4305
+ isError: o.isError === true
4306
+ };
4307
+ }
4308
+ } catch {
4309
+ }
4310
+ return { content: result, isError: false };
4311
+ }
4312
+ if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
4313
+ const o = result;
4314
+ return {
4315
+ content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4316
+ isError: o.isError === true
4317
+ };
4318
+ }
4319
+ return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
4320
+ }
4321
+ function applyStepToTurns(turns, step2) {
4322
+ const callsById = /* @__PURE__ */ new Map();
4323
+ const toolCalls = step2.toolCalls.map((tc) => {
4324
+ callsById.set(tc.toolCallId, tc.toolName);
4325
+ return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
4326
+ });
4327
+ turns.push({
4328
+ role: "assistant",
4329
+ ...step2.text ? { text: step2.text } : {},
4330
+ ...toolCalls.length ? { toolCalls } : {}
4331
+ });
4332
+ if (step2.toolResults.length) {
4333
+ const toolResults = step2.toolResults.map((tr) => {
4334
+ const norm = normalizeToolExecutionResult(tr.result);
4335
+ return {
4336
+ toolCallId: tr.toolCallId,
4337
+ name: callsById.get(tr.toolCallId) ?? "read_file",
4338
+ content: norm.content,
4339
+ ...norm.isError ? { isError: true } : {}
4340
+ };
4341
+ });
4342
+ turns.push({ role: "user", toolResults });
4343
+ }
4344
+ }
4345
+ function recordUsageAndEmit(bus, onUsage, budget, runId, turn, usage) {
4346
+ budget.recordUsage(usage);
4347
+ onUsage?.(usage);
4348
+ bus.emit({ kind: "usage", runId, turn, usage });
4349
+ const u = budget.snapshot().usage;
4350
+ const cacheRead = u.cacheReadTokens ?? 0;
4351
+ const cacheCreate = u.cacheCreationTokens ?? 0;
4352
+ if (cacheRead > 0 || cacheCreate > 0) {
4353
+ const inTok = u.inputTokens;
4354
+ const totalInput = inTok + cacheRead;
4355
+ const cacheHitRatio = totalInput === 0 ? 0 : Math.round(cacheRead / totalInput * 100) / 100;
4356
+ bus.emit({
4357
+ kind: "cache_summary",
4358
+ runId,
4359
+ turn,
4360
+ cacheHitRatio,
4361
+ cacheReadTokens: cacheRead,
4362
+ cacheCreationTokens: cacheCreate
4363
+ });
4364
+ }
4365
+ }
4366
+ function providerResponseFromApiError(provider, modelId, err2) {
4367
+ const body = err2.responseBody ?? "";
4368
+ const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
4369
+ if (nf) {
4370
+ return {
4371
+ stopReason: "error",
4372
+ errorKind: "model_not_found",
4373
+ rawError: modelNotFoundMessage(nf)
4374
+ };
4375
+ }
4376
+ const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
4377
+ if (rl) {
4378
+ return {
4379
+ stopReason: "error",
4380
+ errorKind: "rate_limit",
4381
+ retryAfterSec: rl.retryAfterSec,
4382
+ rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
4383
+ };
4384
+ }
4385
+ return {
4386
+ stopReason: "error",
4387
+ rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
4388
+ };
4389
+ }
4390
+ function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
4391
+ if (response.errorKind === "rate_limit") {
4392
+ return new Error(
4393
+ rateLimitMessage({
4394
+ provider: providerName,
4395
+ retryAfterSec: response.retryAfterSec,
4396
+ rawBody: response.rawError ?? "",
4397
+ retryAlreadyAttempted: retriedRateLimit,
4398
+ retrySkippedReason,
4399
+ maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
4400
+ })
4401
+ );
4402
+ }
4403
+ if (response.errorKind === "model_not_found") {
4404
+ return new Error(response.rawError ?? "planner: model not found");
4405
+ }
4406
+ const base = response.rawError ?? "planner: provider error";
4407
+ return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
4408
+ }
4409
+ var VercelRuntime = class {
4410
+ kind = "vercel";
4411
+ providerName;
4412
+ modelId;
4413
+ languageModel;
4414
+ constructor(provider, modelId, apiKeyOrModel, injectModel) {
4415
+ this.providerName = provider;
4416
+ this.modelId = modelId;
4417
+ if (injectModel === true) {
4418
+ if (typeof apiKeyOrModel === "string") {
4419
+ throw new Error("VercelRuntime: injectModel mode expects a LanguageModelV1 as third argument");
4420
+ }
4421
+ this.languageModel = apiKeyOrModel;
4422
+ return;
4423
+ }
4424
+ if (typeof apiKeyOrModel !== "string") {
4425
+ throw new Error("VercelRuntime: apiKey must be a string unless injectModel is true");
4426
+ }
4427
+ this.languageModel = resolveModel(provider, modelId, apiKeyOrModel).model;
4428
+ }
4429
+ async runScout(input5) {
4430
+ let attempt = 0;
4431
+ while (true) {
4432
+ try {
4433
+ const { object, usage, providerMetadata } = await generateObject({
4434
+ model: this.languageModel,
4435
+ system: input5.systemPrompt,
4436
+ prompt: input5.userMessage,
4437
+ schema: input5.schema,
4438
+ maxTokens: input5.maxOutputTokens,
4439
+ abortSignal: input5.abort
4440
+ });
4441
+ const u = usageFromLanguageModelStep(usage, providerMetadata);
4442
+ return {
4443
+ output: object,
4444
+ usage: u
4445
+ };
4446
+ } catch (e) {
4447
+ if (NoObjectGeneratedError.isInstance(e)) {
4448
+ return null;
4449
+ }
4450
+ if (APICallError.isInstance(e) && attempt === 0) {
4451
+ const status = e.statusCode ?? 0;
4452
+ if (status === 429) {
4453
+ const ra = parseRetryAfterSec(e.responseHeaders ?? {});
4454
+ if (ra !== void 0 && ra <= 90) {
4455
+ await new Promise((r) => setTimeout(r, ra * 1e3));
4456
+ attempt += 1;
4457
+ continue;
4458
+ }
4459
+ }
4460
+ }
4461
+ throw e;
4462
+ }
4463
+ }
4464
+ }
4465
+ async runDraft(input5) {
4466
+ const vl = input5.vercelLoop;
4467
+ if (!vl) {
4468
+ throw new Error("planner: Vercel runtime requires vercelLoop context");
4469
+ }
4470
+ const bus = input5.bus;
4471
+ const runId = input5.runId;
4472
+ const sleepFn = vl.sleep ?? defaultSleep;
4473
+ const baseMaxIter = input5.maxSteps ?? DEFAULT_PLANNER_MAX_ITERATIONS;
4474
+ const maxIter = { current: baseMaxIter };
4475
+ const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
4476
+ const maxOut = { current: baseMaxOut };
4477
+ const turns = [{ role: "user", text: vl.userPrompt }];
4478
+ if (vl.scoutedSection?.trim()) {
4479
+ turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: vl.scoutedSection }) });
4480
+ }
4481
+ let accumulatedText = "";
4482
+ let iterations = 0;
4483
+ let budgetExhausted = false;
4484
+ let finishedNormally = false;
4485
+ const decide = vl.decideOnLimit;
4486
+ const limitCtx = (kind) => ({
4487
+ kind,
4488
+ budgetSnapshot: input5.budget.snapshot(),
4489
+ iterations,
4490
+ maxIterations: maxIter.current,
4491
+ maxOutputTokens: maxOut.current
4492
+ });
4493
+ const extendAllSessionLimits = () => {
4494
+ input5.budget.extendSession();
4495
+ maxIter.current += baseMaxIter;
4496
+ maxOut.current += baseMaxOut;
4497
+ };
4498
+ for (; ; ) {
4499
+ if (input5.abort?.aborted) {
4500
+ bus.emit({ kind: "cancelled", runId });
4501
+ return {
4502
+ text: accumulatedText,
4503
+ finishedNormally: false,
4504
+ iterations,
4505
+ finalUsage: input5.budget.snapshot().usage,
4506
+ budgetExhausted,
4507
+ userCancelled: true
4508
+ };
4509
+ }
4510
+ if (iterations >= maxIter.current) {
4511
+ if (decide) {
4512
+ const d = await decide(limitCtx("max_iterations"));
4513
+ if (d === "cancel") {
4514
+ return {
4515
+ text: accumulatedText,
4516
+ finishedNormally: false,
4517
+ incompleteKind: "max_iterations",
4518
+ iterations,
4519
+ finalUsage: input5.budget.snapshot().usage,
4520
+ budgetExhausted,
4521
+ userCancelled: true
4522
+ };
4523
+ }
4524
+ extendAllSessionLimits();
4525
+ continue;
4526
+ }
4527
+ return {
4528
+ text: accumulatedText,
4529
+ finishedNormally: false,
4530
+ incompleteKind: "max_iterations",
4531
+ iterations,
4532
+ finalUsage: input5.budget.snapshot().usage,
4533
+ budgetExhausted
4534
+ };
4535
+ }
4536
+ iterations += 1;
4537
+ if (input5.budget.timedOut()) {
4538
+ if (decide) {
4539
+ const d = await decide(limitCtx("wall_clock"));
4540
+ if (d === "cancel") {
4541
+ return {
4542
+ text: accumulatedText,
4543
+ finishedNormally: false,
4544
+ incompleteKind: "wall_clock",
4545
+ iterations,
4546
+ finalUsage: input5.budget.snapshot().usage,
4547
+ budgetExhausted,
4548
+ timedOut: true,
4549
+ userCancelled: true
4550
+ };
4551
+ }
4552
+ extendAllSessionLimits();
4553
+ iterations -= 1;
4554
+ continue;
4555
+ }
4556
+ return {
4557
+ text: accumulatedText,
4558
+ finishedNormally: false,
4559
+ iterations,
4560
+ finalUsage: input5.budget.snapshot().usage,
4561
+ budgetExhausted,
4562
+ timedOut: true
4563
+ };
4564
+ }
4565
+ if (iterations >= 2) {
4566
+ bus.emit({ kind: "turn_started", runId, turn: iterations });
4567
+ }
4568
+ const toolDefs = buildPlannerToolDefinitions({
4569
+ root: vl.root,
4570
+ budget: input5.budget,
4571
+ enabled: vl.toolsEnabled,
4572
+ getTurn: () => iterations,
4573
+ runId,
4574
+ bus,
4575
+ onToolCall: input5.onToolCall,
4576
+ decideOnLimit: vl.decideOnLimit,
4577
+ getLimitCtx: () => limitCtx("file_or_context_reads"),
4578
+ extendSessionLimits: extendAllSessionLimits,
4579
+ getAccumulatedText: () => accumulatedText,
4580
+ setBudgetExhausted: (v) => {
4581
+ budgetExhausted = v;
4582
+ }
4583
+ });
4584
+ const tools = vercelToolsFromDefinitions(toolDefs);
4585
+ const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
4586
+ cacheEnabled: vl.cacheEnabled,
4587
+ provider: vl.provider
4588
+ });
4589
+ bus.emit({ kind: "request_sent", runId, turn: iterations });
4590
+ let attempt = 0;
4591
+ let stepResult;
4592
+ let retrySkippedReason;
4593
+ let retriedRateLimit = false;
4594
+ const callStream = async () => {
4595
+ const { streamText } = await import("ai");
4596
+ const stream = streamText({
4597
+ model: this.languageModel,
4598
+ messages,
4599
+ tools,
4600
+ toolChoice: "auto",
4601
+ maxSteps: 1,
4602
+ maxRetries: 0,
4603
+ maxTokens: maxOut.current,
4604
+ abortSignal: input5.abort,
4605
+ onChunk: ({ chunk }) => {
4606
+ if (chunk.type === "text-delta") {
4607
+ accumulatedText += chunk.textDelta;
4608
+ input5.onAssistantText?.(chunk.textDelta);
4609
+ bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
4610
+ }
4611
+ },
4612
+ onStepFinish: (step2) => {
4613
+ const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
4614
+ recordUsageAndEmit(bus, input5.onUsage, input5.budget, runId, iterations, u);
4615
+ }
4616
+ });
4617
+ for await (const part of stream.fullStream) {
4618
+ const p = part;
4619
+ if (p.type === "error") {
4620
+ throw p.error;
4621
+ }
4622
+ }
4623
+ const steps = await stream.steps;
4624
+ const last = steps[steps.length - 1];
4625
+ if (!last) throw new Error("planner: empty model step");
4626
+ stepResult = {
4627
+ text: last.text,
4628
+ finishReason: last.finishReason,
4629
+ toolCalls: last.toolCalls.flatMap(
4630
+ (tc) => tc ? [
4631
+ {
4632
+ toolCallId: tc.toolCallId,
4633
+ toolName: tc.toolName,
4634
+ args: tc.args
4635
+ }
4636
+ ] : []
4637
+ ),
4638
+ toolResults: last.toolResults.flatMap(
4639
+ (tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
4640
+ ),
4641
+ usage: last.usage,
4642
+ providerMetadata: last.providerMetadata
4643
+ };
4644
+ };
4645
+ try {
4646
+ while (true) {
4647
+ try {
4648
+ await callStream();
4649
+ break;
4650
+ } catch (e) {
4651
+ if (PlannerUserCancelledError.is(e)) {
4652
+ return {
4653
+ text: e.planText,
4654
+ finishedNormally: false,
4655
+ iterations,
4656
+ finalUsage: input5.budget.snapshot().usage,
4657
+ budgetExhausted: false,
4658
+ userCancelled: true,
4659
+ incompleteKind: "budget_reads"
4660
+ };
4661
+ }
4662
+ if (isAbortError(e)) {
4663
+ bus.emit({ kind: "cancelled", runId });
4664
+ return {
4665
+ text: accumulatedText,
4666
+ finishedNormally: false,
4667
+ iterations,
4668
+ finalUsage: input5.budget.snapshot().usage,
4669
+ budgetExhausted,
4670
+ userCancelled: true
4671
+ };
4672
+ }
4673
+ if (APICallError.isInstance(e)) {
4674
+ const synthetic = providerResponseFromApiError(vl.provider, vl.modelId, e);
4675
+ if (synthetic.errorKind === "rate_limit") {
4676
+ const asked = synthetic.retryAfterSec;
4677
+ const emitRateLimit = (phase, waitSec) => {
4678
+ bus.emit({
4679
+ kind: "rate_limit",
4680
+ runId,
4681
+ turn: iterations,
4682
+ retryAfterSec: synthetic.retryAfterSec,
4683
+ waitSec,
4684
+ capSec: MAX_RATE_LIMIT_RETRY_SEC,
4685
+ phase,
4686
+ provider: vl.provider,
4687
+ rawBody: (synthetic.rawError ?? "").slice(0, 200)
4688
+ });
4689
+ };
4690
+ if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
4691
+ emitRateLimit("aborted", asked);
4692
+ retrySkippedReason = "retry_after_too_long";
4693
+ throw composePlannerError(vl.provider, synthetic, false, retrySkippedReason);
4694
+ }
4695
+ if (attempt === 0) {
4696
+ const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
4697
+ emitRateLimit("retrying", waitSec);
4698
+ vl.onRateLimit?.(waitSec);
4699
+ await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
4700
+ attempt += 1;
4701
+ retriedRateLimit = true;
4702
+ continue;
4703
+ }
4704
+ throw composePlannerError(vl.provider, synthetic, true, retrySkippedReason);
4705
+ }
4706
+ throw composePlannerError(vl.provider, synthetic, retriedRateLimit, retrySkippedReason);
4707
+ }
4708
+ throw e;
4709
+ }
4710
+ }
4711
+ } catch (e) {
4712
+ if (isAbortError(e)) {
4713
+ bus.emit({ kind: "cancelled", runId });
4714
+ return {
4715
+ text: accumulatedText,
4716
+ finishedNormally: false,
4717
+ iterations,
4718
+ finalUsage: input5.budget.snapshot().usage,
4719
+ budgetExhausted,
4720
+ userCancelled: true
4721
+ };
4722
+ }
4723
+ throw e;
4724
+ }
4725
+ const finishReason = stepResult.finishReason;
4726
+ const toolCallsLen = stepResult.toolCalls.length;
4727
+ if (finishReason === "length" && toolCallsLen === 0) {
4728
+ bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
4729
+ if (decide) {
4730
+ const d = await decide(limitCtx("max_output_tokens"));
4731
+ if (d === "cancel") {
4732
+ return {
4733
+ text: accumulatedText,
4734
+ finishedNormally: false,
4735
+ incompleteKind: "max_output_tokens",
4736
+ iterations,
4737
+ finalUsage: input5.budget.snapshot().usage,
4738
+ budgetExhausted,
4739
+ userCancelled: true
4740
+ };
4741
+ }
4742
+ extendAllSessionLimits();
4743
+ applyStepToTurns(turns, stepResult);
4744
+ turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
4745
+ iterations -= 1;
4746
+ continue;
4747
+ }
4748
+ return {
4749
+ text: accumulatedText,
4750
+ finishedNormally: false,
4751
+ incompleteKind: "max_output_tokens",
4752
+ iterations,
4753
+ finalUsage: input5.budget.snapshot().usage,
4754
+ budgetExhausted
4755
+ };
4756
+ }
4757
+ if (finishReason === "error" || finishReason === "content-filter") {
4758
+ throw new Error(
4759
+ stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
4760
+ );
4761
+ }
4762
+ if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
4763
+ bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
4764
+ finishedNormally = true;
4765
+ return {
4766
+ text: accumulatedText,
4767
+ finishedNormally,
4768
+ iterations,
4769
+ finalUsage: input5.budget.snapshot().usage,
4770
+ budgetExhausted
4771
+ };
4772
+ }
4773
+ if (toolCallsLen === 0) {
4774
+ bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
4775
+ finishedNormally = true;
4776
+ return {
4777
+ text: accumulatedText,
4778
+ finishedNormally,
4779
+ iterations,
4780
+ finalUsage: input5.budget.snapshot().usage,
4781
+ budgetExhausted
4782
+ };
4783
+ }
4784
+ applyStepToTurns(turns, stepResult);
4785
+ if (budgetExhausted) {
4786
+ turns.push({
4787
+ role: "user",
4788
+ text: "Budget is exhausted. Finalise the plan with the information you already have. Do not call any more tools. Output the complete plan markdown now."
4789
+ });
4790
+ }
4791
+ bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
4792
+ }
4793
+ }
4794
+ };
4795
+
4796
+ // src/planner/events.ts
4797
+ var PlannerEventBus = class {
4798
+ listeners = /* @__PURE__ */ new Set();
4799
+ emit(e) {
4800
+ for (const fn of this.listeners) {
4801
+ try {
4802
+ fn(e);
4803
+ } catch {
4804
+ }
4805
+ }
4806
+ }
4807
+ subscribe(fn) {
4199
4808
  this.listeners.add(fn);
4200
4809
  return () => this.listeners.delete(fn);
4201
4810
  }
4202
4811
  };
4203
4812
 
4204
4813
  // src/planner/stages/scout.ts
4205
- import { APICallError, generateObject, NoObjectGeneratedError } from "ai";
4814
+ import { APICallError as APICallError2, NoObjectGeneratedError as NoObjectGeneratedError2 } from "ai";
4815
+
4816
+ // src/planner/stages/scout-schema.ts
4206
4817
  import { z as z4 } from "zod";
4207
4818
  var ScoutOutputSchema = z4.object({
4208
4819
  selectedFiles: z4.array(z4.string()).min(1).max(25),
@@ -4216,32 +4827,34 @@ var ScoutOutputSchema = z4.object({
4216
4827
  })
4217
4828
  ).optional()
4218
4829
  });
4830
+
4831
+ // src/planner/stages/scout.ts
4219
4832
  async function runScout(input5) {
4220
4833
  const maxTokens = input5.maxTokens ?? 2048;
4221
- const anthropicThinking = input5.provider === "anthropic" ? { anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } } } : void 0;
4222
4834
  let attempt = 0;
4223
4835
  while (true) {
4224
4836
  try {
4225
- const { object, usage, providerMetadata } = await generateObject({
4226
- model: input5.model,
4227
- system: input5.systemPrompt,
4228
- prompt: input5.userPrompt,
4837
+ const result = await input5.runtime.runScout({
4838
+ systemPrompt: input5.systemPrompt,
4839
+ userMessage: input5.userPrompt,
4229
4840
  schema: ScoutOutputSchema,
4230
- maxTokens,
4231
- abortSignal: input5.abort,
4232
- ...anthropicThinking ? { providerOptions: anthropicThinking } : {}
4841
+ bus: input5.bus,
4842
+ runId: input5.runId,
4843
+ abort: input5.abort,
4844
+ maxOutputTokens: maxTokens,
4845
+ providerSpecific: input5.providerSpecific
4233
4846
  });
4234
- const u = usageFromLanguageModelStep(usage, providerMetadata);
4235
- input5.budget.recordUsage(u);
4847
+ if (!result) return null;
4848
+ input5.budget.recordUsage(result.usage);
4236
4849
  return {
4237
- output: object,
4238
- usage: { inputTokens: u.inputTokens, outputTokens: u.outputTokens }
4850
+ output: result.output,
4851
+ usage: { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens }
4239
4852
  };
4240
4853
  } catch (e) {
4241
- if (NoObjectGeneratedError.isInstance(e)) {
4854
+ if (NoObjectGeneratedError2.isInstance(e)) {
4242
4855
  return null;
4243
4856
  }
4244
- if (APICallError.isInstance(e) && attempt === 0) {
4857
+ if (APICallError2.isInstance(e) && attempt === 0) {
4245
4858
  const status = e.statusCode ?? 0;
4246
4859
  if (status === 429) {
4247
4860
  const ra = parseRetryAfterSec(e.responseHeaders ?? {});
@@ -4553,24 +5166,6 @@ function validatePlan(input5) {
4553
5166
  }
4554
5167
 
4555
5168
  // src/planner/loop.ts
4556
- var MAX_RATE_LIMIT_RETRY_SEC = 90;
4557
- var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
4558
- function isAbortError(e) {
4559
- return e instanceof Error && e.name === "AbortError";
4560
- }
4561
- async function sleepWithAbort(ms, sleepFn, abort) {
4562
- if (!abort) {
4563
- await sleepFn(ms);
4564
- return;
4565
- }
4566
- if (abort.aborted) throw new DOMException("Aborted", "AbortError");
4567
- await Promise.race([
4568
- sleepFn(ms),
4569
- new Promise((_, reject) => {
4570
- abort.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
4571
- })
4572
- ]);
4573
- }
4574
5169
  function buildPlannerRunStats(budget, turns, runStartedAt) {
4575
5170
  const u = budget.snapshot().usage;
4576
5171
  const cacheRead = u.cacheReadTokens ?? 0;
@@ -4726,450 +5321,81 @@ function scoutStageStats(usage, durationMs) {
4726
5321
  inputTokens: usage.inputTokens,
4727
5322
  outputTokens: usage.outputTokens,
4728
5323
  cacheCreationTokens: 0,
4729
- cacheReadTokens: 0,
4730
- cacheHitRatio: 0,
4731
- durationMs
4732
- };
4733
- }
4734
- function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
4735
- input5.budget.extendSession();
4736
- maxIter.current += baseMaxIter;
4737
- maxOut.current += baseMaxOut;
4738
- }
4739
- function recordUsageAndEmit(bus, input5, runId, turn, usage) {
4740
- input5.budget.recordUsage(usage);
4741
- input5.onUsage?.(usage);
4742
- bus.emit({ kind: "usage", runId, turn, usage });
4743
- const u = input5.budget.snapshot().usage;
4744
- const cacheRead = u.cacheReadTokens ?? 0;
4745
- const cacheCreate = u.cacheCreationTokens ?? 0;
4746
- if (cacheRead > 0 || cacheCreate > 0) {
4747
- const inTok = u.inputTokens;
4748
- const totalInput = inTok + cacheRead;
4749
- const cacheHitRatio = totalInput === 0 ? 0 : Math.round(cacheRead / totalInput * 100) / 100;
4750
- bus.emit({
4751
- kind: "cache_summary",
4752
- runId,
4753
- turn,
4754
- cacheHitRatio,
4755
- cacheReadTokens: cacheRead,
4756
- cacheCreationTokens: cacheCreate
4757
- });
4758
- }
4759
- }
4760
- function cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt) {
4761
- bus.emit({ kind: "cancelled", runId });
4762
- return {
4763
- planText: accumulatedText,
4764
- budgetExhausted,
4765
- timedOut: false,
4766
- finishedNormally: false,
4767
- iterations,
4768
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4769
- userCancelled: true
4770
- };
4771
- }
4772
- function asToolInput(args) {
4773
- if (args && typeof args === "object" && !Array.isArray(args)) return args;
4774
- if (typeof args === "string") {
4775
- try {
4776
- return JSON.parse(args);
4777
- } catch {
4778
- return {};
4779
- }
4780
- }
4781
- return {};
4782
- }
4783
- function normalizeToolExecutionResult(result) {
4784
- if (typeof result === "string") {
4785
- try {
4786
- const j = JSON.parse(result);
4787
- if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
4788
- const o = j;
4789
- return {
4790
- content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4791
- isError: o.isError === true
4792
- };
4793
- }
4794
- } catch {
4795
- }
4796
- return { content: result, isError: false };
4797
- }
4798
- if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
4799
- const o = result;
4800
- return {
4801
- content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4802
- isError: o.isError === true
4803
- };
4804
- }
4805
- return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
4806
- }
4807
- function applyStepToTurns(turns, step2) {
4808
- const callsById = /* @__PURE__ */ new Map();
4809
- const toolCalls = step2.toolCalls.map((tc) => {
4810
- callsById.set(tc.toolCallId, tc.toolName);
4811
- return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
4812
- });
4813
- turns.push({
4814
- role: "assistant",
4815
- ...step2.text ? { text: step2.text } : {},
4816
- ...toolCalls.length ? { toolCalls } : {}
4817
- });
4818
- if (step2.toolResults.length) {
4819
- const toolResults = step2.toolResults.map((tr) => {
4820
- const norm = normalizeToolExecutionResult(tr.result);
4821
- return {
4822
- toolCallId: tr.toolCallId,
4823
- name: callsById.get(tr.toolCallId) ?? "read_file",
4824
- content: norm.content,
4825
- ...norm.isError ? { isError: true } : {}
4826
- };
4827
- });
4828
- turns.push({ role: "user", toolResults });
4829
- }
4830
- }
4831
- function providerResponseFromApiError(provider, modelId, err2) {
4832
- const body = err2.responseBody ?? "";
4833
- const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
4834
- if (nf) {
4835
- return {
4836
- stopReason: "error",
4837
- errorKind: "model_not_found",
4838
- rawError: modelNotFoundMessage(nf)
4839
- };
4840
- }
4841
- const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
4842
- if (rl) {
4843
- return {
4844
- stopReason: "error",
4845
- errorKind: "rate_limit",
4846
- retryAfterSec: rl.retryAfterSec,
4847
- rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
4848
- };
4849
- }
4850
- return {
4851
- stopReason: "error",
4852
- rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
5324
+ cacheReadTokens: 0,
5325
+ cacheHitRatio: 0,
5326
+ durationMs
4853
5327
  };
4854
5328
  }
4855
5329
  async function runDraftStage(input5) {
4856
5330
  const runStartedAt = Date.now();
4857
5331
  const bus = input5.events ?? new PlannerEventBus();
4858
5332
  const runId = input5.runId ?? newRunId();
4859
- const cacheEnabled = input5.cacheEnabled ?? true;
4860
- const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
4861
- const maxIter = { current: baseMaxIter };
4862
- const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
4863
- const maxOut = { current: baseMaxOut };
4864
- const turns = [{ role: "user", text: input5.userPrompt }];
4865
- if (input5.scoutedSection?.trim()) {
4866
- turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: input5.scoutedSection }) });
4867
- }
4868
- let accumulatedText = "";
4869
- let iterations = 0;
4870
- let budgetExhausted = false;
4871
- let finishedNormally = false;
4872
- const decide = input5.decideOnLimit;
4873
- const sleepFn = input5.sleep ?? defaultSleep;
5333
+ const iterProxy = { current: 0 };
4874
5334
  const limitCtx = (kind) => ({
4875
5335
  kind,
4876
5336
  budgetSnapshot: input5.budget.snapshot(),
4877
- iterations,
4878
- maxIterations: maxIter.current,
4879
- maxOutputTokens: maxOut.current
5337
+ iterations: iterProxy.current,
5338
+ maxIterations: input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS,
5339
+ maxOutputTokens: input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS
4880
5340
  });
4881
- for (; ; ) {
4882
- if (input5.abort?.aborted) {
4883
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
4884
- }
4885
- if (iterations >= maxIter.current) {
4886
- if (decide) {
4887
- const d = await decide(limitCtx("max_iterations"));
4888
- if (d === "cancel") {
4889
- return {
4890
- planText: accumulatedText,
4891
- budgetExhausted,
4892
- timedOut: false,
4893
- finishedNormally: false,
4894
- iterations,
4895
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4896
- incompleteKind: "max_iterations",
4897
- userCancelled: true
4898
- };
4899
- }
4900
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
4901
- continue;
4902
- }
4903
- return {
4904
- planText: accumulatedText,
4905
- budgetExhausted,
4906
- timedOut: false,
4907
- finishedNormally: false,
4908
- iterations,
4909
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4910
- incompleteKind: "max_iterations"
4911
- };
4912
- }
4913
- iterations += 1;
4914
- if (input5.budget.timedOut()) {
4915
- if (decide) {
4916
- const d = await decide(limitCtx("wall_clock"));
4917
- if (d === "cancel") {
4918
- return {
4919
- planText: accumulatedText,
4920
- budgetExhausted,
4921
- timedOut: true,
4922
- finishedNormally: false,
4923
- iterations,
4924
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4925
- incompleteKind: "wall_clock",
4926
- userCancelled: true
4927
- };
4928
- }
4929
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
4930
- iterations -= 1;
4931
- continue;
4932
- }
4933
- return {
4934
- planText: accumulatedText,
4935
- budgetExhausted,
4936
- timedOut: true,
4937
- finishedNormally: false,
4938
- iterations,
4939
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
4940
- };
4941
- }
4942
- if (iterations >= 2) {
4943
- bus.emit({ kind: "turn_started", runId, turn: iterations });
4944
- }
4945
- const tools = buildPlannerTools({
5341
+ let budgetExhausted = false;
5342
+ const toolDefs = input5.runtime.kind === "agent-sdk" ? buildPlannerToolDefinitions({
5343
+ root: input5.root,
5344
+ budget: input5.budget,
5345
+ enabled: input5.toolsEnabled,
5346
+ getTurn: () => Math.max(1, iterProxy.current),
5347
+ runId,
5348
+ bus,
5349
+ onToolCall: input5.onToolCall,
5350
+ decideOnLimit: input5.decideOnLimit,
5351
+ getLimitCtx: () => limitCtx("file_or_context_reads"),
5352
+ extendSessionLimits: () => {
5353
+ input5.budget.extendSession();
5354
+ },
5355
+ getAccumulatedText: () => "",
5356
+ setBudgetExhausted: (v) => {
5357
+ budgetExhausted = v;
5358
+ }
5359
+ }) : [];
5360
+ const draft = await input5.runtime.runDraft({
5361
+ systemPrompt: input5.systemPrompt,
5362
+ userMessage: input5.userPrompt,
5363
+ tools: toolDefs,
5364
+ bus,
5365
+ runId,
5366
+ budget: input5.budget,
5367
+ abort: input5.abort,
5368
+ maxSteps: input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS,
5369
+ maxOutputTokens: input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS,
5370
+ providerSpecific: input5.anthropicProviderSpecific?.draft,
5371
+ onAssistantText: input5.onAssistantText,
5372
+ onToolCall: input5.onToolCall,
5373
+ onUsage: input5.onUsage,
5374
+ vercelLoop: input5.runtime instanceof VercelRuntime ? {
5375
+ model: input5.runtime.languageModel,
5376
+ provider: input5.provider,
5377
+ modelId: input5.modelId,
4946
5378
  root: input5.root,
4947
- budget: input5.budget,
4948
- enabled: {
4949
- grep: input5.toolsEnabled?.grep ?? true,
4950
- listDir: input5.toolsEnabled?.listDir ?? true,
4951
- rangedRead: input5.toolsEnabled?.rangedRead ?? true
4952
- },
4953
- readHooks: {
4954
- runId,
4955
- turn: iterations,
4956
- bus,
4957
- onToolCall: input5.onToolCall,
4958
- decideOnLimit: input5.decideOnLimit,
4959
- getLimitCtx: () => limitCtx("file_or_context_reads"),
4960
- extendSessionLimits: () => extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut),
4961
- getAccumulatedText: () => accumulatedText,
4962
- setBudgetExhausted: (v) => {
4963
- budgetExhausted = v;
4964
- }
4965
- }
4966
- });
4967
- const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
4968
- cacheEnabled,
4969
- provider: input5.provider
4970
- });
4971
- bus.emit({ kind: "request_sent", runId, turn: iterations });
4972
- let attempt = 0;
4973
- let stepResult;
4974
- let retrySkippedReason;
4975
- let retriedRateLimit = false;
4976
- const callStream = async () => {
4977
- const { streamText } = await import("ai");
4978
- const stream = streamText({
4979
- model: input5.model,
4980
- messages,
4981
- tools,
4982
- toolChoice: "auto",
4983
- maxSteps: 1,
4984
- maxRetries: 0,
4985
- maxTokens: maxOut.current,
4986
- abortSignal: input5.abort,
4987
- // ai@4.x defaults `temperature: 0` (prepareCallSettings) and @ai-sdk/anthropic@1.x
4988
- // always passes it to the wire unless extended thinking is on. Anthropic Opus 4.7+
4989
- // rejects temperature entirely. Enabling thinking on Anthropic strips temperature
4990
- // via the adapter and gives the planner reasoning budget for free.
4991
- ...input5.provider === "anthropic" && {
4992
- providerOptions: {
4993
- anthropic: { thinking: { type: "enabled", budgetTokens: 2048 } }
4994
- }
4995
- },
4996
- onChunk: ({ chunk }) => {
4997
- if (chunk.type === "text-delta") {
4998
- accumulatedText += chunk.textDelta;
4999
- input5.onAssistantText?.(chunk.textDelta);
5000
- bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
5001
- }
5002
- },
5003
- onStepFinish: (step2) => {
5004
- const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
5005
- recordUsageAndEmit(bus, input5, runId, iterations, u);
5006
- }
5007
- });
5008
- for await (const part of stream.fullStream) {
5009
- const p = part;
5010
- if (p.type === "error") {
5011
- throw p.error;
5012
- }
5013
- }
5014
- const steps = await stream.steps;
5015
- const last = steps[steps.length - 1];
5016
- if (!last) throw new Error("planner: empty model step");
5017
- stepResult = {
5018
- text: last.text,
5019
- finishReason: last.finishReason,
5020
- toolCalls: last.toolCalls.flatMap(
5021
- (tc) => tc ? [
5022
- {
5023
- toolCallId: tc.toolCallId,
5024
- toolName: tc.toolName,
5025
- args: tc.args
5026
- }
5027
- ] : []
5028
- ),
5029
- toolResults: last.toolResults.flatMap(
5030
- (tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
5031
- ),
5032
- usage: last.usage,
5033
- providerMetadata: last.providerMetadata
5034
- };
5035
- };
5036
- try {
5037
- while (true) {
5038
- try {
5039
- await callStream();
5040
- break;
5041
- } catch (e) {
5042
- if (PlannerUserCancelledError.is(e)) {
5043
- return {
5044
- planText: e.planText,
5045
- budgetExhausted: false,
5046
- timedOut: false,
5047
- finishedNormally: false,
5048
- iterations,
5049
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5050
- incompleteKind: "budget_reads",
5051
- userCancelled: true
5052
- };
5053
- }
5054
- if (isAbortError(e)) {
5055
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
5056
- }
5057
- if (APICallError2.isInstance(e)) {
5058
- const synthetic = providerResponseFromApiError(input5.provider, input5.modelId, e);
5059
- if (synthetic.errorKind === "rate_limit") {
5060
- const asked = synthetic.retryAfterSec;
5061
- const emitRateLimit = (phase, waitSec) => {
5062
- bus.emit({
5063
- kind: "rate_limit",
5064
- runId,
5065
- turn: iterations,
5066
- retryAfterSec: synthetic.retryAfterSec,
5067
- waitSec,
5068
- capSec: MAX_RATE_LIMIT_RETRY_SEC,
5069
- phase,
5070
- provider: input5.provider,
5071
- rawBody: (synthetic.rawError ?? "").slice(0, 200)
5072
- });
5073
- };
5074
- if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
5075
- emitRateLimit("aborted", asked);
5076
- retrySkippedReason = "retry_after_too_long";
5077
- throw composePlannerError(input5.provider, synthetic, false, retrySkippedReason);
5078
- }
5079
- if (attempt === 0) {
5080
- const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
5081
- emitRateLimit("retrying", waitSec);
5082
- input5.onRateLimit?.(waitSec);
5083
- await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
5084
- attempt += 1;
5085
- retriedRateLimit = true;
5086
- continue;
5087
- }
5088
- throw composePlannerError(input5.provider, synthetic, true, retrySkippedReason);
5089
- }
5090
- throw composePlannerError(input5.provider, synthetic, retriedRateLimit, retrySkippedReason);
5091
- }
5092
- throw e;
5093
- }
5094
- }
5095
- } catch (e) {
5096
- if (isAbortError(e)) {
5097
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
5098
- }
5099
- throw e;
5100
- }
5101
- const finishReason = stepResult.finishReason;
5102
- const toolCallsLen = stepResult.toolCalls.length;
5103
- if (finishReason === "length" && toolCallsLen === 0) {
5104
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
5105
- if (decide) {
5106
- const d = await decide(limitCtx("max_output_tokens"));
5107
- if (d === "cancel") {
5108
- return {
5109
- planText: accumulatedText,
5110
- budgetExhausted,
5111
- timedOut: false,
5112
- finishedNormally: false,
5113
- iterations,
5114
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5115
- incompleteKind: "max_output_tokens",
5116
- userCancelled: true
5117
- };
5118
- }
5119
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
5120
- applyStepToTurns(turns, stepResult);
5121
- turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
5122
- iterations -= 1;
5123
- continue;
5124
- }
5125
- return {
5126
- planText: accumulatedText,
5127
- budgetExhausted,
5128
- timedOut: false,
5129
- finishedNormally: false,
5130
- iterations,
5131
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5132
- incompleteKind: "max_output_tokens"
5133
- };
5134
- }
5135
- if (finishReason === "error" || finishReason === "content-filter") {
5136
- throw new Error(
5137
- stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
5138
- );
5139
- }
5140
- if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
5141
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5142
- finishedNormally = true;
5143
- return {
5144
- planText: accumulatedText,
5145
- budgetExhausted,
5146
- timedOut: false,
5147
- finishedNormally,
5148
- iterations,
5149
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
5150
- };
5151
- }
5152
- if (toolCallsLen === 0) {
5153
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5154
- finishedNormally = true;
5155
- return {
5156
- planText: accumulatedText,
5157
- budgetExhausted,
5158
- timedOut: false,
5159
- finishedNormally,
5160
- iterations,
5161
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
5162
- };
5163
- }
5164
- applyStepToTurns(turns, stepResult);
5165
- if (budgetExhausted) {
5166
- turns.push({
5167
- role: "user",
5168
- text: "Budget is exhausted. Finalise the plan with the information you already have. Do not call any more tools. Output the complete plan markdown now."
5169
- });
5170
- }
5171
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5172
- }
5379
+ userPrompt: input5.userPrompt,
5380
+ scoutedSection: input5.scoutedSection,
5381
+ cacheEnabled: input5.cacheEnabled ?? true,
5382
+ toolsEnabled: input5.toolsEnabled,
5383
+ decideOnLimit: input5.decideOnLimit,
5384
+ sleep: input5.sleep,
5385
+ onRateLimit: input5.onRateLimit
5386
+ } : void 0
5387
+ });
5388
+ const incompleteKind = draft.incompleteKind;
5389
+ return {
5390
+ planText: draft.text,
5391
+ budgetExhausted: draft.budgetExhausted ?? budgetExhausted,
5392
+ timedOut: draft.timedOut ?? false,
5393
+ finishedNormally: draft.finishedNormally,
5394
+ iterations: draft.iterations,
5395
+ incompleteKind,
5396
+ stats: buildPlannerRunStats(input5.budget, draft.iterations, runStartedAt),
5397
+ userCancelled: draft.userCancelled
5398
+ };
5173
5399
  }
5174
5400
  async function runPlanner(input5) {
5175
5401
  const bus = input5.events ?? new PlannerEventBus();
@@ -5180,7 +5406,8 @@ async function runPlanner(input5) {
5180
5406
  runId,
5181
5407
  provider: input5.provider,
5182
5408
  model: input5.modelId,
5183
- cacheEnabled
5409
+ cacheEnabled,
5410
+ plannerRuntime: input5.runtime.kind
5184
5411
  });
5185
5412
  const merged = { ...input5, events: bus, runId };
5186
5413
  const systemPrompt = input5.systemPrompt;
@@ -5188,11 +5415,11 @@ async function runPlanner(input5) {
5188
5415
  let scoutStats;
5189
5416
  let scoutedSection;
5190
5417
  const scoutOn = input5.stages?.scout?.enabled !== false;
5191
- const scoutModel = input5.stages?.scout?.model;
5418
+ const scoutRuntime = input5.stages?.scout?.runtime;
5192
5419
  const maxScoutFiles = input5.stages?.scout?.maxFiles ?? 12;
5193
5420
  const scoutMaxOut = input5.stages?.scout?.maxOutputTokens ?? 2048;
5194
5421
  const scoutModelIdStr = input5.stages?.scout?.modelId ?? "";
5195
- if (scoutOn && scoutModel) {
5422
+ if (scoutOn && scoutRuntime) {
5196
5423
  if (!input5.scoutSystemPrompt?.trim()) {
5197
5424
  throw new Error(
5198
5425
  "planner: scout enabled but `scoutSystemPrompt` was not provided. Compose it via composeScoutSystemPrompt() or set stages.scout.enabled = false."
@@ -5202,13 +5429,15 @@ async function runPlanner(input5) {
5202
5429
  bus.emit({ kind: "stage_started", runId, stage: "scout" });
5203
5430
  try {
5204
5431
  const scoutRes = await runScout({
5205
- model: scoutModel,
5206
- provider: input5.provider,
5432
+ runtime: scoutRuntime,
5207
5433
  systemPrompt: input5.scoutSystemPrompt,
5208
5434
  userPrompt: input5.userPrompt,
5209
5435
  budget: input5.budget,
5436
+ bus,
5437
+ runId,
5210
5438
  abort: input5.abort,
5211
- maxTokens: scoutMaxOut
5439
+ maxTokens: scoutMaxOut,
5440
+ providerSpecific: input5.anthropicProviderSpecific?.scout
5212
5441
  });
5213
5442
  const durationMs = Date.now() - t0;
5214
5443
  if (scoutRes) {
@@ -5254,7 +5483,7 @@ async function runPlanner(input5) {
5254
5483
  }
5255
5484
  } catch (err2) {
5256
5485
  const msg = err2 instanceof Error ? err2.message : String(err2);
5257
- if (APICallError2.isInstance(err2)) {
5486
+ if (APICallError3.isInstance(err2)) {
5258
5487
  const status = err2.statusCode ?? 0;
5259
5488
  if (status === 401 || status === 403) {
5260
5489
  throw err2;
@@ -5318,25 +5547,6 @@ async function runPlanner(input5) {
5318
5547
  validation: validationOn ? { issues, durationMs: validationDurationMs } : { issues: [], durationMs: 0 }
5319
5548
  };
5320
5549
  }
5321
- function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
5322
- if (response.errorKind === "rate_limit") {
5323
- return new Error(
5324
- rateLimitMessage({
5325
- provider: providerName,
5326
- retryAfterSec: response.retryAfterSec,
5327
- rawBody: response.rawError ?? "",
5328
- retryAlreadyAttempted: retriedRateLimit,
5329
- retrySkippedReason,
5330
- maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
5331
- })
5332
- );
5333
- }
5334
- if (response.errorKind === "model_not_found") {
5335
- return new Error(response.rawError ?? "planner: model not found");
5336
- }
5337
- const base = response.rawError ?? "planner: provider error";
5338
- return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
5339
- }
5340
5550
 
5341
5551
  // src/planner/planner-limit-messages.ts
5342
5552
  function printPlannerApiCostNotice() {
@@ -5410,19 +5620,260 @@ function printPlannerLimitNextSteps() {
5410
5620
  blank();
5411
5621
  }
5412
5622
 
5413
- // src/planner/providers/index.ts
5414
- import { createAnthropic } from "@ai-sdk/anthropic";
5415
- import { createOpenAI } from "@ai-sdk/openai";
5416
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
5417
- function resolveModel(provider, modelId, apiKey) {
5418
- switch (provider) {
5419
- case "anthropic":
5420
- return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
5421
- case "openai":
5422
- return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
5423
- case "google":
5424
- return { provider, model: createGoogleGenerativeAI({ apiKey })(modelId), modelId };
5623
+ // src/planner/runtimes/agent-sdk-runtime.ts
5624
+ import { z as z5 } from "zod";
5625
+
5626
+ // src/planner/runtimes/anthropic-options.ts
5627
+ function extractAnthropicProviderSpecific(cfg, phase, cliOverrides) {
5628
+ const opts = cfg.providerOptions?.anthropic;
5629
+ const phaseEffort = opts?.effortByPhase?.[phase];
5630
+ const baseEffort = opts?.effort ?? "medium";
5631
+ const effort = cliOverrides?.effort ?? phaseEffort ?? baseEffort;
5632
+ return {
5633
+ thinking: cliOverrides?.thinking === "off" ? "off" : opts?.thinking ?? "adaptive",
5634
+ thinkingBudget: opts?.thinkingBudget,
5635
+ effort
5636
+ };
5637
+ }
5638
+ function sdkEffortFromPlanner(effort) {
5639
+ if (effort === "minimal") return "low";
5640
+ if (effort === "high") return "high";
5641
+ return "medium";
5642
+ }
5643
+ function thinkingConfigFromProviderSpecific(ps) {
5644
+ if (!ps || ps.thinking === "off") {
5645
+ return { type: "disabled" };
5646
+ }
5647
+ if (ps.thinking === "disabled") {
5648
+ return { type: "disabled" };
5649
+ }
5650
+ if (ps.thinking === "enabled") {
5651
+ return { type: "enabled", budgetTokens: ps.thinkingBudget ?? 2048 };
5652
+ }
5653
+ return { type: ps.thinking ?? "adaptive" };
5654
+ }
5655
+
5656
+ // src/planner/runtimes/agent-sdk-runtime.ts
5657
+ var BUILTIN_TOOL_DENY = [
5658
+ "Read",
5659
+ "Write",
5660
+ "Edit",
5661
+ "Bash",
5662
+ "Grep",
5663
+ "Glob",
5664
+ "NotebookEdit",
5665
+ "WebFetch",
5666
+ "WebSearch"
5667
+ ];
5668
+ function streamEventTextDelta(ev) {
5669
+ if (!ev || typeof ev !== "object") return "";
5670
+ const o = ev;
5671
+ if (o.type === "content_block_delta") {
5672
+ const d = o.delta;
5673
+ if (d && d.type === "text_delta" && typeof d.text === "string") return d.text;
5674
+ }
5675
+ return "";
5676
+ }
5677
+ var AgentSdkRuntime = class {
5678
+ constructor(modelId, apiKey) {
5679
+ this.modelId = modelId;
5680
+ this.apiKey = apiKey;
5681
+ }
5682
+ modelId;
5683
+ apiKey;
5684
+ kind = "agent-sdk";
5685
+ providerName = "anthropic";
5686
+ async runDraft(input5) {
5687
+ const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
5688
+ const sdkTools = input5.tools.map(
5689
+ (t) => tool5(t.name, t.description, this.zodToFieldsObject(t.parameters), async (args) => {
5690
+ const r = await t.execute(args);
5691
+ if (typeof r === "string") return { content: [{ type: "text", text: r }] };
5692
+ return { content: [{ type: "text", text: r.content }], isError: r.isError };
5693
+ })
5694
+ );
5695
+ const mcpServer = createSdkMcpServer({
5696
+ name: "squad-kit-planner",
5697
+ version: "1.0.0",
5698
+ tools: sdkTools
5699
+ });
5700
+ const ps = input5.providerSpecific;
5701
+ const thinking = thinkingConfigFromProviderSpecific(ps);
5702
+ const effort = ps?.effort !== void 0 ? sdkEffortFromPlanner(ps.effort) : sdkEffortFromPlanner("medium");
5703
+ const ac = new AbortController();
5704
+ const onAbort = () => ac.abort();
5705
+ if (input5.abort) {
5706
+ if (input5.abort.aborted) ac.abort();
5707
+ else input5.abort.addEventListener("abort", onAbort, { once: true });
5708
+ }
5709
+ const queryOptions = {
5710
+ model: this.modelId,
5711
+ tools: [],
5712
+ disallowedTools: [...BUILTIN_TOOL_DENY],
5713
+ mcpServers: { "squad-kit-planner": mcpServer },
5714
+ systemPrompt: input5.systemPrompt,
5715
+ maxTurns: input5.maxSteps,
5716
+ includePartialMessages: true,
5717
+ persistSession: false,
5718
+ settingSources: [],
5719
+ abortController: ac,
5720
+ env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
5721
+ thinking,
5722
+ effort
5723
+ };
5724
+ let text = "";
5725
+ let assistantTurns = 0;
5726
+ let finalUsage = { inputTokens: 0, outputTokens: 0 };
5727
+ let finishedNormally = false;
5728
+ let incompleteKind;
5729
+ let userCancelled = false;
5730
+ try {
5731
+ const q = query({
5732
+ prompt: input5.userMessage,
5733
+ options: queryOptions
5734
+ });
5735
+ for await (const message of q) {
5736
+ const m = message;
5737
+ switch (m.type) {
5738
+ case "stream_event": {
5739
+ const delta = streamEventTextDelta(m.event);
5740
+ if (delta) {
5741
+ text += delta;
5742
+ input5.onAssistantText?.(delta);
5743
+ input5.bus.emit({ kind: "assistant_text", runId: input5.runId, turn: assistantTurns, delta });
5744
+ }
5745
+ break;
5746
+ }
5747
+ case "assistant": {
5748
+ assistantTurns += 1;
5749
+ input5.bus.emit({
5750
+ kind: "turn_complete",
5751
+ runId: input5.runId,
5752
+ turn: assistantTurns,
5753
+ stopReason: "tool_use_or_text"
5754
+ });
5755
+ break;
5756
+ }
5757
+ case "result": {
5758
+ if (typeof m.result === "string" && m.result && !text) text = m.result;
5759
+ finalUsage = usageFromAgentSdkResult(
5760
+ m.usage
5761
+ );
5762
+ const sub = m.subtype;
5763
+ if (sub === "success") {
5764
+ finishedNormally = true;
5765
+ } else if (sub === "error_max_turns") {
5766
+ incompleteKind = "max_iterations";
5767
+ finishedNormally = false;
5768
+ } else if (typeof sub === "string" && sub.startsWith("error")) {
5769
+ finishedNormally = false;
5770
+ }
5771
+ break;
5772
+ }
5773
+ default:
5774
+ break;
5775
+ }
5776
+ }
5777
+ } catch (e) {
5778
+ if (e instanceof Error && (e.name === "AbortError" || ac.signal.aborted)) {
5779
+ userCancelled = true;
5780
+ input5.bus.emit({ kind: "cancelled", runId: input5.runId });
5781
+ } else {
5782
+ throw e;
5783
+ }
5784
+ } finally {
5785
+ if (input5.abort) input5.abort.removeEventListener("abort", onAbort);
5786
+ }
5787
+ input5.budget.recordUsage(finalUsage);
5788
+ input5.onUsage?.(finalUsage);
5789
+ input5.bus.emit({ kind: "usage", runId: input5.runId, turn: assistantTurns, usage: finalUsage });
5790
+ return {
5791
+ text,
5792
+ finishedNormally: userCancelled ? false : finishedNormally,
5793
+ iterations: assistantTurns,
5794
+ incompleteKind,
5795
+ finalUsage,
5796
+ userCancelled: userCancelled || void 0
5797
+ };
5798
+ }
5799
+ async runScout(input5) {
5800
+ const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
5801
+ let scoutOutput = null;
5802
+ const schemaFields = this.zodToFieldsObject(input5.schema);
5803
+ const respondTool = tool5(
5804
+ "respond_with_scout_result",
5805
+ "Respond with the scout selection by calling this tool exactly once. Do not call any other tool.",
5806
+ schemaFields,
5807
+ async (args) => {
5808
+ scoutOutput = args;
5809
+ return { content: [{ type: "text", text: "OK \u2014 scout result captured." }] };
5810
+ }
5811
+ );
5812
+ const mcpServer = createSdkMcpServer({
5813
+ name: "squad-kit-scout",
5814
+ version: "1.0.0",
5815
+ tools: [respondTool]
5816
+ });
5817
+ const ps = input5.providerSpecific;
5818
+ const thinking = thinkingConfigFromProviderSpecific(ps);
5819
+ const effort = ps?.effort !== void 0 ? sdkEffortFromPlanner(ps.effort) : sdkEffortFromPlanner("minimal");
5820
+ const queryOptions = {
5821
+ model: this.modelId,
5822
+ tools: [],
5823
+ disallowedTools: [...BUILTIN_TOOL_DENY],
5824
+ mcpServers: { "squad-kit-scout": mcpServer },
5825
+ systemPrompt: input5.systemPrompt + "\n\nIMPORTANT: respond by calling the `respond_with_scout_result` tool with the JSON object \u2014 do not write the JSON as text.",
5826
+ maxTurns: 2,
5827
+ persistSession: false,
5828
+ settingSources: [],
5829
+ env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
5830
+ thinking,
5831
+ effort
5832
+ };
5833
+ let usage = { inputTokens: 0, outputTokens: 0 };
5834
+ const ac = new AbortController();
5835
+ const onAbort = () => ac.abort();
5836
+ if (input5.abort) {
5837
+ if (input5.abort.aborted) ac.abort();
5838
+ else input5.abort.addEventListener("abort", onAbort, { once: true });
5839
+ }
5840
+ try {
5841
+ const q = query({
5842
+ prompt: input5.userMessage,
5843
+ options: { ...queryOptions, abortController: ac }
5844
+ });
5845
+ for await (const message of q) {
5846
+ const m = message;
5847
+ if (m.type === "result") {
5848
+ usage = usageFromAgentSdkResult(
5849
+ m.usage
5850
+ );
5851
+ }
5852
+ }
5853
+ } finally {
5854
+ if (input5.abort) input5.abort.removeEventListener("abort", onAbort);
5855
+ }
5856
+ if (!scoutOutput) return null;
5857
+ const parsed = input5.schema.safeParse(scoutOutput);
5858
+ if (!parsed.success) return null;
5859
+ return { output: parsed.data, usage };
5860
+ }
5861
+ zodToFieldsObject(schema) {
5862
+ if (schema instanceof z5.ZodObject) {
5863
+ return schema.shape;
5864
+ }
5865
+ return { payload: schema };
5866
+ }
5867
+ };
5868
+
5869
+ // src/planner/runtimes/index.ts
5870
+ function resolveRuntime(input5) {
5871
+ if (input5.provider === "anthropic") {
5872
+ const choice = input5.anthropicRuntime ?? "agent-sdk";
5873
+ if (choice === "agent-sdk") return new AgentSdkRuntime(input5.modelId, input5.apiKey);
5874
+ return new VercelRuntime("anthropic", input5.modelId, input5.apiKey);
5425
5875
  }
5876
+ return new VercelRuntime(input5.provider, input5.modelId, input5.apiKey);
5426
5877
  }
5427
5878
 
5428
5879
  // src/planner/budget.ts
@@ -5672,6 +6123,9 @@ function formatPlannerCacheLine(opts) {
5672
6123
  if (!opts.cacheEnabled) {
5673
6124
  return "cache disabled";
5674
6125
  }
6126
+ if (opts.plannerRuntimeKind === "agent-sdk") {
6127
+ return "(agent-sdk: not exposed)";
6128
+ }
5675
6129
  const { stats } = opts;
5676
6130
  if (stats.cacheReadTokens > 0) {
5677
6131
  const pct = Math.round(stats.cacheHitRatio * 100);
@@ -5859,12 +6313,33 @@ async function emitViaApi(story, paths, config2, opts) {
5859
6313
  );
5860
6314
  }
5861
6315
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
5862
- const { model } = resolveModel(planner.provider, modelId, apiKey);
6316
+ const anthropicRuntimeChoice = opts.anthropicRuntime ?? planner.runtime?.anthropic ?? "agent-sdk";
6317
+ const draftRuntime = resolveRuntime({
6318
+ provider: planner.provider,
6319
+ modelId,
6320
+ apiKey,
6321
+ anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
6322
+ });
6323
+ const thinkingCli = opts.noThinking ? "off" : void 0;
6324
+ const anthropicProviderSpecific = planner.provider === "anthropic" ? {
6325
+ draft: extractAnthropicProviderSpecific(planner, "draft", {
6326
+ effort: opts.effort,
6327
+ thinking: thinkingCli
6328
+ }),
6329
+ scout: extractAnthropicProviderSpecific(planner, "scout", {
6330
+ effort: opts.scoutEffort,
6331
+ thinking: thinkingCli
6332
+ })
6333
+ } : void 0;
5863
6334
  const budget = new Budget(planner.budget);
5864
6335
  banner();
5865
6336
  step(`planning ${story.feature} / ${story.id}`);
5866
6337
  kv("provider", planner.provider);
5867
6338
  kv("model", modelId);
6339
+ kv(
6340
+ "runtime",
6341
+ `${draftRuntime.kind} \xB7 ${draftRuntime.providerName}` + (planner.provider === "anthropic" ? ` (${anthropicRuntimeChoice})` : "")
6342
+ );
5868
6343
  kv(
5869
6344
  "budget",
5870
6345
  `${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
@@ -5892,7 +6367,7 @@ async function emitViaApi(story, paths, config2, opts) {
5892
6367
  const strictValidation = !!opts.strictValidation || planner.validation?.strict === true;
5893
6368
  const maxScoutFiles = opts.maxScoutFiles ?? planner.stages?.scout?.maxFiles ?? 12;
5894
6369
  let scoutModelId = "";
5895
- let scoutModelHandle;
6370
+ let scoutRuntime;
5896
6371
  let scoutSystemPrompt;
5897
6372
  if (scoutEnabled) {
5898
6373
  scoutModelId = modelFor(
@@ -5901,7 +6376,12 @@ async function emitViaApi(story, paths, config2, opts) {
5901
6376
  planner.modelOverride,
5902
6377
  opts.scoutModel ?? planner.stages?.scout?.modelOverride
5903
6378
  );
5904
- scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
6379
+ scoutRuntime = resolveRuntime({
6380
+ provider: planner.provider,
6381
+ modelId: scoutModelId,
6382
+ apiKey,
6383
+ anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
6384
+ });
5905
6385
  scoutSystemPrompt = composeScoutSystemPrompt({
5906
6386
  projectRoots: config2.project.projectRoots ?? ["."],
5907
6387
  primaryLanguage: config2.project.primaryLanguage ?? "",
@@ -5990,9 +6470,10 @@ async function emitViaApi(story, paths, config2, opts) {
5990
6470
  try {
5991
6471
  result = await runPlanner({
5992
6472
  root: paths.root,
5993
- model,
6473
+ runtime: draftRuntime,
5994
6474
  provider: planner.provider,
5995
6475
  modelId,
6476
+ anthropicProviderSpecific,
5996
6477
  systemPrompt,
5997
6478
  userPrompt,
5998
6479
  budget,
@@ -6004,7 +6485,7 @@ async function emitViaApi(story, paths, config2, opts) {
6004
6485
  stages: {
6005
6486
  scout: {
6006
6487
  enabled: scoutEnabled,
6007
- model: scoutModelHandle,
6488
+ runtime: scoutRuntime,
6008
6489
  modelId: scoutModelId,
6009
6490
  maxFiles: maxScoutFiles,
6010
6491
  maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
@@ -6083,7 +6564,9 @@ async function emitViaApi(story, paths, config2, opts) {
6083
6564
  issuesCount: issueCount,
6084
6565
  issuesByKind,
6085
6566
  durationMs: result.validation?.durationMs
6086
- }
6567
+ },
6568
+ plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
6569
+ providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
6087
6570
  });
6088
6571
  } catch {
6089
6572
  }
@@ -6103,9 +6586,16 @@ async function emitViaApi(story, paths, config2, opts) {
6103
6586
  { key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
6104
6587
  {
6105
6588
  key: "tokens",
6106
- value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out`
6589
+ value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out` + (draftRuntime.kind === "agent-sdk" ? " (agent-sdk: aggregate-only)" : "")
6590
+ },
6591
+ {
6592
+ key: "cache",
6593
+ value: formatPlannerCacheLine({
6594
+ cacheEnabled,
6595
+ stats: result.stats,
6596
+ plannerRuntimeKind: draftRuntime.kind
6597
+ })
6107
6598
  },
6108
- { key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
6109
6599
  {
6110
6600
  key: "max out",
6111
6601
  value: `${planner.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS} tok/req`
@@ -6772,6 +7262,71 @@ async function checkPlannerCache(paths, ctx) {
6772
7262
  detail: `caching active \u2014 last run ${hitPct}% hit (${formatTokenK(stats.cacheReadTokens)} read, ${lastRun.provider}/${lastRun.model})`
6773
7263
  };
6774
7264
  }
7265
+ async function checkPlannerRuntimeInfo(_paths, ctx) {
7266
+ if (!ctx.config?.planner?.enabled) {
7267
+ return {
7268
+ id: "planner-runtime-info",
7269
+ name: "planner runtime (resolved)",
7270
+ status: "skip",
7271
+ detail: "planner disabled"
7272
+ };
7273
+ }
7274
+ const p = ctx.config.planner;
7275
+ const anth = p.provider === "anthropic" ? p.runtime?.anthropic ?? "agent-sdk" : "vercel (openai/google use Vercel AI SDK)";
7276
+ return {
7277
+ id: "planner-runtime-info",
7278
+ name: "planner runtime (resolved)",
7279
+ status: "ok",
7280
+ detail: `${p.provider} \u2192 ${String(anth)}`
7281
+ };
7282
+ }
7283
+ function anthropicPlanModelLooksPost47(modelId) {
7284
+ return /opus[-_]4[-._]?7/i.test(modelId) || /claude-opus-4-7/i.test(modelId);
7285
+ }
7286
+ async function checkPlannerAnthropicRuntimeModelFit(_paths, ctx) {
7287
+ const id = "planner-anthropic-runtime-model-fit";
7288
+ const name = "Anthropic Opus 4.7+ vs Vercel runtime";
7289
+ if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
7290
+ return { id, name, status: "skip", detail: "not applicable" };
7291
+ }
7292
+ const p = ctx.config.planner;
7293
+ if ((p.runtime?.anthropic ?? "agent-sdk") !== "vercel") {
7294
+ return { id, name, status: "ok", detail: "using Agent SDK or default" };
7295
+ }
7296
+ const planModel = modelFor("anthropic", "plan", p.modelOverride);
7297
+ if (!anthropicPlanModelLooksPost47(planModel)) {
7298
+ return { id, name, status: "ok", detail: `${planModel} is fine on the Vercel runtime` };
7299
+ }
7300
+ return {
7301
+ id,
7302
+ name,
7303
+ status: "warn",
7304
+ detail: `Plan model ${planModel} needs the Agent SDK request shape; config uses Vercel runtime.`,
7305
+ fixHint: "Remove planner.runtime.anthropic: vercel (default is agent-sdk), or set planner.modelOverride.anthropic to a pre-4.7 id (e.g. claude-sonnet-4-5-20250929)."
7306
+ };
7307
+ }
7308
+ async function checkAgentSdkBinaryPresent(_paths, ctx) {
7309
+ const id = "agent-sdk-binary-present";
7310
+ const name = "Anthropic Agent SDK install";
7311
+ if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
7312
+ return { id, name, status: "skip", detail: "not applicable" };
7313
+ }
7314
+ if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel") {
7315
+ return { id, name, status: "skip", detail: "vercel runtime selected" };
7316
+ }
7317
+ try {
7318
+ await import("@anthropic-ai/claude-agent-sdk");
7319
+ return { id, name, status: "ok", detail: "package resolves" };
7320
+ } catch (e) {
7321
+ return {
7322
+ id,
7323
+ name,
7324
+ status: "warn",
7325
+ detail: e.message.slice(0, 160),
7326
+ fixHint: "Run pnpm install / npm install in the squad-kit package so the Agent SDK (and its platform binary) is present."
7327
+ };
7328
+ }
7329
+ }
6775
7330
  async function checkTrackerConfig(_paths, ctx) {
6776
7331
  if (!ctx.config) {
6777
7332
  return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
@@ -6913,6 +7468,9 @@ async function runAllChecks(paths, ctx, fix) {
6913
7468
  await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
6914
7469
  await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
6915
7470
  await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
7471
+ await add("planner runtime (resolved)", () => checkPlannerRuntimeInfo(paths, ctx));
7472
+ await add("Anthropic Opus 4.7+ vs Vercel runtime", () => checkPlannerAnthropicRuntimeModelFit(paths, ctx));
7473
+ await add("Anthropic Agent SDK install", () => checkAgentSdkBinaryPresent(paths, ctx));
6916
7474
  await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
6917
7475
  await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
6918
7476
  await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
@@ -7367,7 +7925,7 @@ import { secureHeaders } from "hono/secure-headers";
7367
7925
 
7368
7926
  // src/console/api/stories.ts
7369
7927
  import fs37 from "fs";
7370
- import { z as z5 } from "zod";
7928
+ import { z as z6 } from "zod";
7371
7929
 
7372
7930
  // src/utils/path-safety.ts
7373
7931
  import path30 from "path";
@@ -7378,13 +7936,13 @@ function isInside(abs, parent) {
7378
7936
  }
7379
7937
 
7380
7938
  // src/console/api/stories.ts
7381
- var CreateBody = z5.object({
7382
- feature: z5.string().min(1).max(80),
7383
- title: z5.string().max(200).optional(),
7384
- trackerId: z5.string().max(80).optional()
7939
+ var CreateBody = z6.object({
7940
+ feature: z6.string().min(1).max(80),
7941
+ title: z6.string().max(200).optional(),
7942
+ trackerId: z6.string().max(80).optional()
7385
7943
  });
7386
- var PatchBody = z5.object({
7387
- intakeContent: z5.string().min(1).max(1e6)
7944
+ var PatchBody = z6.object({
7945
+ intakeContent: z6.string().min(1).max(1e6)
7388
7946
  });
7389
7947
  function safeFeatureParam(feature) {
7390
7948
  return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
@@ -7476,12 +8034,12 @@ function mountStoriesApi(app, opts) {
7476
8034
  // src/console/api/plans.ts
7477
8035
  import fs38 from "fs";
7478
8036
  import path31 from "path";
7479
- import { z as z6 } from "zod";
8037
+ import { z as z7 } from "zod";
7480
8038
  import { diffLines } from "diff";
7481
- var DiffQuery = z6.object({
7482
- feature: z6.string().min(1).max(80),
7483
- a: z6.string().min(1).max(200),
7484
- b: z6.string().min(1).max(200)
8039
+ var DiffQuery = z7.object({
8040
+ feature: z7.string().min(1).max(80),
8041
+ a: z7.string().min(1).max(200),
8042
+ b: z7.string().min(1).max(200)
7485
8043
  });
7486
8044
  function safeFeatureParam2(feature) {
7487
8045
  return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
@@ -7555,8 +8113,8 @@ function mountPlansApi(app, opts) {
7555
8113
  }
7556
8114
 
7557
8115
  // src/console/api/config.ts
7558
- import { z as z7 } from "zod";
7559
- var ConfigBody = z7.object({}).passthrough();
8116
+ import { z as z8 } from "zod";
8117
+ var ConfigBody = z8.object({}).passthrough();
7560
8118
  function mountConfigApi(app, opts) {
7561
8119
  app.get("/api/config", (c) => {
7562
8120
  const config2 = loadConfig(opts.paths.configFile);
@@ -7596,14 +8154,14 @@ function mountMetaApi(app, opts) {
7596
8154
 
7597
8155
  // src/console/api/runs.ts
7598
8156
  import { streamSSE } from "hono/streaming";
7599
- import { z as z8 } from "zod";
8157
+ import { z as z9 } from "zod";
7600
8158
  import path32 from "path";
7601
8159
  import fs39 from "fs";
7602
8160
  var active = /* @__PURE__ */ new Map();
7603
8161
  var EVENT_BUF_CAP = 512;
7604
- var StartBody = z8.object({
7605
- feature: z8.string().min(1),
7606
- storyId: z8.string().min(1)
8162
+ var StartBody = z9.object({
8163
+ feature: z9.string().min(1),
8164
+ storyId: z9.string().min(1)
7607
8165
  });
7608
8166
  function emptyStats(durationMs) {
7609
8167
  return {
@@ -7660,7 +8218,17 @@ function mountRunsApi(app, opts) {
7660
8218
  const cfgFresh = loadConfig(opts.paths.configFile);
7661
8219
  const planner = cfgFresh.planner;
7662
8220
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
7663
- const { model } = resolveModel(planner.provider, modelId, apiKey);
8221
+ const anthropicRuntimeChoice = planner.runtime?.anthropic ?? "agent-sdk";
8222
+ const draftRuntime = resolveRuntime({
8223
+ provider: planner.provider,
8224
+ modelId,
8225
+ apiKey,
8226
+ anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
8227
+ });
8228
+ const anthropicProviderSpecific = planner.provider === "anthropic" ? {
8229
+ draft: extractAnthropicProviderSpecific(planner, "draft"),
8230
+ scout: extractAnthropicProviderSpecific(planner, "scout")
8231
+ } : void 0;
7664
8232
  const budget = new Budget(planner.budget);
7665
8233
  const cacheEnabled = planner.cache?.enabled ?? true;
7666
8234
  const scoutEnabled = planner.stages?.scout?.enabled !== false;
@@ -7668,7 +8236,7 @@ function mountRunsApi(app, opts) {
7668
8236
  const strictValidation = planner.validation?.strict === true;
7669
8237
  const maxScoutFiles = planner.stages?.scout?.maxFiles ?? 12;
7670
8238
  let scoutModelId = "";
7671
- let scoutModelHandle;
8239
+ let scoutRuntime;
7672
8240
  let scoutSystemPrompt;
7673
8241
  if (scoutEnabled) {
7674
8242
  scoutModelId = modelFor(
@@ -7677,7 +8245,12 @@ function mountRunsApi(app, opts) {
7677
8245
  planner.modelOverride,
7678
8246
  planner.stages?.scout?.modelOverride
7679
8247
  );
7680
- scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
8248
+ scoutRuntime = resolveRuntime({
8249
+ provider: planner.provider,
8250
+ modelId: scoutModelId,
8251
+ apiKey,
8252
+ anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
8253
+ });
7681
8254
  }
7682
8255
  const repoMap = buildRepoMap(opts.paths.root, { format: "tree" });
7683
8256
  if (scoutEnabled) {
@@ -7702,9 +8275,10 @@ function mountRunsApi(app, opts) {
7702
8275
  try {
7703
8276
  result = await runPlanner({
7704
8277
  root: opts.paths.root,
7705
- model,
8278
+ runtime: draftRuntime,
7706
8279
  provider: planner.provider,
7707
8280
  modelId,
8281
+ anthropicProviderSpecific,
7708
8282
  systemPrompt,
7709
8283
  userPrompt,
7710
8284
  budget,
@@ -7716,7 +8290,7 @@ function mountRunsApi(app, opts) {
7716
8290
  stages: {
7717
8291
  scout: {
7718
8292
  enabled: scoutEnabled,
7719
- model: scoutModelHandle,
8293
+ runtime: scoutRuntime,
7720
8294
  modelId: scoutModelId,
7721
8295
  maxFiles: maxScoutFiles,
7722
8296
  maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
@@ -7806,7 +8380,9 @@ function mountRunsApi(app, opts) {
7806
8380
  issuesCount: issueCount,
7807
8381
  issuesByKind,
7808
8382
  durationMs: result.validation?.durationMs
7809
- }
8383
+ },
8384
+ plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
8385
+ providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
7810
8386
  });
7811
8387
  } catch {
7812
8388
  }
@@ -7879,7 +8455,7 @@ function mountRunsApi(app, opts) {
7879
8455
  }
7880
8456
 
7881
8457
  // src/console/api/secrets.ts
7882
- import { z as z9 } from "zod";
8458
+ import { z as z10 } from "zod";
7883
8459
 
7884
8460
  // src/core/mask-token.ts
7885
8461
  function maskToken(t) {
@@ -7917,26 +8493,26 @@ function mountSecretsApi(app, opts) {
7917
8493
  }
7918
8494
  });
7919
8495
  });
7920
- const PutBody = z9.object({
7921
- planner: z9.object({
7922
- anthropic: z9.string().optional(),
7923
- openai: z9.string().optional(),
7924
- google: z9.string().optional()
8496
+ const PutBody = z10.object({
8497
+ planner: z10.object({
8498
+ anthropic: z10.string().optional(),
8499
+ openai: z10.string().optional(),
8500
+ google: z10.string().optional()
7925
8501
  }).optional(),
7926
- tracker: z9.object({
7927
- jira: z9.object({
7928
- host: z9.string().optional(),
7929
- email: z9.string().optional(),
7930
- token: z9.string().optional()
8502
+ tracker: z10.object({
8503
+ jira: z10.object({
8504
+ host: z10.string().optional(),
8505
+ email: z10.string().optional(),
8506
+ token: z10.string().optional()
7931
8507
  }).optional(),
7932
- azure: z9.object({
7933
- organization: z9.string().optional(),
7934
- project: z9.string().optional(),
7935
- pat: z9.string().optional()
8508
+ azure: z10.object({
8509
+ organization: z10.string().optional(),
8510
+ project: z10.string().optional(),
8511
+ pat: z10.string().optional()
7936
8512
  }).optional(),
7937
- github: z9.object({
7938
- host: z9.string().optional(),
7939
- pat: z9.string().optional()
8513
+ github: z10.object({
8514
+ host: z10.string().optional(),
8515
+ pat: z10.string().optional()
7940
8516
  }).optional()
7941
8517
  }).optional()
7942
8518
  });
@@ -7976,12 +8552,12 @@ function mountSecretsApi(app, opts) {
7976
8552
  }
7977
8553
 
7978
8554
  // src/console/api/tracker.ts
7979
- import { z as z10 } from "zod";
7980
- var SearchQuery = z10.object({ q: z10.string().max(500) });
7981
- var ImportBody = z10.object({
7982
- issueId: z10.string().min(1),
7983
- feature: z10.string().min(1).max(80),
7984
- withAttachments: z10.boolean().optional()
8555
+ import { z as z11 } from "zod";
8556
+ var SearchQuery = z11.object({ q: z11.string().max(500) });
8557
+ var ImportBody = z11.object({
8558
+ issueId: z11.string().min(1),
8559
+ feature: z11.string().min(1).max(80),
8560
+ withAttachments: z11.boolean().optional()
7985
8561
  });
7986
8562
  function mountTrackerApi(app, opts) {
7987
8563
  app.get("/api/tracker/search", async (c) => {
@@ -8738,7 +9314,24 @@ program.command("new-plan [intake-path]").description("Generate a plan (via dire
8738
9314
  "--max-scout-files <n>",
8739
9315
  "Maximum files the scout can pre-select (default from config or 12)",
8740
9316
  (v) => parseInt(v, 10)
8741
- ).option("--no-validation", "Disable post-write validation", false).option("--strict-validation", "Write partial plan when validation finds issues", false).action(wrapArgs(runNewPlan));
9317
+ ).option("--no-validation", "Disable post-write validation", false).option("--strict-validation", "Write partial plan when validation finds issues", false).addOption(
9318
+ new Option("--anthropic-runtime <mode>", "Anthropic only: agent-sdk or legacy vercel AI SDK").choices([
9319
+ "agent-sdk",
9320
+ "vercel"
9321
+ ])
9322
+ ).addOption(
9323
+ new Option("--effort <name>", "Anthropic Agent SDK: draft-stage effort").choices([
9324
+ "minimal",
9325
+ "medium",
9326
+ "high"
9327
+ ])
9328
+ ).addOption(
9329
+ new Option("--scout-effort <name>", "Anthropic Agent SDK: scout-stage effort").choices([
9330
+ "minimal",
9331
+ "medium",
9332
+ "high"
9333
+ ])
9334
+ ).option("--no-thinking", "Anthropic Agent SDK: disable extended thinking for this run", false).action(wrapArgs(runNewPlan));
8742
9335
  program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
8743
9336
  program.command("doctor").description("Run a health check on the local .squad/ workspace and its external integrations").option("--fix", "Apply non-destructive repairs (gitignore, permissions, missing dirs)", false).option("--json", "Emit results as JSON to stdout (for scripting)", false).action(wrap(runDoctor));
8744
9337
  program.command("migrate").description("Apply one-shot structural migrations to bring .squad/ to the latest version").option("--dry-run", "Show what would change without touching the filesystem", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runMigrate));
@@ -8804,4 +9397,3 @@ function wrapArgs(fn) {
8804
9397
  }
8805
9398
  };
8806
9399
  }
8807
- //# sourceMappingURL=cli.js.map