squad-kit 0.9.0 → 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,30 +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
4834
  let attempt = 0;
4222
4835
  while (true) {
4223
4836
  try {
4224
- const { object, usage, providerMetadata } = await generateObject({
4225
- model: input5.model,
4226
- system: input5.systemPrompt,
4227
- prompt: input5.userPrompt,
4837
+ const result = await input5.runtime.runScout({
4838
+ systemPrompt: input5.systemPrompt,
4839
+ userMessage: input5.userPrompt,
4228
4840
  schema: ScoutOutputSchema,
4229
- maxTokens,
4230
- abortSignal: input5.abort
4841
+ bus: input5.bus,
4842
+ runId: input5.runId,
4843
+ abort: input5.abort,
4844
+ maxOutputTokens: maxTokens,
4845
+ providerSpecific: input5.providerSpecific
4231
4846
  });
4232
- const u = usageFromLanguageModelStep(usage, providerMetadata);
4233
- input5.budget.recordUsage(u);
4847
+ if (!result) return null;
4848
+ input5.budget.recordUsage(result.usage);
4234
4849
  return {
4235
- output: object,
4236
- usage: { inputTokens: u.inputTokens, outputTokens: u.outputTokens }
4850
+ output: result.output,
4851
+ usage: { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens }
4237
4852
  };
4238
4853
  } catch (e) {
4239
- if (NoObjectGeneratedError.isInstance(e)) {
4854
+ if (NoObjectGeneratedError2.isInstance(e)) {
4240
4855
  return null;
4241
4856
  }
4242
- if (APICallError.isInstance(e) && attempt === 0) {
4857
+ if (APICallError2.isInstance(e) && attempt === 0) {
4243
4858
  const status = e.statusCode ?? 0;
4244
4859
  if (status === 429) {
4245
4860
  const ra = parseRetryAfterSec(e.responseHeaders ?? {});
@@ -4551,24 +5166,6 @@ function validatePlan(input5) {
4551
5166
  }
4552
5167
 
4553
5168
  // src/planner/loop.ts
4554
- var MAX_RATE_LIMIT_RETRY_SEC = 90;
4555
- var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
4556
- function isAbortError(e) {
4557
- return e instanceof Error && e.name === "AbortError";
4558
- }
4559
- async function sleepWithAbort(ms, sleepFn, abort) {
4560
- if (!abort) {
4561
- await sleepFn(ms);
4562
- return;
4563
- }
4564
- if (abort.aborted) throw new DOMException("Aborted", "AbortError");
4565
- await Promise.race([
4566
- sleepFn(ms),
4567
- new Promise((_, reject) => {
4568
- abort.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
4569
- })
4570
- ]);
4571
- }
4572
5169
  function buildPlannerRunStats(budget, turns, runStartedAt) {
4573
5170
  const u = budget.snapshot().usage;
4574
5171
  const cacheRead = u.cacheReadTokens ?? 0;
@@ -4722,443 +5319,83 @@ function scoutStageStats(usage, durationMs) {
4722
5319
  return {
4723
5320
  turns: 0,
4724
5321
  inputTokens: usage.inputTokens,
4725
- outputTokens: usage.outputTokens,
4726
- cacheCreationTokens: 0,
4727
- cacheReadTokens: 0,
4728
- cacheHitRatio: 0,
4729
- durationMs
4730
- };
4731
- }
4732
- function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
4733
- input5.budget.extendSession();
4734
- maxIter.current += baseMaxIter;
4735
- maxOut.current += baseMaxOut;
4736
- }
4737
- function recordUsageAndEmit(bus, input5, runId, turn, usage) {
4738
- input5.budget.recordUsage(usage);
4739
- input5.onUsage?.(usage);
4740
- bus.emit({ kind: "usage", runId, turn, usage });
4741
- const u = input5.budget.snapshot().usage;
4742
- const cacheRead = u.cacheReadTokens ?? 0;
4743
- const cacheCreate = u.cacheCreationTokens ?? 0;
4744
- if (cacheRead > 0 || cacheCreate > 0) {
4745
- const inTok = u.inputTokens;
4746
- const totalInput = inTok + cacheRead;
4747
- const cacheHitRatio = totalInput === 0 ? 0 : Math.round(cacheRead / totalInput * 100) / 100;
4748
- bus.emit({
4749
- kind: "cache_summary",
4750
- runId,
4751
- turn,
4752
- cacheHitRatio,
4753
- cacheReadTokens: cacheRead,
4754
- cacheCreationTokens: cacheCreate
4755
- });
4756
- }
4757
- }
4758
- function cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt) {
4759
- bus.emit({ kind: "cancelled", runId });
4760
- return {
4761
- planText: accumulatedText,
4762
- budgetExhausted,
4763
- timedOut: false,
4764
- finishedNormally: false,
4765
- iterations,
4766
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4767
- userCancelled: true
4768
- };
4769
- }
4770
- function asToolInput(args) {
4771
- if (args && typeof args === "object" && !Array.isArray(args)) return args;
4772
- if (typeof args === "string") {
4773
- try {
4774
- return JSON.parse(args);
4775
- } catch {
4776
- return {};
4777
- }
4778
- }
4779
- return {};
4780
- }
4781
- function normalizeToolExecutionResult(result) {
4782
- if (typeof result === "string") {
4783
- try {
4784
- const j = JSON.parse(result);
4785
- if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
4786
- const o = j;
4787
- return {
4788
- content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4789
- isError: o.isError === true
4790
- };
4791
- }
4792
- } catch {
4793
- }
4794
- return { content: result, isError: false };
4795
- }
4796
- if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
4797
- const o = result;
4798
- return {
4799
- content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
4800
- isError: o.isError === true
4801
- };
4802
- }
4803
- return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
4804
- }
4805
- function applyStepToTurns(turns, step2) {
4806
- const callsById = /* @__PURE__ */ new Map();
4807
- const toolCalls = step2.toolCalls.map((tc) => {
4808
- callsById.set(tc.toolCallId, tc.toolName);
4809
- return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
4810
- });
4811
- turns.push({
4812
- role: "assistant",
4813
- ...step2.text ? { text: step2.text } : {},
4814
- ...toolCalls.length ? { toolCalls } : {}
4815
- });
4816
- if (step2.toolResults.length) {
4817
- const toolResults = step2.toolResults.map((tr) => {
4818
- const norm = normalizeToolExecutionResult(tr.result);
4819
- return {
4820
- toolCallId: tr.toolCallId,
4821
- name: callsById.get(tr.toolCallId) ?? "read_file",
4822
- content: norm.content,
4823
- ...norm.isError ? { isError: true } : {}
4824
- };
4825
- });
4826
- turns.push({ role: "user", toolResults });
4827
- }
4828
- }
4829
- function providerResponseFromApiError(provider, modelId, err2) {
4830
- const body = err2.responseBody ?? "";
4831
- const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
4832
- if (nf) {
4833
- return {
4834
- stopReason: "error",
4835
- errorKind: "model_not_found",
4836
- rawError: modelNotFoundMessage(nf)
4837
- };
4838
- }
4839
- const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
4840
- if (rl) {
4841
- return {
4842
- stopReason: "error",
4843
- errorKind: "rate_limit",
4844
- retryAfterSec: rl.retryAfterSec,
4845
- rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
4846
- };
4847
- }
4848
- return {
4849
- stopReason: "error",
4850
- rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
5322
+ outputTokens: usage.outputTokens,
5323
+ cacheCreationTokens: 0,
5324
+ cacheReadTokens: 0,
5325
+ cacheHitRatio: 0,
5326
+ durationMs
4851
5327
  };
4852
5328
  }
4853
5329
  async function runDraftStage(input5) {
4854
5330
  const runStartedAt = Date.now();
4855
5331
  const bus = input5.events ?? new PlannerEventBus();
4856
5332
  const runId = input5.runId ?? newRunId();
4857
- const cacheEnabled = input5.cacheEnabled ?? true;
4858
- const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
4859
- const maxIter = { current: baseMaxIter };
4860
- const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
4861
- const maxOut = { current: baseMaxOut };
4862
- const turns = [{ role: "user", text: input5.userPrompt }];
4863
- if (input5.scoutedSection?.trim()) {
4864
- turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: input5.scoutedSection }) });
4865
- }
4866
- let accumulatedText = "";
4867
- let iterations = 0;
4868
- let budgetExhausted = false;
4869
- let finishedNormally = false;
4870
- const decide = input5.decideOnLimit;
4871
- const sleepFn = input5.sleep ?? defaultSleep;
5333
+ const iterProxy = { current: 0 };
4872
5334
  const limitCtx = (kind) => ({
4873
5335
  kind,
4874
5336
  budgetSnapshot: input5.budget.snapshot(),
4875
- iterations,
4876
- maxIterations: maxIter.current,
4877
- maxOutputTokens: maxOut.current
5337
+ iterations: iterProxy.current,
5338
+ maxIterations: input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS,
5339
+ maxOutputTokens: input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS
4878
5340
  });
4879
- for (; ; ) {
4880
- if (input5.abort?.aborted) {
4881
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
4882
- }
4883
- if (iterations >= maxIter.current) {
4884
- if (decide) {
4885
- const d = await decide(limitCtx("max_iterations"));
4886
- if (d === "cancel") {
4887
- return {
4888
- planText: accumulatedText,
4889
- budgetExhausted,
4890
- timedOut: false,
4891
- finishedNormally: false,
4892
- iterations,
4893
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4894
- incompleteKind: "max_iterations",
4895
- userCancelled: true
4896
- };
4897
- }
4898
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
4899
- continue;
4900
- }
4901
- return {
4902
- planText: accumulatedText,
4903
- budgetExhausted,
4904
- timedOut: false,
4905
- finishedNormally: false,
4906
- iterations,
4907
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4908
- incompleteKind: "max_iterations"
4909
- };
4910
- }
4911
- iterations += 1;
4912
- if (input5.budget.timedOut()) {
4913
- if (decide) {
4914
- const d = await decide(limitCtx("wall_clock"));
4915
- if (d === "cancel") {
4916
- return {
4917
- planText: accumulatedText,
4918
- budgetExhausted,
4919
- timedOut: true,
4920
- finishedNormally: false,
4921
- iterations,
4922
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
4923
- incompleteKind: "wall_clock",
4924
- userCancelled: true
4925
- };
4926
- }
4927
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
4928
- iterations -= 1;
4929
- continue;
4930
- }
4931
- return {
4932
- planText: accumulatedText,
4933
- budgetExhausted,
4934
- timedOut: true,
4935
- finishedNormally: false,
4936
- iterations,
4937
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
4938
- };
4939
- }
4940
- if (iterations >= 2) {
4941
- bus.emit({ kind: "turn_started", runId, turn: iterations });
4942
- }
4943
- 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,
4944
5378
  root: input5.root,
4945
- budget: input5.budget,
4946
- enabled: {
4947
- grep: input5.toolsEnabled?.grep ?? true,
4948
- listDir: input5.toolsEnabled?.listDir ?? true,
4949
- rangedRead: input5.toolsEnabled?.rangedRead ?? true
4950
- },
4951
- readHooks: {
4952
- runId,
4953
- turn: iterations,
4954
- bus,
4955
- onToolCall: input5.onToolCall,
4956
- decideOnLimit: input5.decideOnLimit,
4957
- getLimitCtx: () => limitCtx("file_or_context_reads"),
4958
- extendSessionLimits: () => extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut),
4959
- getAccumulatedText: () => accumulatedText,
4960
- setBudgetExhausted: (v) => {
4961
- budgetExhausted = v;
4962
- }
4963
- }
4964
- });
4965
- const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
4966
- cacheEnabled,
4967
- provider: input5.provider
4968
- });
4969
- bus.emit({ kind: "request_sent", runId, turn: iterations });
4970
- let attempt = 0;
4971
- let stepResult;
4972
- let retrySkippedReason;
4973
- let retriedRateLimit = false;
4974
- const callStream = async () => {
4975
- const { streamText } = await import("ai");
4976
- const stream = streamText({
4977
- model: input5.model,
4978
- messages,
4979
- tools,
4980
- toolChoice: "auto",
4981
- maxSteps: 1,
4982
- maxRetries: 0,
4983
- maxTokens: maxOut.current,
4984
- abortSignal: input5.abort,
4985
- onChunk: ({ chunk }) => {
4986
- if (chunk.type === "text-delta") {
4987
- accumulatedText += chunk.textDelta;
4988
- input5.onAssistantText?.(chunk.textDelta);
4989
- bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
4990
- }
4991
- },
4992
- onStepFinish: (step2) => {
4993
- const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
4994
- recordUsageAndEmit(bus, input5, runId, iterations, u);
4995
- }
4996
- });
4997
- for await (const part of stream.fullStream) {
4998
- const p = part;
4999
- if (p.type === "error") {
5000
- throw p.error;
5001
- }
5002
- }
5003
- const steps = await stream.steps;
5004
- const last = steps[steps.length - 1];
5005
- if (!last) throw new Error("planner: empty model step");
5006
- stepResult = {
5007
- text: last.text,
5008
- finishReason: last.finishReason,
5009
- toolCalls: last.toolCalls.flatMap(
5010
- (tc) => tc ? [
5011
- {
5012
- toolCallId: tc.toolCallId,
5013
- toolName: tc.toolName,
5014
- args: tc.args
5015
- }
5016
- ] : []
5017
- ),
5018
- toolResults: last.toolResults.flatMap(
5019
- (tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
5020
- ),
5021
- usage: last.usage,
5022
- providerMetadata: last.providerMetadata
5023
- };
5024
- };
5025
- try {
5026
- while (true) {
5027
- try {
5028
- await callStream();
5029
- break;
5030
- } catch (e) {
5031
- if (PlannerUserCancelledError.is(e)) {
5032
- return {
5033
- planText: e.planText,
5034
- budgetExhausted: false,
5035
- timedOut: false,
5036
- finishedNormally: false,
5037
- iterations,
5038
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5039
- incompleteKind: "budget_reads",
5040
- userCancelled: true
5041
- };
5042
- }
5043
- if (isAbortError(e)) {
5044
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
5045
- }
5046
- if (APICallError2.isInstance(e)) {
5047
- const synthetic = providerResponseFromApiError(input5.provider, input5.modelId, e);
5048
- if (synthetic.errorKind === "rate_limit") {
5049
- const asked = synthetic.retryAfterSec;
5050
- const emitRateLimit = (phase, waitSec) => {
5051
- bus.emit({
5052
- kind: "rate_limit",
5053
- runId,
5054
- turn: iterations,
5055
- retryAfterSec: synthetic.retryAfterSec,
5056
- waitSec,
5057
- capSec: MAX_RATE_LIMIT_RETRY_SEC,
5058
- phase,
5059
- provider: input5.provider,
5060
- rawBody: (synthetic.rawError ?? "").slice(0, 200)
5061
- });
5062
- };
5063
- if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
5064
- emitRateLimit("aborted", asked);
5065
- retrySkippedReason = "retry_after_too_long";
5066
- throw composePlannerError(input5.provider, synthetic, false, retrySkippedReason);
5067
- }
5068
- if (attempt === 0) {
5069
- const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
5070
- emitRateLimit("retrying", waitSec);
5071
- input5.onRateLimit?.(waitSec);
5072
- await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
5073
- attempt += 1;
5074
- retriedRateLimit = true;
5075
- continue;
5076
- }
5077
- throw composePlannerError(input5.provider, synthetic, true, retrySkippedReason);
5078
- }
5079
- throw composePlannerError(input5.provider, synthetic, retriedRateLimit, retrySkippedReason);
5080
- }
5081
- throw e;
5082
- }
5083
- }
5084
- } catch (e) {
5085
- if (isAbortError(e)) {
5086
- return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
5087
- }
5088
- throw e;
5089
- }
5090
- const finishReason = stepResult.finishReason;
5091
- const toolCallsLen = stepResult.toolCalls.length;
5092
- if (finishReason === "length" && toolCallsLen === 0) {
5093
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
5094
- if (decide) {
5095
- const d = await decide(limitCtx("max_output_tokens"));
5096
- if (d === "cancel") {
5097
- return {
5098
- planText: accumulatedText,
5099
- budgetExhausted,
5100
- timedOut: false,
5101
- finishedNormally: false,
5102
- iterations,
5103
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5104
- incompleteKind: "max_output_tokens",
5105
- userCancelled: true
5106
- };
5107
- }
5108
- extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
5109
- applyStepToTurns(turns, stepResult);
5110
- turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
5111
- iterations -= 1;
5112
- continue;
5113
- }
5114
- return {
5115
- planText: accumulatedText,
5116
- budgetExhausted,
5117
- timedOut: false,
5118
- finishedNormally: false,
5119
- iterations,
5120
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
5121
- incompleteKind: "max_output_tokens"
5122
- };
5123
- }
5124
- if (finishReason === "error" || finishReason === "content-filter") {
5125
- throw new Error(
5126
- stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
5127
- );
5128
- }
5129
- if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
5130
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5131
- finishedNormally = true;
5132
- return {
5133
- planText: accumulatedText,
5134
- budgetExhausted,
5135
- timedOut: false,
5136
- finishedNormally,
5137
- iterations,
5138
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
5139
- };
5140
- }
5141
- if (toolCallsLen === 0) {
5142
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5143
- finishedNormally = true;
5144
- return {
5145
- planText: accumulatedText,
5146
- budgetExhausted,
5147
- timedOut: false,
5148
- finishedNormally,
5149
- iterations,
5150
- stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
5151
- };
5152
- }
5153
- applyStepToTurns(turns, stepResult);
5154
- if (budgetExhausted) {
5155
- turns.push({
5156
- role: "user",
5157
- 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."
5158
- });
5159
- }
5160
- bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
5161
- }
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
+ };
5162
5399
  }
