pi-squire 0.14.0 → 0.15.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/src/results.ts CHANGED
@@ -35,6 +35,8 @@ const INBOUND = "message:inbound";
35
35
  export type Delegation = {
36
36
  id: string;
37
37
  agent: string;
38
+ /** The agent's `display_name` at launch, shown instead of `agent`; absent when it had none. */
39
+ displayName?: string;
38
40
  model: string;
39
41
  thinking: string;
40
42
  /** The delegate's session and window name; absent in records from before labels, where it was `<agent>-<8 id characters>`. */
@@ -8,8 +8,8 @@ import { isAbsolute } from "node:path";
8
8
 
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
 
11
- import type { Agent } from "./agents.ts";
12
- import { sessionHeaderCwd, sessionIdOf } from "./result-sessions.ts";
11
+ import { type Agent, disabledProblem, isThinking, withoutLocked } from "./agents.ts";
12
+ import { sessionHeaderCwd, sessionIdOf, sessionThinking } from "./result-sessions.ts";
13
13
  import { deriveState, finishedModel, locateSession, type StartedRecorded } from "./results.ts";
14
14
  import type { ResumeDeps, ResumeRequest } from "./resume.ts";
15
15
  import { requireTmuxVersion, type TmuxWindow } from "./tmux.ts";
@@ -79,10 +79,35 @@ export function sessionFor(record: StartedRecorded, ctx: ExtensionContext): Sess
79
79
  return { path, cwd, dir };
80
80
  }
81
81
 
