@pencil-agent/nano-pencil 1.11.15 → 1.11.16

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.
@@ -222,6 +222,11 @@ export declare class AgentSession {
222
222
  * Includes built-in commands, extension commands, prompt templates, and skills.
223
223
  */
224
224
  getSlashCommands(): SessionSlashCommandDescriptor[];
225
+ /**
226
+ * Try to execute an extension slash command directly.
227
+ * Returns true when a matching extension command was found, even if it failed internally.
228
+ */
229
+ tryExecuteExtensionCommand(text: string): Promise<boolean>;
225
230
  /** Emit an event to all listeners */
226
231
  private _emit;
227
232
  private _lastAssistantMessage;
@@ -257,6 +257,13 @@ export class AgentSession {
257
257
  ...skillCommands,
258
258
  ];
259
259
  }
260
+ /**
261
+ * Try to execute an extension slash command directly.
262
+ * Returns true when a matching extension command was found, even if it failed internally.
263
+ */
264
+ async tryExecuteExtensionCommand(text) {
265
+ return this._tryExecuteExtensionCommand(text);
266
+ }
260
267
  // =========================================================================
261
268
  // Event Subscription
262
269
  // =========================================================================
@@ -39,6 +39,20 @@ function recordLoopEvent(pi, message) {
39
39
  timestamp: Date.now(),
40
40
  });
41
41
  }
42
+ function publishLoopUpdate(pi, bus, message, type = "info") {
43
+ recordLoopEvent(pi, message);
44
+ notify(bus, message, type);
45
+ pi.sendMessage({
46
+ customType: LOOP_CUSTOM_TYPE,
47
+ content: message,
48
+ display: true,
49
+ details: {
50
+ message,
51
+ level: type,
52
+ timestamp: Date.now(),
53
+ },
54
+ });
55
+ }
42
56
  function notify(bus, message, type = "info") {
43
57
  notifyByBus.get(bus)?.(message, type);
44
58
  }
@@ -243,7 +257,7 @@ function dispatchNextIteration(pi, bus, controller) {
243
257
  }
244
258
  const prompt = controller.buildPrompt();
245
259
  controller.markDispatched();
246
- notify(bus, `[Loop] Starting iteration ${task.currentIteration} for ${task.id}`, "info");
260
+ publishLoopUpdate(pi, bus, `[Loop] Starting iteration ${task.currentIteration} for ${task.id}.`, "info");
247
261
  pi.sendUserMessage(prompt, { deliverAs: "followUp" });
248
262
  }