5163
5400
  async function runPlanner(input5) {
5164
5401
  const bus = input5.events ?? new PlannerEventBus();
@@ -5169,7 +5406,8 @@ async function runPlanner(input5) {
5169
5406
  runId,
5170
5407
  provider: input5.provider,
5171
5408
  model: input5.modelId,
5172
- cacheEnabled
5409
+ cacheEnabled,
5410
+ plannerRuntime: input5.runtime.kind
5173
5411
  });
5174
5412
  const merged = { ...input5, events: bus, runId };
5175
5413
  const systemPrompt = input5.systemPrompt;
@@ -5177,11 +5415,11 @@ async function runPlanner(input5) {
5177
5415
  let scoutStats;
5178
5416
  let scoutedSection;
5179
5417
  const scoutOn = input5.stages?.scout?.enabled !== false;
5180
- const scoutModel = input5.stages?.scout?.model;
5418
+ const scoutRuntime = input5.stages?.scout?.runtime;
5181
5419
  const maxScoutFiles = input5.stages?.scout?.maxFiles ?? 12;
5182
5420
  const scoutMaxOut = input5.stages?.scout?.maxOutputTokens ?? 2048;
5183
5421
  const scoutModelIdStr = input5.stages?.scout?.modelId ?? "";
5184
- if (scoutOn && scoutModel) {
5422
+ if (scoutOn && scoutRuntime) {
5185
5423
  if (!input5.scoutSystemPrompt?.trim()) {
5186
5424
  throw new Error(
5187
5425
  "planner: scout enabled but `scoutSystemPrompt` was not provided. Compose it via composeScoutSystemPrompt() or set stages.scout.enabled = false."
@@ -5191,12 +5429,15 @@ async function runPlanner(input5) {
5191
5429
  bus.emit({ kind: "stage_started", runId, stage: "scout" });
5192
5430
  try {
5193
5431
  const scoutRes = await runScout({
5194
- model: scoutModel,
5432
+ runtime: scoutRuntime,
5195
5433
  systemPrompt: input5.scoutSystemPrompt,
5196
5434
  userPrompt: input5.userPrompt,
5197
5435
  budget: input5.budget,
5436
+ bus,
5437
+ runId,
5198
5438
  abort: input5.abort,
5199
- maxTokens: scoutMaxOut
5439
+ maxTokens: scoutMaxOut,
5440
+ providerSpecific: input5.anthropicProviderSpecific?.scout
5200
5441
  });
5201
5442
  const durationMs = Date.now() - t0;
5202
5443
  if (scoutRes) {
@@ -5242,7 +5483,7 @@ async function runPlanner(input5) {
5242
5483
  }
5243
5484
  } catch (err2) {
5244
5485
  const msg = err2 instanceof Error ? err2.message : String(err2);
5245
- if (APICallError2.isInstance(err2)) {
5486
+ if (APICallError3.isInstance(err2)) {
5246
5487
  const status = err2.statusCode ?? 0;
5247
5488
  if (status === 401 || status === 403) {
5248
5489
  throw err2;
@@ -5306,25 +5547,6 @@ async function runPlanner(input5) {
5306
5547
  validation: validationOn ? { issues, durationMs: validationDurationMs } : { issues: [], durationMs: 0 }
5307
5548
  };
5308
5549
  }
5309
- function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
5310
- if (response.errorKind === "rate_limit") {
5311
- return new Error(
5312
- rateLimitMessage({
5313
- provider: providerName,
5314
- retryAfterSec: response.retryAfterSec,
5315
- rawBody: response.rawError ?? "",
5316
- retryAlreadyAttempted: retriedRateLimit,
5317
- retrySkippedReason,
5318
- maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
5319
- })
5320
- );
5321
- }
5322
- if (response.errorKind === "model_not_found") {
5323
- return new Error(response.rawError ?? "planner: model not found");
5324
- }
5325
- const base = response.rawError ?? "planner: provider error";
5326
- return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
5327
- }
5328
5550
 
5329
5551
  // src/planner/planner-limit-messages.ts
5330
5552
  function printPlannerApiCostNotice() {
@@ -5398,19 +5620,260 @@ function printPlannerLimitNextSteps() {
5398
5620
  blank();
5399
5621
  }
5400
5622
 
5401
- // src/planner/providers/index.ts
5402
- import { createAnthropic } from "@ai-sdk/anthropic";
5403
- import { createOpenAI } from "@ai-sdk/openai";
5404
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
5405
- function resolveModel(provider, modelId, apiKey) {
5406
- switch (provider) {
5407
- case "anthropic":
5408
- return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
5409
- case "openai":
5410
- return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
5411
- case "google":
5412
- 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);
5413
5875
  }
5876
+ return new VercelRuntime(input5.provider, input5.modelId, input5.apiKey);
5414
5877
  }
5415
5878
 
5416
5879
  // src/planner/budget.ts
@@ -5660,6 +6123,9 @@ function formatPlannerCacheLine(opts) {
5660
6123
  if (!opts.cacheEnabled) {
5661
6124
  return "cache disabled";
5662
6125
  }
6126
+ if (opts.plannerRuntimeKind === "agent-sdk") {
6127
+ return "(agent-sdk: not exposed)";
6128
+ }
5663
6129
  const { stats } = opts;
5664
6130
  if (stats.cacheReadTokens > 0) {
5665
6131
  const pct = Math.round(stats.cacheHitRatio * 100);
@@ -5847,12 +6313,33 @@ async function emitViaApi(story, paths, config2, opts) {
5847
6313
  );
5848
6314
  }
5849
6315
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
5850
- 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;
5851
6334
  const budget = new Budget(planner.budget);
5852
6335
  banner();
5853
6336
  step(`planning ${story.feature} / ${story.id}`);
5854
6337
  kv("provider", planner.provider);
5855
6338
  kv("model", modelId);
6339
+ kv(
6340
+ "runtime",
6341
+ `${draftRuntime.kind} \xB7 ${draftRuntime.providerName}` + (planner.provider === "anthropic" ? ` (${anthropicRuntimeChoice})` : "")
6342
+ );
5856
6343
  kv(
5857
6344
  "budget",
5858
6345
  `${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
@@ -5880,7 +6367,7 @@ async function emitViaApi(story, paths, config2, opts) {
5880
6367
  const strictValidation = !!opts.strictValidation || planner.validation?.strict === true;
5881
6368
  const maxScoutFiles = opts.maxScoutFiles ?? planner.stages?.scout?.maxFiles ?? 12;
5882
6369
  let scoutModelId = "";
5883
- let scoutModelHandle;
6370
+ let scoutRuntime;
5884
6371
  let scoutSystemPrompt;
5885
6372
  if (scoutEnabled) {
5886
6373
  scoutModelId = modelFor(
@@ -5889,7 +6376,12 @@ async function emitViaApi(story, paths, config2, opts) {
5889
6376
  planner.modelOverride,
5890
6377
  opts.scoutModel ?? planner.stages?.scout?.modelOverride
5891
6378
  );
5892
- 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
+ });
5893
6385
  scoutSystemPrompt = composeScoutSystemPrompt({
5894
6386
  projectRoots: config2.project.projectRoots ?? ["."],
5895
6387
  primaryLanguage: config2.project.primaryLanguage ?? "",
@@ -5978,9 +6470,10 @@ async function emitViaApi(story, paths, config2, opts) {
5978
6470
  try {
5979
6471
  result = await runPlanner({
5980
6472
  root: paths.root,
5981
- model,
6473
+ runtime: draftRuntime,
5982
6474
  provider: planner.provider,
5983
6475
  modelId,
6476
+ anthropicProviderSpecific,
5984
6477
  systemPrompt,
5985
6478
  userPrompt,
5986
6479
  budget,
@@ -5992,7 +6485,7 @@ async function emitViaApi(story, paths, config2, opts) {
5992
6485
  stages: {
5993
6486
  scout: {
5994
6487
  enabled: scoutEnabled,
5995
- model: scoutModelHandle,
6488
+ runtime: scoutRuntime,
5996
6489
  modelId: scoutModelId,
5997
6490
  maxFiles: maxScoutFiles,
5998
6491
  maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
@@ -6071,7 +6564,9 @@ async function emitViaApi(story, paths, config2, opts) {
6071
6564
  issuesCount: issueCount,
6072
6565
  issuesByKind,
6073
6566
  durationMs: result.validation?.durationMs
6074
- }
6567
+ },
6568
+ plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
6569
+ providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
6075
6570
  });
6076
6571
  } catch {
6077
6572
  }
@@ -6091,9 +6586,16 @@ async function emitViaApi(story, paths, config2, opts) {
6091
6586
  { key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
6092
6587
  {
6093
6588
  key: "tokens",
6094
- 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
+ })
6095
6598
  },
6096
- { key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
6097
6599
  {
6098
6600
  key: "max out",
6099
6601
  value: `${planner.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS} tok/req`
@@ -6760,6 +7262,71 @@ async function checkPlannerCache(paths, ctx) {
6760
7262
  detail: `caching active \u2014 last run ${hitPct}% hit (${formatTokenK(stats.cacheReadTokens)} read, ${lastRun.provider}/${lastRun.model})`
6761
7263
  };
6762
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
+ }
6763
7330
  async function checkTrackerConfig(_paths, ctx) {
6764
7331
  if (!ctx.config) {
6765
7332
  return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
@@ -6901,6 +7468,9 @@ async function runAllChecks(paths, ctx, fix) {
6901
7468
  await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
6902
7469
  await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
6903
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));
6904
7474
  await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
6905
7475
  await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
6906
7476
  await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
@@ -7355,7 +7925,7 @@ import { secureHeaders } from "hono/secure-headers";
7355
7925
 
7356
7926
  // src/console/api/stories.ts
7357
7927
  import fs37 from "fs";
7358
- import { z as z5 } from "zod";
7928
+ import { z as z6 } from "zod";
7359
7929
 
7360
7930
  // src/utils/path-safety.ts
7361
7931
  import path30 from "path";
@@ -7366,13 +7936,13 @@ function isInside(abs, parent) {
7366
7936
  }
7367
7937
 
7368
7938
  // src/console/api/stories.ts
7369
- var CreateBody = z5.object({
7370
- feature: z5.string().min(1).max(80),
7371
- title: z5.string().max(200).optional(),
7372
- 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()
7373
7943
  });
7374
- var PatchBody = z5.object({
7375
- intakeContent: z5.string().min(1).max(1e6)
7944
+ var PatchBody = z6.object({
7945
+ intakeContent: z6.string().min(1).max(1e6)
7376
7946
  });
7377
7947
  function safeFeatureParam(feature) {
7378
7948
  return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
@@ -7464,12 +8034,12 @@ function mountStoriesApi(app, opts) {
7464
8034
  // src/console/api/plans.ts
7465
8035
  import fs38 from "fs";
7466
8036
  import path31 from "path";
7467
- import { z as z6 } from "zod";
8037
+ import { z as z7 } from "zod";
7468
8038
  import { diffLines } from "diff";
7469
- var DiffQuery = z6.object({
7470
- feature: z6.string().min(1).max(80),
7471
- a: z6.string().min(1).max(200),
7472
- 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)
7473
8043
  });
7474
8044
  function safeFeatureParam2(feature) {
7475
8045
  return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
@@ -7543,8 +8113,8 @@ function mountPlansApi(app, opts) {
7543
8113
  }
7544
8114
 
7545
8115
  // src/console/api/config.ts
7546
- import { z as z7 } from "zod";
7547
- var ConfigBody = z7.object({}).passthrough();
8116
+ import { z as z8 } from "zod";
8117
+ var ConfigBody = z8.object({}).passthrough();
7548
8118
  function mountConfigApi(app, opts) {
7549
8119
  app.get("/api/config", (c) => {
7550
8120
  const config2 = loadConfig(opts.paths.configFile);
@@ -7584,14 +8154,14 @@ function mountMetaApi(app, opts) {
7584
8154
 
7585
8155
  // src/console/api/runs.ts
7586
8156
  import { streamSSE } from "hono/streaming";
7587
- import { z as z8 } from "zod";
8157
+ import { z as z9 } from "zod";
7588
8158
  import path32 from "path";
7589
8159
  import fs39 from "fs";
7590
8160
  var active = /* @__PURE__ */ new Map();
7591
8161
  var EVENT_BUF_CAP = 512;
7592
- var StartBody = z8.object({
7593
- feature: z8.string().min(1),
7594
- storyId: z8.string().min(1)
8162
+ var StartBody = z9.object({
8163
+ feature: z9.string().min(1),
8164
+ storyId: z9.string().min(1)
7595
8165
  });
7596
8166
  function emptyStats(durationMs) {
7597
8167
  return {
@@ -7648,7 +8218,17 @@ function mountRunsApi(app, opts) {
7648
8218
  const cfgFresh = loadConfig(opts.paths.configFile);
7649
8219
  const planner = cfgFresh.planner;
7650
8220
  const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
7651
- 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;
7652
8232
  const budget = new Budget(planner.budget);
7653
8233
  const cacheEnabled = planner.cache?.enabled ?? true;
7654
8234
  const scoutEnabled = planner.stages?.scout?.enabled !== false;
@@ -7656,7 +8236,7 @@ function mountRunsApi(app, opts) {
7656
8236
  const strictValidation = planner.validation?.strict === true;
7657
8237
  const maxScoutFiles = planner.stages?.scout?.maxFiles ?? 12;
7658
8238
  let scoutModelId = "";
7659
- let scoutModelHandle;
8239
+ let scoutRuntime;
7660
8240
  let scoutSystemPrompt;
7661
8241
  if (scoutEnabled) {
7662
8242
  scoutModelId = modelFor(
@@ -7665,7 +8245,12 @@ function mountRunsApi(app, opts) {
7665
8245
  planner.modelOverride,
7666
8246
  planner.stages?.scout?.modelOverride
7667
8247
  );
7668
- 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
+ });
7669
8254
  }
7670
8255
  const repoMap = buildRepoMap(opts.paths.root, { format: "tree" });
7671
8256
  if (scoutEnabled) {
@@ -7690,9 +8275,10 @@ function mountRunsApi(app, opts) {
7690
8275
  try {
7691
8276
  result = await runPlanner({
7692
8277
  root: opts.paths.root,
7693
- model,
8278
+ runtime: draftRuntime,
7694
8279
  provider: planner.provider,
7695
8280
  modelId,
8281
+ anthropicProviderSpecific,
7696
8282
  systemPrompt,
7697
8283
  userPrompt,
7698
8284
  budget,
@@ -7704,7 +8290,7 @@ function mountRunsApi(app, opts) {
7704
8290
  stages: {
7705
8291
  scout: {
7706
8292
  enabled: scoutEnabled,
7707
- model: scoutModelHandle,
8293
+ runtime: scoutRuntime,
7708
8294
  modelId: scoutModelId,
7709
8295
  maxFiles: maxScoutFiles,
7710
8296
  maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
@@ -7794,7 +8380,9 @@ function mountRunsApi(app, opts) {
7794
8380
  issuesCount: issueCount,
7795
8381
  issuesByKind,
7796
8382
  durationMs: result.validation?.durationMs
7797
- }
8383
+ },
8384
+ plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
8385
+ providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
7798
8386
  });
7799
8387
  } catch {
7800
8388
  }
@@ -7867,7 +8455,7 @@ function mountRunsApi(app, opts) {
7867
8455
  }
7868
8456
 
7869
8457
  // src/console/api/secrets.ts
7870
- import { z as z9 } from "zod";
8458
+ import { z as z10 } from "zod";
7871
8459
 
7872
8460
  // src/core/mask-token.ts
7873
8461
  function maskToken(t) {
@@ -7905,26 +8493,26 @@ function mountSecretsApi(app, opts) {
7905
8493
  }
7906
8494
  });
7907
8495
  });
7908
- const PutBody = z9.object({
7909
- planner: z9.object({
7910
- anthropic: z9.string().optional(),
7911
- openai: z9.string().optional(),
7912
- 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()
7913
8501
  }).optional(),
7914
- tracker: z9.object({
7915
- jira: z9.object({
7916
- host: z9.string().optional(),
7917
- email: z9.string().optional(),
7918
- 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()
7919
8507
  }).optional(),
7920
- azure: z9.object({
7921
- organization: z9.string().optional(),
7922
- project: z9.string().optional(),
7923
- pat: z9.string().optional()
8508
+ azure: z10.object({
8509
+ organization: z10.string().optional(),
8510
+ project: z10.string().optional(),
8511
+ pat: z10.string().optional()
7924
8512
  }).optional(),
7925
- github: z9.object({
7926
- host: z9.string().optional(),
7927
- pat: z9.string().optional()
8513
+ github: z10.object({
8514
+ host: z10.string().optional(),
8515
+ pat: z10.string().optional()
7928
8516
  }).optional()
7929
8517
  }).optional()
7930
8518
  });
@@ -7964,12 +8552,12 @@ function mountSecretsApi(app, opts) {
7964
8552
  }
7965
8553
 
7966
8554
  // src/console/api/tracker.ts
7967
- import { z as z10 } from "zod";
7968
- var SearchQuery = z10.object({ q: z10.string().max(500) });
7969
- var ImportBody = z10.object({
7970
- issueId: z10.string().min(1),
7971
- feature: z10.string().min(1).max(80),
7972
- 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()
7973
8561
  });
7974
8562
  function mountTrackerApi(app, opts) {
7975
8563
  app.get("/api/tracker/search", async (c) => {
@@ -8726,7 +9314,24 @@ program.command("new-plan [intake-path]").description("Generate a plan (via dire
8726
9314
  "--max-scout-files <n>",
8727
9315
  "Maximum files the scout can pre-select (default from config or 12)",
8728
9316
  (v) => parseInt(v, 10)
8729
- ).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));
8730
9335
  program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
8731
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));
8732
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));
@@ -8792,4 +9397,3 @@ function wrapArgs(fn) {
8792
9397
  }
8793
9398
  };
8794
9399
  }
8795
- //# sourceMappingURL=cli.js.map