82
- /** The model choices: an explicit model alone, else where the delegate ended, then the agent's fallbacks. */
83
- export function preferredModels(record: StartedRecorded, agent: Agent, sessionPath: string): string[] {
84
- const ended = finishedModel(sessionPath) ?? record.result?.finishedModel ?? record.model;
85
- return [...new Set([ended, ...agent.fallback])];
82
+ /**
83
+ * What a resume runs with when the call passes nothing. A field the agent
84
+ * locks takes the agent file's current value, or the parent's current one when
85
+ * the file sets none, even when the delegate last ran on another (a fallback
86
+ * after a usage limit, or a value from before the file changed). A field it
87
+ * does not lock keeps what the delegate last ran on: the model of its last
88
+ * answer, and the thinking level its session last changed to, or the
89
+ * recorded ones when the session does not say. The models are in order, the
90
+ * agent's fallbacks after the first.
91
+ */
92
+ export function resumeDefaults(
93
+ record: StartedRecorded,
94
+ agent: Agent,
95
+ sessionPath: string,
96
+ parent: { model?: string; thinking: string },
97
+ ): { models: string[]; thinking: string } {
98
+ const first = agent.locked.includes("model")
99
+ ? (agent.model ?? parent.model)
100
+ : (finishedModel(sessionPath) ?? record.result?.finishedModel ?? record.model);
101
+ const thinking = agent.locked.includes("thinking")
102
+ ? (agent.thinking ?? parent.thinking)
103
+ : lastThinking(record, sessionPath);
104
+ return { models: [...new Set([...(first === undefined ? [] : [first]), ...agent.fallback])], thinking };
105
+ }
106
+
107
+ /** The thinking level the delegate last ran on: its session's last change, else the level recorded at its start. */
108
+ function lastThinking(record: StartedRecorded, sessionPath: string): string {
109
+ const level = sessionThinking(sessionPath);
110
+ return level !== undefined && isThinking(level) ? level : record.thinking;
86
111
  }
87
112
 
88
113
  /** Steps 5 and 6: the session file and the agent, and no resume of the delegation already under way. */
@@ -92,7 +117,10 @@ export function recordCheck(
92
117
  ctx: ExtensionContext,
93
118
  ): { session: SessionFile; agent: Agent } {
94
119
  const session = sessionFor(record, ctx);
95
- const agent = deps.roster(ctx).agents.find((item) => item.name === record.agent);
120
+ const roster = deps.roster(ctx);
121
+ const disabled = disabledProblem(roster, record.agent);
122
+ if (disabled !== undefined) throw new Error(`${disabled}, so delegation ${record.id} is not resumed`);
123
+ const agent = roster.agents.find((item) => item.name === record.agent);
96
124
  if (agent === undefined) {
97
125
  throw new Error(
98
126
  `agent ${JSON.stringify(record.agent)} is no longer in the roster, so delegation ${record.id} is not resumed`,
@@ -113,12 +141,17 @@ export async function settleModel(
113
141
  },
114
142
  ctx: ExtensionContext,
115
143
  ): Promise<{ thinking: string; chosen: Awaited<ReturnType<ResumeDeps["chooseModel"]>> }> {
116
- const { record, agent, session, request } = input;
117
- const thinking = request.thinking ?? record.thinking;
144
+ const { record, agent, session } = input;
145
+ const defaults = resumeDefaults(record, agent, session.path, deps.parentDefaults(ctx));
146
+ const { request, notice } = withoutLocked(agent, input.request, {
147
+ model: defaults.models[0],
148
+ thinking: defaults.thinking,
149
+ });
150
+ const thinking = request.thinking ?? defaults.thinking;
118
151
  const badThinking = deps.thinkingProblem(thinking);
119
152
  if (badThinking !== undefined) throw new Error(badThinking);
120
- const preferred = preferredModels(record, agent, session.path);
121
- return { thinking, chosen: await deps.chooseModel(request.model, agent, ctx.modelRegistry, preferred) };
153
+ const chosen = await deps.chooseModel(request.model, agent, ctx.modelRegistry, defaults.models);
154
+ return { thinking, chosen: notice === undefined ? chosen : { ...chosen, notices: [notice, ...chosen.notices] } };
122
155
  }
123
156
 
124
157
  /** The call-wide prerequisites of a resume: inside tmux, a new enough tmux, a valid placement setting (read once). */
@@ -143,7 +176,7 @@ export async function passes(
143
176
  windowCheck(record, windows);
144
177
  const { session, agent } = recordCheck(deps, record, ctx);
145
178
  // Model choice depends on the agent and where the delegate ended, so records alike share one answer.
146
- const key = JSON.stringify([record.thinking, agent.name, preferredModels(record, agent, session.path)]);
179
+ const key = JSON.stringify([agent.name, resumeDefaults(record, agent, session.path, deps.parentDefaults(ctx))]);
147
180
  let ok = models.get(key);
148
181
  if (ok === undefined) {
149
182
  ok = await settleModel(deps, { record, agent, session, request: {} }, ctx).then(
package/src/resume.ts CHANGED
@@ -52,6 +52,8 @@ export type ResumeDeps = {
52
52
  /** The window placement for this call; throws on an invalid setting. */
53
53
  placement(): WindowPlacement;
54
54
  roster(ctx: ExtensionContext): Roster;
55
+ /** The parent's current model and thinking level, which a field the agent locks but does not set keeps. */
56
+ parentDefaults(ctx: ExtensionContext): { model?: string; thinking: string };
55
57
  chooseModel(
56
58
  explicit: string | undefined,
57
59
  agent: Agent,
@@ -243,6 +245,7 @@ async function startResume(
243
245
  {
244
246
  id,
245
247
  agent: start.agent.name,
248
+ ...(start.agent.displayName === undefined ? {} : { displayName: start.agent.displayName }),
246
249
  model: start.model,
247
250
  thinking: start.thinking,
248
251
  name,
@@ -348,11 +351,13 @@ function resumeTool(deps: ResumeDeps): ToolDef {
348
351
  model: Type.Optional(
349
352
  Type.String({
350
353
  description:
351
- "provider/id to resume on; used as given. Omit to continue on the model the delegate finished on, or its agent's next fallback when that one is unavailable.",
354
+ "provider/id to resume on; used as given. Omit to continue on the model the delegate finished on, or its agent's next fallback when that one is unavailable. Not used when the agent locks model: the agent's current model applies.",
352
355
  }),
353
356
  ),
354
357
  thinking: Type.Optional(
355
- Type.String({ description: `Thinking level (${THINKING_LEVELS.join(", ")}); defaults to the delegation's.` }),
358
+ Type.String({
359
+ description: `Thinking level (${THINKING_LEVELS.join(", ")}); defaults to the level the delegate last ran on, from its session. Not used when the agent locks thinking: the agent's current level applies.`,
360
+ }),
356
361
  ),
357
362
  auto_exit: Type.Optional(
358
363
  Type.Boolean({ description: "Close the window once it finishes; defaults to the delegation's setting." }),