249
263
  export default async function loopExtension(pi) {
@@ -294,11 +308,10 @@ export default async function loopExtension(pi) {
294
308
  const failure = controller.recordFailure("Loop run ended without an assistant message.");
295
309
  if (failure.action === "stop") {
296
310
  const message = describeTerminalSnapshot(failure.snapshot);
297
- recordLoopEvent(pi, message);
298
- notify(bus, message, "warning");
311
+ publishLoopUpdate(pi, bus, message, "warning");
299
312
  return;
300
313
  }
301
- recordLoopEvent(pi, `[Loop] Iteration failed. Retrying iteration ${failure.task?.currentIteration}.`);
314
+ publishLoopUpdate(pi, bus, `[Loop] Iteration failed. Retrying iteration ${failure.task?.currentIteration}.`, "warning");
302
315
  dispatchNextIteration(pi, bus, controller);
303
316
  return;
304
317
  }
@@ -307,20 +320,18 @@ export default async function loopExtension(pi) {
307
320
  const failure = controller.recordFailure("Assistant response did not include a valid <loop-state> block.");
308
321
  if (failure.action === "stop") {
309
322
  const message = describeTerminalSnapshot(failure.snapshot);
310
- recordLoopEvent(pi, message);
311
- notify(bus, message, "warning");
323
+ publishLoopUpdate(pi, bus, message, "warning");
312
324
  return;
313
325
  }
314
- recordLoopEvent(pi, `[Loop] Missing or invalid loop-state block. Retrying iteration ${failure.task?.currentIteration}.`);
326
+ publishLoopUpdate(pi, bus, `[Loop] Missing or invalid loop-state block. Retrying iteration ${failure.task?.currentIteration}.`, "warning");
315
327
  dispatchNextIteration(pi, bus, controller);
316
328
  return;
317
329
  }
318
- recordLoopEvent(pi, describeDecision(decision));
330
+ publishLoopUpdate(pi, bus, describeDecision(decision), "info");
319
331
  const next = controller.finishTurn(decision);
320
332
  if (next.action === "stop") {
321
333
  const message = describeTerminalSnapshot(next.snapshot);
322
- recordLoopEvent(pi, message);
323
- notify(bus, message, decision.status === "complete" ? "info" : "warning");
334
+ publishLoopUpdate(pi, bus, message, decision.status === "complete" ? "info" : "warning");
324
335
  return;
325
336
  }
326
337
  dispatchNextIteration(pi, bus, controller);
@@ -335,8 +346,7 @@ export default async function loopExtension(pi) {
335
346
  if (parsed.type === "help") {
336
347
  const reason = parsed.reason === "empty" ? "Missing loop goal." : undefined;
337
348
  const help = buildHelp(reason);
338
- recordLoopEvent(pi, help);
339
- notify(bus, help, "warning");
349
+ publishLoopUpdate(pi, bus, help, "warning");
340
350
  return;
341
351
  }
342
352
  if (parsed.type === "status") {
@@ -346,16 +356,14 @@ export default async function loopExtension(pi) {
346
356
  : state.lastTerminal
347
357
  ? formatSnapshot(state.lastTerminal)
348
358
  : "[Loop] No loop task has been started in this session.";
349
- recordLoopEvent(pi, message);
350
- notify(bus, message, "info");
359
+ publishLoopUpdate(pi, bus, message, "info");
351
360
  return;
352
361
  }
353
362
  if (parsed.type === "stop") {
354
363
  const activeTask = controller.getActiveTask();
355
364
  if (!activeTask) {
356
365
  const message = "[Loop] No active loop is running.";
357
- recordLoopEvent(pi, message);
358
- notify(bus, message, "warning");
366
+ publishLoopUpdate(pi, bus, message, "warning");
359
367
  return;
360
368
  }
361
369
  controller.stop("Stopped by user request.", "stopped");
@@ -363,8 +371,7 @@ export default async function loopExtension(pi) {
363
371
  ctx.abort();
364
372
  }
365
373
  const message = `[Loop] Stopped loop ${activeTask.id}.`;
366
- recordLoopEvent(pi, message);
367
- notify(bus, message, "info");
374
+ publishLoopUpdate(pi, bus, message, "info");
368
375
  return;
369
376
  }
370
377
  try {
@@ -374,15 +381,13 @@ export default async function loopExtension(pi) {
374
381
  `Goal: ${task.goal}`,
375
382
  `Safety limits: ${task.maxIterations} iterations, ${task.maxConsecutiveFailures} consecutive failures.`,
376
383
  ].join("\n");
377
- recordLoopEvent(pi, message);
378
- notify(bus, `[Loop] Started ${task.id}: ${summarizeGoal(task.goal)}`, "info");
384
+ publishLoopUpdate(pi, bus, message, "info");
379
385
  dispatchNextIteration(pi, bus, controller);
380
386
  }
381
387
  catch (error) {
382
388
  const message = error instanceof Error ? error.message : String(error);
383
389
  const output = `[Loop] ${message}`;
384
- recordLoopEvent(pi, output);
385
- notify(bus, output, "error");
390
+ publishLoopUpdate(pi, bus, output, "error");
386
391
  }
387
392
  },
388
393
  });
@@ -349,6 +349,49 @@ function formatState(state) {
349
349
  }
350
350
  return lines.join("\n");
351
351
  }
