pi-landstrip 0.20.4 → 0.20.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.ts +65 -84
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -65,7 +65,7 @@ and `/landstrip-subagents` for subagent/task management (`Tasks`, `Log`, `Subage
65
65
  `Primary`, `Settings`, `Help`). Task IDs accept unique prefixes; bare IDs open logs.
66
66
  `/landstrip-subagents tasks kill <id>` terminates a task. `Ctrl+Shift+A` cycles
67
67
  primary agents while idle. The `task` tool accepts `background: true`, `include_context: true`,
68
- and `task_id` to continue a saved task; the `tasks` tool supports `list`, `inspect`, `wait`,
68
+ and `task_id` to continue a saved task; the `tasks` tool supports `list`, `inspect`,
69
69
  `steer`, and `cancel`. Press `S` in `Tasks` or `Enter` in running/queued logs to steer workers.
70
70
 
71
71
  ### Worker environment and authentication
package/dist/index.ts CHANGED
@@ -2711,23 +2711,25 @@ var taskParameters = Type.Object({
2711
2711
  subagent_type: Type.String({ description: "Subagent name" }),
2712
2712
  task_id: Type.Optional(Type.String({ description: "Task ID to resume" })),
2713
2713
  command: Type.Optional(Type.String({ description: "Originating command, if any" })),
2714
- background: Type.Optional(Type.Boolean({ description: "Return immediately and run in background" })),
2714
+ background: Type.Optional(Type.Boolean({
2715
+ description: "Return immediately and run in background; completion automatically triggers a follow-up turn"
2716
+ })),
2715
2717
  include_context: Type.Optional(Type.Boolean({
2716
2718
  description: "Seed the subagent prompt with a bounded summary of recent parent conversation"
2717
2719
  }))
2718
2720
  });
2721
+ var BACKGROUND_LAUNCH_GUIDANCE = [
2722
+ "Terminal notification: enabled.",
2723
+ "Automatic follow-up turn: enabled.",
2724
+ "Next action: do not poll or sleep to wait; continue only independent useful work, otherwise end this turn and wait for the completion notification."
2725
+ ].join(`
2726
+ `);
2719
2727
  var TasksToolSchema = Type.Object({
2720
- action: Type.Union([
2721
- Type.Literal("list"),
2722
- Type.Literal("inspect"),
2723
- Type.Literal("wait"),
2724
- Type.Literal("steer"),
2725
- Type.Literal("cancel")
2726
- ], {
2727
- description: 'Task management action: "list" to view task sessions, "inspect" for status/output, "wait" to await completion, "steer" to guide a running subagent, or "cancel" to stop an active task'
2728
+ action: Type.Union([Type.Literal("list"), Type.Literal("inspect"), Type.Literal("steer"), Type.Literal("cancel")], {
2729
+ description: 'Task management action: "list" to view task sessions, "inspect" for point-in-time status/output, "steer" to guide a running subagent, or "cancel" to stop an active task'
2728
2730
  }),
2729
2731
  task_id: Type.Optional(Type.String({
2730
- description: 'Task ID or unique prefix (required for "inspect", "wait", "steer", and "cancel")'
2732
+ description: 'Task ID or unique prefix (required for "inspect", "steer", and "cancel")'
2731
2733
  })),
2732
2734
  mode: Type.Optional(Type.Union([Type.Literal("all"), Type.Literal("background"), Type.Literal("foreground")], {
2733
2735
  description: 'Optional execution mode filter for "list": "all", "background", or "foreground"'
@@ -2745,17 +2747,12 @@ var TasksToolSchema = Type.Object({
2745
2747
  lines: Type.Optional(Type.Integer({
2746
2748
  minimum: 1,
2747
2749
  maximum: 500,
2748
- description: 'Number of transcript lines to return for "inspect" or "wait" (default 100)'
2750
+ description: 'Number of transcript lines to return for "inspect" (default 100)'
2749
2751
  })),
2750
2752
  offset: Type.Optional(Type.Integer({
2751
2753
  minimum: 0,
2752
2754
  description: '0-indexed starting line offset for "inspect" (omit for tail)'
2753
2755
  })),
2754
- timeout_ms: Type.Optional(Type.Integer({
2755
- minimum: 1,
2756
- maximum: 600000,
2757
- description: 'Optional timeout in milliseconds for "wait"'
2758
- })),
2759
2756
  message: Type.Optional(Type.String({
2760
2757
  description: 'Steering message to send to a running subagent (required for "steer")'
2761
2758
  }))
@@ -3537,6 +3534,10 @@ function registerSubagentWorker(pi, config) {
3537
3534
  name: "task",
3538
3535
  label: "Task",
3539
3536
  description: "Delegate a task to a Pi subagent process.",
3537
+ promptSnippet: "Delegate a task to a sandboxed Pi subagent process",
3538
+ promptGuidelines: [
3539
+ "After launching a task with background: true, continue only independent work that does not wait on the subagent; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not poll tasks to wait."
3540
+ ],
3540
3541
  parameters: taskParameters,
3541
3542
  executionMode: "parallel",
3542
3543
  async execute(_id, input, signal, _onUpdate, ctx) {
@@ -4818,7 +4819,12 @@ ${this.primaryAgent.prompt}` };
4818
4819
  return {
4819
4820
  name: "tasks",
4820
4821
  label: "Tasks",
4821
- description: "Inspect, list, and cancel background tasks and subagents managed by Landstrip.",
4822
+ description: "Inspect, list, steer, and cancel background tasks and subagents managed by Landstrip. Point-in-time management only, not a waiting primitive; background tasks automatically notify and trigger a follow-up turn when done.",
4823
+ promptSnippet: "Inspect, list, steer, or cancel background tasks and subagents without polling",
4824
+ promptGuidelines: [
4825
+ "Use tasks for point-in-time inspection, steering, or cancellation, not as a waiting primitive.",
4826
+ "Do not poll tasks with list or inspect while waiting for a background task to finish; completion automatically triggers a follow-up turn."
4827
+ ],
4822
4828
  parameters: TasksToolSchema,
4823
4829
  executionMode: "parallel",
4824
4830
  renderCall: (input, theme) => {
@@ -4826,7 +4832,7 @@ ${this.primaryAgent.prompt}` };
4826
4832
  const target = input.task_id ? ` ${input.task_id.slice(0, 8)}` : "";
4827
4833
  return new Text(`${theme.fg("toolTitle", theme.bold("Tasks"))} ${theme.fg("accent", action)}${theme.fg("dim", target)}`, 0, 0);
4828
4834
  },
4829
- execute: async (_toolCallId, input, signal, _onUpdate, callCtx) => {
4835
+ execute: async (_toolCallId, input, _signal, _onUpdate, callCtx) => {
4830
4836
  const ctx = boundCtx ?? callCtx;
4831
4837
  if (!ctx)
4832
4838
  throw new Error("Task context is unavailable");
@@ -4841,7 +4847,7 @@ ${this.primaryAgent.prompt}` };
4841
4847
  throw new Error(`Task ID prefix "${input.task_id}" matches ${matched.length} tasks`);
4842
4848
  return matched[0];
4843
4849
  };