352
+ function createProgressReport(state) {
353
+ return {
354
+ id: state.id,
355
+ goal: state.goal,
356
+ mode: state.mode,
357
+ status: state.status,
358
+ startedAt: state.startedAt,
359
+ finishedAt: state.updatedAt,
360
+ plan: state.plan ?? createFallbackPlan(state.goal, state.mode),
361
+ results: [...state.results],
362
+ finalSummary: state.lastWorkerSummary ?? "",
363
+ };
364
+ }
365
+ function buildProgressUpdate(state, message) {
366
+ const lines = [
367
+ `Team run ${state.id} is in stage "${state.stage}".`,
368
+ `Goal: ${summarizeGoal(state.goal, 120)}`,
369
+ ];
370
+ if (message) {
371
+ lines.push(`Progress: ${message}`);
372
+ }
373
+ if (state.plan?.summary) {
374
+ lines.push(`Plan: ${state.plan.summary}`);
375
+ }
376
+ if (state.results.length > 0) {
377
+ lines.push(`Completed workers: ${state.results.length}`);
378
+ }
379
+ if (state.lastWorkerSummary) {
380
+ lines.push(`Latest result: ${state.lastWorkerSummary}`);
381
+ }
382
+ if (state.lastError) {
383
+ lines.push(`Last error: ${state.lastError}`);
384
+ }
385
+ return lines.join("\n");
386
+ }
387
+ function emitProgressUpdate(onUpdate, state, message) {
388
+ if (!onUpdate || !state)
389
+ return;
390
+ onUpdate({
391
+ content: [{ type: "text", text: buildProgressUpdate(state, message) }],
392
+ details: createProgressReport(state),
393
+ });
394
+ }
352
395
  function formatReport(report) {
353
396
  const lines = [
354
397
  `[Team] Run ${report.id}`,
@@ -733,7 +776,7 @@ async function runWorker(pi, ctx, goal, plan, worker, previousResults) {
733
776
  }
734
777
  return parsed;
735
778
  }
736
- async function orchestrateTeamRun(pi, ctx, goal, mode) {
779
+ async function orchestrateTeamRun(pi, ctx, goal, mode, onUpdate) {
737
780
  const controller = getController(pi);
738
781
  const active = controller.getActive();
739
782
  if (!active) {
@@ -742,16 +785,21 @@ async function orchestrateTeamRun(pi, ctx, goal, mode) {
742
785
  controller.update({ stage: "planning" });
743
786
  persistState(pi, controller.getActive());
744
787
  syncRunUi(ctx, controller.getActive());
788
+ emitProgressUpdate(onUpdate, controller.getActive(), "Creating the team plan.");
745
789
  const plan = await createPlan(pi, ctx, goal, mode);
746
790
  controller.update({ plan, stage: "parallel research" });
747
791
  persistState(pi, controller.getActive());
748
792
  syncRunUi(ctx, controller.getActive());
749
- const researchResults = await Promise.all(plan.researchWorkers.map((worker) => runWorker(pi, ctx, goal, plan, worker, [])));
750
- for (const result of researchResults) {
793
+ emitProgressUpdate(onUpdate, controller.getActive(), `Plan ready. Launching ${plan.researchWorkers.length} research worker${plan.researchWorkers.length === 1 ? "" : "s"}.`);
794
+ const researchResults = await Promise.all(plan.researchWorkers.map(async (worker) => {
795
+ emitProgressUpdate(onUpdate, controller.getActive(), `Started ${worker.role} (${worker.id}).`);
796
+ const result = await runWorker(pi, ctx, goal, plan, worker, []);
751
797
  controller.appendResult(result);
752
798
  persistState(pi, controller.getActive());
753
799
  syncRunUi(ctx, controller.getActive());
754
- }
800
+ emitProgressUpdate(onUpdate, controller.getActive(), `${worker.role} finished with status ${result.status}.`);
801
+ return result;
802
+ }));
755
803
  const allResults = [...researchResults];
756
804
  const executionMode = mode === "research" ? "research_only" : plan.executionMode;
757
805
  if (executionMode === "implement_and_review") {
@@ -765,10 +813,12 @@ async function orchestrateTeamRun(pi, ctx, goal, mode) {
765
813
  controller.update({ stage: "implementation" });
766
814
  persistState(pi, controller.getActive());
767
815
  syncRunUi(ctx, controller.getActive());
816
+ emitProgressUpdate(onUpdate, controller.getActive(), "Starting the implementation worker.");
768
817
  const implementationResult = await runWorker(pi, ctx, goal, plan, implementationWorker, allResults);
769
818
  controller.appendResult(implementationResult);
770
819
  persistState(pi, controller.getActive());
771
820
  syncRunUi(ctx, controller.getActive());
821
+ emitProgressUpdate(onUpdate, controller.getActive(), `Implementation worker finished with status ${implementationResult.status}.`);
772
822
  allResults.push(implementationResult);
773
823
  const reviewWorker = {
774
824
  id: "reviewer",
@@ -780,10 +830,12 @@ async function orchestrateTeamRun(pi, ctx, goal, mode) {
780
830
  controller.update({ stage: "review" });
781
831
  persistState(pi, controller.getActive());
782
832
  syncRunUi(ctx, controller.getActive());
833
+ emitProgressUpdate(onUpdate, controller.getActive(), "Starting the review worker.");
783
834
  const reviewResult = await runWorker(pi, ctx, goal, plan, reviewWorker, allResults);
784
835
  controller.appendResult(reviewResult);
785
836
  persistState(pi, controller.getActive());
786
837
  syncRunUi(ctx, controller.getActive());
838
+ emitProgressUpdate(onUpdate, controller.getActive(), `Review worker finished with status ${reviewResult.status}.`);
787
839
  allResults.push(reviewResult);
788
840
  }
789
841
  const status = allResults.some((result) => result.status === "failed")
@@ -803,6 +855,14 @@ async function orchestrateTeamRun(pi, ctx, goal, mode) {
803
855
  finalSummary: "",
804
856
  };
805
857
  const finalSummary = buildFinalSummary(provisionalReport);
858
+ controller.update({
859
+ stage: "finished",
860
+ status,
861
+ lastWorkerSummary: finalSummary,
862
+ });
863
+ persistState(pi, controller.getActive());
864
+ syncRunUi(ctx, controller.getActive());
865
+ emitProgressUpdate(onUpdate, controller.getActive(), `Team run finished with status ${status}.`);
806
866
  const report = controller.finish(status, finalSummary);
807
867
  if (!report) {
808
868
  return { ...provisionalReport, finalSummary };
@@ -887,13 +947,14 @@ export default async function teamExtension(pi) {
887
947
  description: "Delegate a task to a coordinated team of workers, but only when the user explicitly asked for Agent team or multi-agent execution.",
888
948
  guidance: "Only use team_run when the user explicitly requested Agent team, multi-agent, or subagent execution. Do not call it proactively.",
889
949
  parameters: TEAM_TOOL_PARAMS,
890
- execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
950
+ execute: async (_toolCallId, params, _signal, onUpdate, ctx) => {
891
951
  ensureExplicitTeamTrigger(ctx, params.goal);
892
952
  const controller = getController(pi);
893
953
  const active = controller.start(params.goal, params.mode ?? "auto");
894
954
  persistState(pi, active);
955
+ emitProgressUpdate(onUpdate, active, "Team run started.");
895
956
  try {
896
- const report = await orchestrateTeamRun(pi, ctx, params.goal, params.mode ?? "auto");
957
+ const report = await orchestrateTeamRun(pi, ctx, params.goal, params.mode ?? "auto", onUpdate);
897
958
  report.artifactPath = writeReportArtifact(report, ctx.cwd);
898
959
  persistReport(pi, report);
899
960
  return {
@@ -904,6 +965,7 @@ export default async function teamExtension(pi) {
904
965
  catch (error) {
905
966
  const message = error instanceof Error ? error.message : String(error);
906
967
  controller.update({ lastError: message, stage: "failed" });
968
+ emitProgressUpdate(onUpdate, controller.getActive(), message);
907
969
  const report = controller.finish("failed", message) ?? {
908
970
  id: active.id,
909
971
  goal: params.goal,
@@ -66,6 +66,21 @@ function asText(value) {
66
66
  return String(value);
67
67
  }
68
68
  }
69
+ function toolPayloadToText(value) {
70
+ if (value &&
71
+ typeof value === "object" &&
72
+ "content" in value &&
73
+ Array.isArray(value.content)) {
74
+ const text = value.content
75
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
76
+ .map((block) => block.text)
77
+ .join("\n");
78
+ if (text.trim()) {
79
+ return text;
80
+ }
81
+ }
82
+ return asText(value);
83
+ }
69
84
  function createSlashCommandsUpdate(session) {
70
85
  const commands = session.getSlashCommands();
71
86
  const seen = new Set();
@@ -98,6 +113,14 @@ function getMessageText(message) {
98
113
  .filter((part) => part.length > 0)
99
114
  .join("\n");
100
115
  }
116
+ function isVisibleCustomMessage(message) {
117
+ return (typeof message === "object" &&
118
+ message !== null &&
119
+ "role" in message &&
120
+ message.role === "custom" &&
121
+ "display" in message &&
122
+ message.display === true);
123
+ }
101
124
  function isMutatingTool(tool) {
102
125
  if (MUTATING_TOOL_NAMES.has(tool.name))
103
126
  return true;
@@ -328,6 +351,11 @@ class NanoPencilAgent {
328
351
  this.mapEventToAcp(params.sessionId, event);
329
352
  });
330
353
  try {
354
+ const extensionHandled = await this.session.tryExecuteExtensionCommand(userText);
355
+ if (extensionHandled) {
356
+ await this.emitSessionMetadata(sessionState);
357
+ return { stopReason: "end_turn" };
358
+ }
331
359
  // @ts-expect-error - source is for internal use
332
360
  await this.session.prompt(userText, { source: "acp" });
333
361
  await this.emitSessionMetadata(sessionState);
@@ -814,6 +842,21 @@ class NanoPencilAgent {
814
842
  }
815
843
  break;
816
844
  }
845
+ case "message_end":
846
+ if (isVisibleCustomMessage(event.message)) {
847
+ const text = getMessageText(event.message);
848
+ if (text.trim()) {
849
+ void this.connection.sessionUpdate({
850
+ sessionId,
851
+ update: {
852
+ sessionUpdate: "agent_message_chunk",
853
+ content: textToContent(text),
854
+ messageId: createMessageId(),
855
+ },
856
+ });
857
+ }
858
+ }
859
+ break;
817
860
  case "tool_execution_start":
818
861
  void this.connection.sessionUpdate({
819
862
  sessionId,
@@ -828,6 +871,26 @@ class NanoPencilAgent {
828
871
  },
829
872
  });
830
873
  break;
874
+ case "tool_execution_update":
875
+ void this.connection.sessionUpdate({
876
+ sessionId,
877
+ update: {
878
+ sessionUpdate: "tool_call_update",
879
+ toolCallId: event.toolCallId,
880
+ status: "pending",
881
+ content: [
882
+ {
883
+ type: "content",
884
+ content: {
885
+ type: "text",
886
+ text: toolPayloadToText(event.partialResult),
887
+ },
888
+ },
889
+ ],
890
+ rawOutput: event.partialResult,
891
+ },
892
+ });
893
+ break;
831
894
  case "tool_execution_end":
832
895
  void this.connection.sessionUpdate({
833
896
  sessionId,
@@ -840,7 +903,7 @@ class NanoPencilAgent {
840
903
  type: "content",
841
904
  content: {
842
905
  type: "text",
843
- text: asText(event.result),
906
+ text: toolPayloadToText(event.result),
844
907
  },
845
908
  },
846
909
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.11.15",
3
+ "version": "1.11.16",
4
4
  "description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
5
5
  "type": "module",
6
6
  "bin": {