4844
- const formatInspectResult = (task, timedOut = false) => {
4850
+ const formatInspectResult = (task) => {
4845
4851
  const details = taskDetails(task);
4846
4852
  const transcript = taskTranscript(task);
4847
4853
  const maxLines = Math.max(1, Math.min(500, input.lines ?? 100));
@@ -4849,18 +4855,18 @@ ${this.primaryAgent.prompt}` };
4849
4855
  const outputText = sliced.join(`
4850
4856
  `);
4851
4857
  const modeLabel = details.background ? "background" : "foreground";
4852
- const timeoutNote = timedOut ? `
4853
- Wait: timed out (task still active)` : "";
4858
+ const activeGuidance = details.background && (details.state === "queued" || details.state === "running") ? `
4859
+ Next action: task is still active; do not poll inspect in a loop—end this turn and wait for the automatic completion follow-up turn.` : "";
4854
4860
  const header = task.kind === "command" ? `$ bash ${task.command ?? task.description}
4855
4861
  Mode: ${modeLabel}
4856
4862
  CWD: ${task.cwd}
4857
4863
  State: ${details.state}
4858
4864
  Duration: ${taskDuration(details) ?? "unknown"}
4859
- Exit code: ${details.exitCode ?? "none"}${timeoutNote}` : `@${task.agent}: ${task.description}
4865
+ Exit code: ${details.exitCode ?? "none"}${activeGuidance}` : `@${task.agent}: ${task.description}
4860
4866
  Mode: ${modeLabel}
4861
4867
  State: ${details.state}
4862
4868
  Duration: ${taskDuration(details) ?? "unknown"}
4863
- Tool calls: ${task.toolCalls ?? 0}${timeoutNote}`;
4869
+ Tool calls: ${task.toolCalls ?? 0}${activeGuidance}`;
4864
4870
  return {
4865
4871
  content: [
4866
4872
  { type: "text", text: `${header}
@@ -4906,49 +4912,6 @@ ${outputText}` }
4906
4912
  const task = resolveSingleTask("inspect");
4907
4913
  return formatInspectResult(task);
4908
4914
  }
4909
- if (action === "wait") {
4910
- const task = resolveSingleTask("wait");
4911
- let timedOut = false;
4912
- const runPromise = this.runPromises.get(task.id);
4913
- if (runPromise && (task.state === "queued" || task.state === "running")) {
4914
- const waiters = [
4915
- runPromise.then(() => {
4916
- return;
4917
- }, () => {
4918
- return;
4919
- })
4920
- ];
4921
- let timer;
4922
- if (input.timeout_ms && input.timeout_ms > 0) {
4923
- waiters.push(new Promise((resolveWait) => {
4924
- timer = setTimeout(() => {
4925
- timedOut = true;
4926
- resolveWait();
4927
- }, input.timeout_ms);
4928
- }));
4929
- }
4930
- let onAbort;
4931
- if (signal) {
4932
- waiters.push(new Promise((_, rejectWait) => {
4933
- if (signal.aborted) {
4934
- rejectWait(new Error("Wait aborted"));
4935
- return;
4936
- }
4937
- onAbort = () => rejectWait(new Error("Wait aborted"));
4938
- signal.addEventListener("abort", onAbort, { once: true });
4939
- }));
4940
- }
4941
- try {
4942
- await Promise.race(waiters);
4943
- } finally {
4944
- if (timer !== undefined)
4945
- clearTimeout(timer);
4946
- if (onAbort && signal)
4947
- signal.removeEventListener("abort", onAbort);
4948
- }
4949
- }
4950
- return formatInspectResult(task, timedOut);
4951
- }
4952
4915
  if (action === "steer") {
4953
4916
  const task = resolveSingleTask("steer");
4954
4917
  const message = input.message?.trim();
@@ -5071,17 +5034,22 @@ ${outputText}` }
5071
5034
  text: `$ bash ${task.description} ${exitMsg} (${durationSec})`,
5072
5035
  finishedAt: Date.now()
5073
5036
  });
5074
- const commandOutput = task.outputFile && existsSync5(task.outputFile) ? readFileSync6(task.outputFile, "utf8").trim() : task.output ?? "";
5075
- const deliveryText = `Background command ${task.state === "completed" ? "completed" : "failed"}: ${task.description}
5037
+ if (!this.shuttingDown && !task.delivered && task.state !== "cancelled") {
5038
+ const rawOutput = task.outputFile && existsSync5(task.outputFile) ? readFileSync6(task.outputFile, "utf8").trim() : task.output ?? "";
5039
+ const commandOutput = rawOutput ? this.storeTaskText(task, rawOutput, "output") : "";
5040
+ if (commandOutput)
5041
+ task.output = commandOutput;
5042
+ const deliveryText = `Background command ${task.state === "completed" ? "completed" : "failed"}: ${task.description}
5076
5043
 
5077
5044
  ${renderTaskResult(task.id, task.state, commandOutput || (task.error ?? ""))}`;
5078
- const delivered = await this.deliverBackground(task, deliveryText);
5079
- if (delivered) {
5080
- task.delivered = true;
5081
- this.persist(task);
5082
- }
5083
- if (task.state === "error") {
5084
- ctx.ui.notify(`Background command failed: ${task.description}`, "error");
5045
+ const delivered = await this.deliverBackground(task, deliveryText);
5046
+ if (delivered) {
5047
+ task.delivered = true;
5048
+ this.persist(task);
5049
+ }
5050
+ if (task.state === "error") {
5051
+ ctx.ui.notify(`Background command failed: ${task.description}`, "error");
5052
+ }
5085
5053
  }
5086
5054
  this.notifyTaskUpdate();
5087
5055
  this.updateTaskWidget(ctx);
@@ -5093,10 +5061,14 @@ ${renderTaskResult(task.id, task.state, commandOutput || (task.error ?? ""))}`;
5093
5061
  content: [
5094
5062
  {
5095
5063
  type: "text",
5096
- text: `Command launched in background.
5097
- Task ID: ${taskId}
5098
- Output file: ${outputFile}
5099
- Use /landstrip-subagents tasks to view live progress or cancel.`
5064
+ text: [
5065
+ "Command launched in background.",
5066
+ `Task ID: ${taskId}`,
5067
+ `Output file: ${outputFile}`,
5068
+ BACKGROUND_LAUNCH_GUIDANCE,
5069
+ "Use /landstrip-subagents tasks to view live progress or cancel."
5070
+ ].join(`
5071
+ `)
5100
5072
  }
5101
5073
  ],
5102
5074
  details: {
@@ -5120,6 +5092,10 @@ Use /landstrip-subagents tasks to view live progress or cancel.`
5120
5092
 
5121
5093
  Available subagents:
5122
5094
  ${descriptions}` : ""),
5095
+ promptSnippet: "Delegate a task to a sandboxed Pi subagent process",
5096
+ promptGuidelines: [
5097
+ "After launching a task with background: true, continue only independent work that does not wait on the subagent; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not poll tasks to wait."
5098
+ ],
5123
5099
  parameters: taskParameters,
5124
5100
  executionMode: "parallel",
5125
5101
  renderCall: (input, theme) => {
@@ -5284,7 +5260,8 @@ ${catalog.diagnostics.join(`
5284
5260
  this.notifyWhenDone(task, existingRun, ctx);
5285
5261
  }
5286
5262
  const state = task.state === "queued" ? "queued" : "running";
5287
- const status = state === "queued" ? "Background task queued" : "Background task updated";
5263
+ const status = `${state === "queued" ? "Background task queued" : "Background task updated"}
5264
+ ${BACKGROUND_LAUNCH_GUIDANCE}`;
5288
5265
  return { task, text: renderTaskResult(task.id, state, status), state };
5289
5266
  }
5290
5267
  this.claimForeground(task.id);
@@ -5337,7 +5314,8 @@ ${catalog.diagnostics.join(`
5337
5314
  this.notifyWhenDone(task, run, ctx);
5338
5315
  return {
5339
5316
  task,
5340
- text: renderTaskResult(task.id, "queued", "Background task queued"),
5317
+ text: renderTaskResult(task.id, "queued", `Background task queued
5318
+ ${BACKGROUND_LAUNCH_GUIDANCE}`),
5341
5319
  state: "queued"
5342
5320
  };
5343
5321
  }
@@ -6167,8 +6145,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
6167
6145
  this.primaryRules = undefined;
6168
6146
  this.primaryConfigurationError = false;
6169
6147
  const catalog = this.loadCatalog(ctx.cwd, getAgentDir4(), isProjectTrusted(ctx));
6170
- this.maxSubagents = catalog.maxSubagents;
6171
- this.semaphore = new Semaphore(this.maxSubagents);
6148
+ this.setMaxSubagents(catalog.maxSubagents);
6172
6149
  if (catalog.diagnostics.length > 0) {
6173
6150
  this.invalidatePrimaryAgent(catalog, ctx);
6174
6151
  return;
@@ -6309,7 +6286,7 @@ var LandstripBashSchema = Type2.Object({
6309
6286
  command: Type2.String({ description: "Shell command to execute" }),
6310
6287
  timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
6311
6288
  background: Type2.Optional(Type2.Boolean({
6312
- description: "Run command asynchronously in the background and return a task ID"
6289
+ description: "Run command asynchronously in the background and return a task ID immediately; completion automatically triggers a follow-up turn"
6313
6290
  })),
6314
6291
  name: Type2.Optional(Type2.String({
6315
6292
  description: "Concise label for the background command execution"
@@ -8000,6 +7977,10 @@ ${message}
8000
7977
  ...localBash,
8001
7978
  label: "bash (landstrip)",
8002
7979
  description: "Execute commands with the active Landstrip shell provider.",
7980
+ promptGuidelines: [
7981
+ ...localBash.promptGuidelines ?? [],
7982
+ "After launching a bash command with background: true, continue only independent work that does not wait on the command; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not sleep or poll tasks to wait."
7983
+ ],
8003
7984
  parameters: LandstripBashSchema,
8004
7985
  renderCall(args, theme, context) {
8005
7986
  const bashArgs = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip-api": "^0.20.4",
42
+ "@landstrip/landstrip-api": "^0.20.6",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "undici": "^8.9.0",