@oyasmi/pipiclaw 0.6.6-beta.2 → 0.6.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.
@@ -11,15 +11,17 @@ export declare class ChannelRunner implements AgentRunner {
11
11
  private readonly channelDir;
12
12
  private readonly workspacePath;
13
13
  private readonly workspaceDir;
14
- private readonly session;
15
- private readonly agent;
16
- private readonly sessionManager;
14
+ private session;
15
+ private agent;
16
+ private sessionManager;
17
17
  private readonly settingsManager;
18
18
  private readonly modelRegistry;
19
19
  private readonly memoryLifecycle;
20
20
  private readonly memoryCandidateStore;
21
21
  private readonly sessionResourceGate;
22
22
  private readonly sessionReady;
23
+ private readonly sessionRuntime;
24
+ private sessionUnsubscribe?;
23
25
  private subAgentDiscovery;
24
26
  private activeModel;
25
27
  private currentSkills;
@@ -34,7 +36,6 @@ export declare class ChannelRunner implements AgentRunner {
34
36
  }>;
35
37
  handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
36
38
  queueSteer(text: string, userName?: string): Promise<void>;
37
- queueFollowUp(text: string, userName?: string): Promise<void>;
38
39
  flushMemoryForShutdown(): Promise<void>;
39
40
  getMemoryMaintenanceContext(): Promise<MemoryMaintenanceRuntimeContext>;
40
41
  abort(): Promise<void>;
@@ -54,6 +55,10 @@ export declare class ChannelRunner implements AgentRunner {
54
55
  private refreshSubAgentDiscovery;
55
56
  private reportSettingsDiagnostics;
56
57
  private reportConfigDiagnostics;
58
+ private createResourceLoader;
59
+ private createAgentSessionServices;
60
+ private createEmptyExtensionsResult;
61
+ private createSessionRuntime;
57
62
  private buildRuntimeTools;
58
63
  private rebuildSessionTools;
59
64
  private subscribeToSessionEvents;
@@ -1,5 +1,5 @@
1
1
  import { Agent } from "@mariozechner/pi-agent-core";
2
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
2
+ import { AgentSession, AgentSessionRuntime, AuthStorage, convertToLlm, createExtensionRuntime, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
3
3
  import { mkdir, readFile, writeFile } from "fs/promises";
4
4
  import { dirname, join, resolve } from "path";
5
5
  import * as log from "../log.js";
@@ -80,15 +80,14 @@ export class ChannelRunner {
80
80
  this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
81
81
  log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
82
82
  this.subAgentDiscovery = this.refreshSubAgentDiscovery();
83
- // Create tools
84
- const tools = this.buildRuntimeTools();
85
- // Create agent
83
+ const initialSessionManager = this.sessionManager;
84
+ const initialTools = this.buildRuntimeTools();
86
85
  this.agent = new Agent({
87
86
  initialState: {
88
87
  systemPrompt: "",
89
88
  model: this.activeModel,
90
89
  thinkingLevel: "off",
91
- tools,
90
+ tools: initialTools,
92
91
  },
93
92
  convertToLlm,
94
93
  getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
@@ -105,62 +104,33 @@ export class ChannelRunner {
105
104
  void this.recordMemoryActivity(event);
106
105
  },
107
106
  });
108
- const resourceLoader = new DefaultResourceLoader({
109
- cwd: process.cwd(),
110
- agentDir: APP_HOME_DIR,
111
- settingsManager: asSdkSettingsManager(this.settingsManager),
112
- extensionFactories: [
113
- this.memoryLifecycle.createExtensionFactory(),
114
- createCommandExtension({
115
- getCurrentModel: () => this.session.model ?? this.activeModel,
116
- getAvailableModels: async () => {
117
- this.modelRegistry.refresh();
118
- return await this.modelRegistry.getAvailable();
119
- },
120
- getSessionStats: () => this.session.getSessionStats(),
121
- getThinkingLevel: () => this.session.thinkingLevel,
122
- switchModel: async (model) => {
123
- await this.session.setModel(model);
124
- this.activeModel = model;
125
- },
126
- refreshSessionResources: async () => {
127
- await this.refreshSessionResources();
128
- },
129
- }),
130
- ],
131
- appendSystemPromptOverride: (base) => {
132
- const soul = getSoul(this.workspaceDir);
133
- const sections = [...base];
134
- if (soul) {
135
- sections.unshift(soul);
136
- }
137
- sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
138
- subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
139
- }));
140
- return sections;
141
- },
142
- agentsFilesOverride: () => {
143
- const agentConfig = getAgentConfig(this.channelDir);
144
- return {
145
- agentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],
146
- };
147
- },
148
- skillsOverride: (base) => ({
149
- skills: [...base.skills, ...this.currentSkills],
150
- diagnostics: base.diagnostics,
151
- }),
152
- });
153
- const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
154
- // Create AgentSession
107
+ const initialResourceLoader = this.createResourceLoader();
108
+ const baseToolsOverride = Object.fromEntries(initialTools.map((tool) => [tool.name, tool]));
155
109
  this.session = new AgentSession({
156
110
  agent: this.agent,
157
- sessionManager: this.sessionManager,
111
+ sessionManager: initialSessionManager,
158
112
  settingsManager: asSdkSettingsManager(this.settingsManager),
159
113
  cwd: process.cwd(),
160
114
  modelRegistry: this.modelRegistry,
161
- resourceLoader,
115
+ resourceLoader: initialResourceLoader,
162
116
  baseToolsOverride,
163
117
  });
118
+ this.sessionRuntime = new AgentSessionRuntime(this.session, this.createAgentSessionServices(initialResourceLoader), async ({ sessionManager, sessionStartEvent }) => {
119
+ const next = this.createSessionRuntime(sessionManager, sessionStartEvent);
120
+ return {
121
+ session: next.session,
122
+ extensionsResult: this.createEmptyExtensionsResult(),
123
+ services: this.createAgentSessionServices(next.resourceLoader),
124
+ diagnostics: [],
125
+ };
126
+ });
127
+ this.sessionRuntime.setRebindSession(async (session) => {
128
+ this.session = session;
129
+ this.agent = session.agent;
130
+ this.sessionManager = session.sessionManager;
131
+ await this.bindSessionExtensions();
132
+ this.subscribeToSessionEvents();
133
+ });
164
134
  this.sessionResourceGate = new SessionResourceGate(async () => {
165
135
  await this.reloadSessionResources();
166
136
  });
@@ -356,10 +326,7 @@ export class ChannelRunner {
356
326
  }
357
327
  }
358
328
  async queueSteer(text, userName) {
359
- await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"), userName);
360
- }
361
- async queueFollowUp(text, userName) {
362
- await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
329
+ await this.queueBusyMessage(this.requireQueuedMessage(text, "steer"), userName);
363
330
  }
364
331
  async flushMemoryForShutdown() {
365
332
  await this.memoryLifecycle.flushForShutdown();
@@ -437,7 +404,7 @@ export class ChannelRunner {
437
404
  const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
438
405
  return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
439
406
  }
440
- async queueBusyMessage(delivery, text, userName) {
407
+ async queueBusyMessage(text, userName) {
441
408
  if (!this.acceptingBusyMessages) {
442
409
  throw new Error("No task is currently running.");
443
410
  }
@@ -461,12 +428,7 @@ export class ChannelRunner {
461
428
  if (this.agentLoopStarted && !this.session.isStreaming) {
462
429
  throw new Error("No task is currently running.");
463
430
  }
464
- if (delivery === "followUp") {
465
- await this.session.followUp(queuedMessage);
466
- }
467
- else {
468
- await this.session.steer(queuedMessage);
469
- }
431
+ await this.session.steer(queuedMessage);
470
432
  };
471
433
  await this.sessionResourceGate.runPrompt(queueMessage);
472
434
  }
@@ -502,20 +464,18 @@ export class ChannelRunner {
502
464
  commandContextActions: {
503
465
  waitForIdle: () => this.session.agent.waitForIdle(),
504
466
  newSession: async (options) => {
505
- const success = await this.session.newSession(options);
506
- return { cancelled: !success };
467
+ return await this.sessionRuntime.newSession(options);
507
468
  },
508
- fork: async (entryId) => {
509
- const result = await this.session.fork(entryId);
469
+ fork: async (entryId, options) => {
470
+ const result = await this.sessionRuntime.fork(entryId, options);
510
471
  return { cancelled: result.cancelled };
511
472
  },
512
473
  navigateTree: async (targetId, options) => {
513
474
  const result = await this.session.navigateTree(targetId, options);
514
475
  return { cancelled: result.cancelled };
515
476
  },
516
- switchSession: async (sessionPath) => {
517
- const success = await this.session.switchSession(sessionPath);
518
- return { cancelled: !success };
477
+ switchSession: async (sessionPath, options) => {
478
+ return await this.sessionRuntime.switchSession(sessionPath, options);
519
479
  },
520
480
  reload: async () => {
521
481
  await this.refreshSessionResources();
@@ -565,6 +525,96 @@ export class ChannelRunner {
565
525
  log.logWarning(`[${this.channelId}] ${formatConfigDiagnostic(diagnostic)}`, diagnostic.path);
566
526
  }
567
527
  }
528
+ createResourceLoader() {
529
+ return new DefaultResourceLoader({
530
+ cwd: process.cwd(),
531
+ agentDir: APP_HOME_DIR,
532
+ settingsManager: asSdkSettingsManager(this.settingsManager),
533
+ extensionFactories: [
534
+ this.memoryLifecycle.createExtensionFactory(),
535
+ createCommandExtension({
536
+ getCurrentModel: () => this.session.model ?? this.activeModel,
537
+ getAvailableModels: async () => {
538
+ this.modelRegistry.refresh();
539
+ return await this.modelRegistry.getAvailable();
540
+ },
541
+ getSessionStats: () => this.session.getSessionStats(),
542
+ getThinkingLevel: () => this.session.thinkingLevel,
543
+ switchModel: async (model) => {
544
+ await this.session.setModel(model);
545
+ this.activeModel = model;
546
+ },
547
+ refreshSessionResources: async () => {
548
+ await this.refreshSessionResources();
549
+ },
550
+ }),
551
+ ],
552
+ appendSystemPromptOverride: (base) => {
553
+ const soul = getSoul(this.workspaceDir);
554
+ const sections = [...base];
555
+ if (soul) {
556
+ sections.unshift(soul);
557
+ }
558
+ sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
559
+ subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
560
+ }));
561
+ return sections;
562
+ },
563
+ agentsFilesOverride: () => {
564
+ const agentConfig = getAgentConfig(this.channelDir);
565
+ return {
566
+ agentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],
567
+ };
568
+ },
569
+ skillsOverride: (base) => ({
570
+ skills: [...base.skills, ...this.currentSkills],
571
+ diagnostics: base.diagnostics,
572
+ }),
573
+ });
574
+ }
575
+ createAgentSessionServices(resourceLoader) {
576
+ return {
577
+ cwd: process.cwd(),
578
+ agentDir: APP_HOME_DIR,
579
+ authStorage: AuthStorage.create(AUTH_CONFIG_PATH),
580
+ settingsManager: asSdkSettingsManager(this.settingsManager),
581
+ modelRegistry: this.modelRegistry,
582
+ resourceLoader,
583
+ diagnostics: [],
584
+ };
585
+ }
586
+ createEmptyExtensionsResult() {
587
+ return {
588
+ extensions: [],
589
+ errors: [],
590
+ runtime: createExtensionRuntime(),
591
+ };
592
+ }
593
+ createSessionRuntime(sessionManager, sessionStartEvent) {
594
+ const tools = this.buildRuntimeTools();
595
+ const agent = new Agent({
596
+ initialState: {
597
+ systemPrompt: "",
598
+ model: this.activeModel,
599
+ thinkingLevel: "off",
600
+ tools,
601
+ },
602
+ convertToLlm,
603
+ getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
604
+ });
605
+ const resourceLoader = this.createResourceLoader();
606
+ const session = new AgentSession({
607
+ agent,
608
+ sessionManager,
609
+ settingsManager: asSdkSettingsManager(this.settingsManager),
610
+ cwd: process.cwd(),
611
+ modelRegistry: this.modelRegistry,
612
+ resourceLoader,
613
+ baseToolsOverride: Object.fromEntries(tools.map((tool) => [tool.name, tool])),
614
+ sessionStartEvent,
615
+ });
616
+ return { agent, session, resourceLoader };
617
+ }
568
618
  buildRuntimeTools() {
569
619
  const securityLoad = loadSecurityConfigWithDiagnostics(APP_HOME_DIR);
570
620
  const toolsLoad = loadToolsConfigWithDiagnostics(APP_HOME_DIR);
@@ -589,13 +639,15 @@ export class ChannelRunner {
589
639
  }
590
640
  rebuildSessionTools() {
591
641
  const tools = this.buildRuntimeTools();
592
- this.agent.setTools(tools);
593
642
  this.session._baseToolsOverride =
594
643
  Object.fromEntries(tools.map((tool) => [tool.name, tool]));
644
+ this.agent.state.tools = tools;
645
+ this.session.setActiveToolsByName(tools.map((tool) => tool.name));
595
646
  }
596
647
  // === Session event subscription ===
597
648
  subscribeToSessionEvents() {
598
- this.session.subscribe(async (event) => {
649
+ this.sessionUnsubscribe?.();
650
+ this.sessionUnsubscribe = this.session.subscribe(async (event) => {
599
651
  if (isRecord(event) && event.type === "message_start") {
600
652
  this.agentLoopStarted = true;
601
653
  }
@@ -27,8 +27,8 @@ async function runCompact(ctx, customInstructions) {
27
27
  });
28
28
  });
29
29
  }
30
- function sendCommandResult(pi, text) {
31
- pi.sendMessage({
30
+ function sendCommandResult(sender, text) {
31
+ return sender.sendMessage({
32
32
  customType: COMMAND_RESULT_CUSTOM_TYPE,
33
33
  content: text,
34
34
  display: true,
@@ -83,13 +83,21 @@ ${available}`);
83
83
  pi.registerCommand("new", {
84
84
  description: "Start a new session",
85
85
  handler: async (_args, ctx) => {
86
- const result = await ctx.newSession();
87
- if (!result.cancelled) {
86
+ let sentFromReplacement = false;
87
+ const result = await ctx.newSession({
88
+ withSession: async (nextCtx) => {
89
+ sentFromReplacement = true;
90
+ await options.refreshSessionResources();
91
+ await sendCommandResult(nextCtx, `已开启新会话。\n\nSession ID: \`${nextCtx.sessionManager.getSessionId()}\``);
92
+ },
93
+ });
94
+ if (result.cancelled) {
95
+ sendCommandResult(pi, "新会话已取消。");
96
+ }
97
+ else if (!sentFromReplacement) {
88
98
  await options.refreshSessionResources();
99
+ sendCommandResult(pi, `已开启新会话。\n\nSession ID: \`${ctx.sessionManager.getSessionId()}\``);
89
100
  }
90
- sendCommandResult(pi, result.cancelled
91
- ? "新会话已取消。"
92
- : `已开启新会话。\n\nSession ID: \`${ctx.sessionManager.getSessionId()}\``);
93
101
  },
94
102
  });
95
103
  pi.registerCommand("compact", {
@@ -127,7 +127,6 @@ export async function handleSessionEvent(event, context) {
127
127
  const commandResultText = extractCustomCommandResultText(event.message);
128
128
  if (commandResultText) {
129
129
  runState.finalOutcome = { kind: "final", text: commandResultText };
130
- runState.finalResponseDelivered = false;
131
130
  log.logResponse(logCtx, commandResultText);
132
131
  queue.enqueue(async () => {
133
132
  const delivered = await ctx.respondPlain(commandResultText);
@@ -196,7 +195,6 @@ export async function handleSessionEvent(event, context) {
196
195
  return;
197
196
  }
198
197
  runState.finalOutcome = { kind: "final", text: finalText };
199
- runState.finalResponseDelivered = false;
200
198
  memoryLifecycle.noteCompletedAssistantTurn();
201
199
  log.logResponse(logCtx, finalText);
202
200
  queue.enqueue(async () => {
@@ -10,7 +10,6 @@ export interface AgentRunner {
10
10
  }>;
11
11
  handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
12
12
  queueSteer(text: string, userName?: string): Promise<void>;
13
- queueFollowUp(text: string, userName?: string): Promise<void>;
14
13
  flushMemoryForShutdown(): Promise<void>;
15
14
  getMemoryMaintenanceContext(): Promise<MemoryMaintenanceRuntimeContext>;
16
15
  abort(): Promise<void>;
@@ -45,6 +45,6 @@ export declare class MemoryLifecycle {
45
45
  private handleSessionBeforeCompact;
46
46
  private handleSessionCompact;
47
47
  private handleSessionBeforeSwitch;
48
- private handleSessionSwitch;
48
+ private handleSessionStart;
49
49
  private recordActivity;
50
50
  }
@@ -33,8 +33,8 @@ export class MemoryLifecycle {
33
33
  pi.on("session_before_switch", async (event) => {
34
34
  await this.handleSessionBeforeSwitch(event);
35
35
  });
36
- pi.on("session_switch", async (event) => {
37
- this.handleSessionSwitch(event);
36
+ pi.on("session_start", async (event) => {
37
+ this.handleSessionStart(event);
38
38
  });
39
39
  };
40
40
  }
@@ -204,7 +204,7 @@ export class MemoryLifecycle {
204
204
  }
205
205
  await this.runPreflightConsolidation("new-session");
206
206
  }
207
- handleSessionSwitch(event) {
207
+ handleSessionStart(event) {
208
208
  if (event.reason !== "new") {
209
209
  return;
210
210
  }
@@ -393,17 +393,21 @@ export function createRuntimeContext(options) {
393
393
  },
394
394
  async handleBusyMessage(event, bot, mode, queueText) {
395
395
  if (shuttingDown) {
396
- return true;
396
+ return { kind: "handled" };
397
397
  }
398
398
  const state = getState(event.channelId);
399
399
  const trimmedQueueText = queueText.trim();
400
+ if (!trimmedQueueText) {
401
+ const commandName = mode === "followUp" ? "followup" : "steer";
402
+ await bot.sendPlain(event.channelId, `Could not queue this message: /${commandName} requires a message.`);
403
+ return { kind: "handled" };
404
+ }
405
+ if (mode === "followUp") {
406
+ log.logInfo(`[${event.channelId}] Queued followUp as next task: ${trimmedQueueText.substring(0, 80)}`);
407
+ return { kind: "requeue", text: trimmedQueueText };
408
+ }
400
409
  try {
401
- if (mode === "followUp") {
402
- await state.runner.queueFollowUp(trimmedQueueText, event.userName);
403
- }
404
- else {
405
- await state.runner.queueSteer(trimmedQueueText, event.userName);
406
- }
410
+ await state.runner.queueSteer(trimmedQueueText, event.userName);
407
411
  await archiveIncomingMessage(event.channelId, {
408
412
  date: new Date().toISOString(),
409
413
  ts: event.ts,
@@ -414,26 +418,22 @@ export function createRuntimeContext(options) {
414
418
  deliveryMode: mode,
415
419
  skipContextSync: true,
416
420
  }, `${mode} message`);
417
- const confirmation = mode === "followUp"
418
- ? event.text.trim().startsWith("/")
419
- ? "Queued as follow-up. I’ll handle it after the current task completes."
420
- : "Queued as follow-up. I’ll handle it after the current task completes. Use `/steer <message>` to apply it after the current tool step finishes."
421
- : event.text.trim().startsWith("/")
422
- ? "Queued as steer. I’ll apply it after the current tool step finishes."
423
- : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
421
+ const confirmation = event.text.trim().startsWith("/")
422
+ ? "Queued as steer. I’ll apply it after the current tool step finishes."
423
+ : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
424
424
  await bot.sendPlain(event.channelId, confirmation);
425
425
  log.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);
426
- return true;
426
+ return { kind: "handled" };
427
427
  }
428
428
  catch (err) {
429
429
  const errMsg = err instanceof Error ? err.message : String(err);
430
430
  if (isNoRunningTaskQueueError(err)) {
431
431
  log.logInfo(`[${event.channelId}] Busy ${mode} window closed; requeueing as a normal message`);
432
- return false;
432
+ return { kind: "requeue", text: trimmedQueueText };
433
433
  }
434
434
  log.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);
435
435
  await bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);
436
- return true;
436
+ return { kind: "handled" };
437
437
  }
438
438
  },
439
439
  async handleEvent(event, bot, _isEvent) {
@@ -96,28 +96,10 @@ class ChannelDeliveryController {
96
96
  log.logWarning(`[${this.event.channelId}] Failed to archive bot response`, err instanceof Error ? err.message : String(err));
97
97
  });
98
98
  }
99
- resetProgressAfterFinal() {
100
- if (!this.finalResponseDelivered) {
101
- return;
102
- }
103
- this.progressSegments = [];
104
- this.cachedProgressText = "";
105
- this.progressTextDirty = false;
106
- this.mode = "progress";
107
- this.progressStartedAt = 0;
108
- this.progressWindowStartedAt = 0;
109
- this.toolCallCount = 0;
110
- this.sentProgressChars = 0;
111
- this.replayRequired = false;
112
- this.finalReplacementText = "";
113
- this.cardWarmupTriggered = false;
114
- this.finalResponseDelivered = false;
115
- }
116
99
  async appendProgress(text, shouldLog) {
117
- if (this.closed || !text.trim())
100
+ if (this.closed || this.finalResponseDelivered || !text.trim())
118
101
  return;
119
102
  this.clearCardWarmup();
120
- this.resetProgressAfterFinal();
121
103
  if (this.progressStartedAt === 0) {
122
104
  this.progressStartedAt = Date.now();
123
105
  }
@@ -142,8 +124,8 @@ class ChannelDeliveryController {
142
124
  this.bumpRevision(false);
143
125
  }
144
126
  async sendFinal(text, shouldLog) {
145
- if (this.closed)
146
- return false;
127
+ if (this.closed || this.finalResponseDelivered)
128
+ return this.finalResponseDelivered;
147
129
  this.clearCardWarmup();
148
130
  if (shouldLog) {
149
131
  this.archiveBotResponse(text);
@@ -158,7 +140,7 @@ class ChannelDeliveryController {
158
140
  return true;
159
141
  }
160
142
  async replaceWithFinal(text) {
161
- if (this.closed)
143
+ if (this.closed || this.finalResponseDelivered)
162
144
  return;
163
145
  this.clearCardWarmup();
164
146
  this.finalReplacementText = text;
@@ -47,11 +47,17 @@ export interface DingTalkContext {
47
47
  flush: () => Promise<void>;
48
48
  close: () => Promise<void>;
49
49
  }
50
+ export type BusyMessageResult = {
51
+ kind: "handled";
52
+ } | {
53
+ kind: "requeue";
54
+ text: string;
55
+ };
50
56
  export interface DingTalkHandler {
51
57
  isRunning(channelId: string): boolean;
52
58
  handleEvent(event: DingTalkEvent, bot: DingTalkBot, isEvent?: boolean): Promise<void>;
53
59
  handleStop(channelId: string, bot: DingTalkBot): Promise<void>;
54
- handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<boolean>;
60
+ handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<BusyMessageResult>;
55
61
  }
56
62
  export declare class DingTalkBot {
57
63
  private handler;
@@ -481,11 +481,12 @@ export class DingTalkBot {
481
481
  });
482
482
  return true;
483
483
  }
484
- enqueueStreamMessage(event) {
484
+ enqueueStreamMessage(event, textOverride) {
485
+ const queuedEvent = textOverride === undefined ? event : { ...event, text: textOverride };
485
486
  this.getQueue(event.channelId).enqueue(async () => {
486
487
  this.activeMessageProcessing = true;
487
488
  try {
488
- await this.handler.handleEvent(event, this);
489
+ await this.handler.handleEvent(queuedEvent, this);
489
490
  }
490
491
  finally {
491
492
  this.activeMessageProcessing = false;
@@ -870,16 +871,16 @@ export class DingTalkBot {
870
871
  return;
871
872
  }
872
873
  if (builtInCommand?.name === "steer") {
873
- const handled = await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
874
- if (!handled) {
875
- this.enqueueStreamMessage(event);
874
+ const result = await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
875
+ if (result.kind === "requeue") {
876
+ this.enqueueStreamMessage(event, result.text);
876
877
  }
877
878
  return;
878
879
  }
879
880
  if (builtInCommand?.name === "followup") {
880
- const handled = await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
881
- if (!handled) {
882
- this.enqueueStreamMessage(event);
881
+ const result = await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
882
+ if (result.kind === "requeue") {
883
+ this.enqueueStreamMessage(event, result.text);
883
884
  }
884
885
  return;
885
886
  }
@@ -891,9 +892,9 @@ export class DingTalkBot {
891
892
  await this.sendPlain(channelId, "A task is already running. Only `/stop`, `/steer <message>`, and `/followup <message>` are available while streaming.");
892
893
  return;
893
894
  }
894
- const handled = await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
895
- if (!handled) {
896
- this.enqueueStreamMessage(event);
895
+ const result = await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
896
+ if (result.kind === "requeue") {
897
+ this.enqueueStreamMessage(event, result.text);
897
898
  }
898
899
  return;
899
900
  }
@@ -1,5 +1,6 @@
1
1
  import { type AgentEvent, type AgentMessage, type AgentTool } from "@mariozechner/pi-agent-core";
2
2
  import type { Api, Model } from "@mariozechner/pi-ai";
3
+ import { Type } from "typebox";
3
4
  import type { MemoryCandidateStore } from "../memory/candidates.js";
4
5
  import type { Executor } from "../sandbox.js";
5
6
  import type { SecurityConfig } from "../security/types.js";
@@ -7,21 +8,21 @@ import type { PipiclawMemoryRecallSettings } from "../settings.js";
7
8
  import type { UsageTotals } from "../shared/types.js";
8
9
  import type { PipiclawWebToolsConfig } from "../tools/config.js";
9
10
  import { type ResolvedSubAgentConfig, type SubAgentDiscoveryResult } from "./discovery.js";
10
- declare const subagentSchema: import("@sinclair/typebox").TObject<{
11
- label: import("@sinclair/typebox").TString;
12
- agent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
13
- name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
14
- task: import("@sinclair/typebox").TString;
15
- systemPrompt: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
16
- tools: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
17
- model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
18
- maxTurns: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
19
- maxToolCalls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
20
- maxWallTimeSec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
21
- bashTimeoutSec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
22
- contextMode: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
23
- memory: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
24
- paths: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
11
+ declare const subagentSchema: Type.TObject<{
12
+ label: Type.TString;
13
+ agent: Type.TOptional<Type.TString>;
14
+ name: Type.TOptional<Type.TString>;
15
+ task: Type.TString;
16
+ systemPrompt: Type.TOptional<Type.TString>;
17
+ tools: Type.TOptional<Type.TArray<Type.TString>>;
18
+ model: Type.TOptional<Type.TString>;
19
+ maxTurns: Type.TOptional<Type.TNumber>;
20
+ maxToolCalls: Type.TOptional<Type.TNumber>;
21
+ maxWallTimeSec: Type.TOptional<Type.TNumber>;
22
+ bashTimeoutSec: Type.TOptional<Type.TNumber>;
23
+ contextMode: Type.TOptional<Type.TString>;
24
+ memory: Type.TOptional<Type.TString>;
25
+ paths: Type.TOptional<Type.TArray<Type.TString>>;
25
26
  }>;
26
27
  export interface SubAgentToolDetails {
27
28
  kind: "subagent";
@@ -1,6 +1,6 @@
1
1
  import { Agent } from "@mariozechner/pi-agent-core";
2
2
  import { convertToLlm } from "@mariozechner/pi-coding-agent";
3
- import { Type } from "@sinclair/typebox";
3
+ import { Type } from "typebox";
4
4
  import { readChannelSession } from "../memory/files.js";
5
5
  import { recallRelevantMemory } from "../memory/recall.js";
6
6
  import { formatModelReference } from "../models/utils.js";
@@ -1,9 +1,10 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
3
- declare const attachSchema: import("@sinclair/typebox").TObject<{
4
- label: import("@sinclair/typebox").TString;
5
- path: import("@sinclair/typebox").TString;
6
- title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
4
+ declare const attachSchema: Type.TObject<{
5
+ label: Type.TString;
6
+ path: Type.TString;
7
+ title: Type.TOptional<Type.TString>;
7
8
  }>;
8
9
  export type UploadFunction = (filePath: string, title?: string) => Promise<void>;
9
10
  export interface AttachToolOptions {
@@ -1,5 +1,5 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import { basename, resolve as resolvePath } from "path";
2
+ import { Type } from "typebox";
3
3
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
4
  import { logSecurityEvent } from "../security/logger.js";
5
5
  import { guardPath } from "../security/path-guard.js";
@@ -1,10 +1,11 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { Executor } from "../sandbox.js";
3
4
  import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
4
- declare const bashSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- command: import("@sinclair/typebox").TString;
7
- timeout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
5
+ declare const bashSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ command: Type.TString;
8
+ timeout: Type.TOptional<Type.TNumber>;
8
9
  }>;
9
10
  export interface BashToolOptions {
10
11
  defaultTimeoutSeconds?: number;
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { createWriteStream } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { Type } from "@sinclair/typebox";
5
+ import { Type } from "typebox";
6
6
  import { guardCommand } from "../security/command-guard.js";
7
7
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
8
8
  import { logSecurityEvent } from "../security/logger.js";
@@ -1,11 +1,12 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { Executor } from "../sandbox.js";
3
4
  import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
4
- declare const editSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- path: import("@sinclair/typebox").TString;
7
- oldText: import("@sinclair/typebox").TString;
8
- newText: import("@sinclair/typebox").TString;
5
+ declare const editSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ path: Type.TString;
8
+ oldText: Type.TString;
9
+ newText: Type.TString;
9
10
  }>;
10
11
  export interface EditToolOptions {
11
12
  securityConfig?: SecurityConfig;
@@ -1,5 +1,5 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import * as Diff from "diff";
2
+ import { Type } from "typebox";
3
3
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
4
  import { logSecurityEvent } from "../security/logger.js";
5
5
  import { guardPath } from "../security/path-guard.js";
@@ -1,11 +1,12 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { Executor } from "../sandbox.js";
3
4
  import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
4
- declare const readSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- path: import("@sinclair/typebox").TString;
7
- offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
8
- limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
5
+ declare const readSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ path: Type.TString;
8
+ offset: Type.TOptional<Type.TNumber>;
9
+ limit: Type.TOptional<Type.TNumber>;
9
10
  }>;
10
11
  export interface ReadToolOptions {
11
12
  securityConfig?: SecurityConfig;
@@ -1,5 +1,5 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import { extname } from "path";
2
+ import { Type } from "typebox";
3
3
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
4
  import { logSecurityEvent } from "../security/logger.js";
5
5
  import { guardPath } from "../security/path-guard.js";
@@ -1,11 +1,12 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
2
  import type { Api, Model } from "@mariozechner/pi-ai";
3
+ import { Type } from "typebox";
3
4
  import type { PipiclawSessionSearchSettings } from "../settings.js";
4
- declare const sessionSearchSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
- limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
8
- roleFilter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
5
+ declare const sessionSearchSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ query: Type.TOptional<Type.TString>;
8
+ limit: Type.TOptional<Type.TNumber>;
9
+ roleFilter: Type.TOptional<Type.TArray<Type.TString>>;
9
10
  }>;
10
11
  export interface SessionSearchToolOptions {
11
12
  channelDir: string;
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import { Type } from "typebox";
2
2
  import { searchChannelSessions } from "../memory/session-search.js";
3
3
  const sessionSearchSchema = Type.Object({
4
4
  label: Type.String({ description: "Brief description of what you're searching for and why (shown to user)" }),
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
- declare const skillListSchema: import("@sinclair/typebox").TObject<{
3
- label: import("@sinclair/typebox").TString;
2
+ import { Type } from "typebox";
3
+ declare const skillListSchema: Type.TObject<{
4
+ label: Type.TString;
4
5
  }>;
5
6
  export interface WorkspaceSkillSummary {
6
7
  name: string;
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { Type } from "@sinclair/typebox";
3
+ import { Type } from "typebox";
4
4
  import { validateSkillFrontmatter, validateSkillName } from "./skill-security.js";
5
5
  const skillListSchema = Type.Object({
6
6
  label: Type.String({ description: "Brief description of why you're listing workspace skills (shown to user)" }),
@@ -1,12 +1,13 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
- declare const skillManageSchema: import("@sinclair/typebox").TObject<{
3
- label: import("@sinclair/typebox").TString;
4
- action: import("@sinclair/typebox").TString;
5
- name: import("@sinclair/typebox").TString;
6
- content: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
- filePath: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
- find: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
9
- replace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
2
+ import { Type } from "typebox";
3
+ declare const skillManageSchema: Type.TObject<{
4
+ label: Type.TString;
5
+ action: Type.TString;
6
+ name: Type.TString;
7
+ content: Type.TOptional<Type.TString>;
8
+ filePath: Type.TOptional<Type.TString>;
9
+ find: Type.TOptional<Type.TString>;
10
+ replace: Type.TOptional<Type.TString>;
10
11
  }>;
11
12
  export type SkillManageAction = "create" | "patch" | "write_file";
12
13
  export interface SkillManageResult {
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { Type } from "@sinclair/typebox";
4
+ import { Type } from "typebox";
5
5
  import { createAtomicTempPath, writeFileAtomically } from "../shared/atomic-file.js";
6
6
  import { resolveSkillPath, resolveSkillSupportingFile, scanSkillContent, validateSkillMarkdown, } from "./skill-security.js";
7
7
  const skillManageSchema = Type.Object({
@@ -1,8 +1,9 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
- declare const skillViewSchema: import("@sinclair/typebox").TObject<{
3
- label: import("@sinclair/typebox").TString;
4
- name: import("@sinclair/typebox").TString;
5
- filePath: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
2
+ import { Type } from "typebox";
3
+ declare const skillViewSchema: Type.TObject<{
4
+ label: Type.TString;
5
+ name: Type.TString;
6
+ filePath: Type.TOptional<Type.TString>;
6
7
  }>;
7
8
  export interface SkillViewToolOptions {
8
9
  workspaceDir: string;
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { Type } from "@sinclair/typebox";
3
+ import { Type } from "typebox";
4
4
  import { resolveSkillPath, resolveSkillSupportingFile } from "./skill-security.js";
5
5
  const skillViewSchema = Type.Object({
6
6
  label: Type.String({ description: "Brief description of why you're viewing this skill (shown to user)" }),
@@ -1,11 +1,12 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { SecurityConfig } from "../security/types.js";
3
4
  import type { PipiclawWebToolsConfig } from "./config.js";
4
- declare const webFetchSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- url: import("@sinclair/typebox").TString;
7
- extractMode: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"markdown">, import("@sinclair/typebox").TLiteral<"text">]>>;
8
- maxChars: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
5
+ declare const webFetchSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ url: Type.TString;
8
+ extractMode: Type.TOptional<Type.TUnion<[Type.TLiteral<"markdown">, Type.TLiteral<"text">]>>;
9
+ maxChars: Type.TOptional<Type.TNumber>;
9
10
  }>;
10
11
  export interface WebFetchToolOptions {
11
12
  webConfig: PipiclawWebToolsConfig;
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import { Type } from "typebox";
2
2
  import { resolveWebFetchRequest } from "../web/config.js";
3
3
  import { runWebFetch } from "../web/fetch.js";
4
4
  const webFetchSchema = Type.Object({
@@ -1,10 +1,11 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { SecurityConfig } from "../security/types.js";
3
4
  import type { PipiclawWebToolsConfig } from "./config.js";
4
- declare const webSearchSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- query: import("@sinclair/typebox").TString;
7
- count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
5
+ declare const webSearchSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ query: Type.TString;
8
+ count: Type.TOptional<Type.TNumber>;
8
9
  }>;
9
10
  export interface WebSearchToolOptions {
10
11
  webConfig: PipiclawWebToolsConfig;
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import { Type } from "typebox";
2
2
  import { resolveWebSearchRequest } from "../web/config.js";
3
3
  import { runWebSearch } from "../web/search.js";
4
4
  const webSearchSchema = Type.Object({
@@ -1,10 +1,11 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { Type } from "typebox";
2
3
  import type { Executor } from "../sandbox.js";
3
4
  import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
4
- declare const writeSchema: import("@sinclair/typebox").TObject<{
5
- label: import("@sinclair/typebox").TString;
6
- path: import("@sinclair/typebox").TString;
7
- content: import("@sinclair/typebox").TString;
5
+ declare const writeSchema: Type.TObject<{
6
+ label: Type.TString;
7
+ path: Type.TString;
8
+ content: Type.TString;
8
9
  }>;
9
10
  export interface WriteToolOptions {
10
11
  securityConfig?: SecurityConfig;
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import { Type } from "typebox";
2
2
  import { writeContent } from "./write-content.js";
3
3
  const writeSchema = Type.Object({
4
4
  label: Type.String({ description: "Brief description of what you're writing (shown to user)" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.6-beta.2",
3
+ "version": "0.6.6",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,11 +33,10 @@
33
33
  "prepublishOnly": "npm run clean && npm run build && npm run check"
34
34
  },
35
35
  "dependencies": {
36
- "@mariozechner/pi-agent-core": "^0.63.0",
37
- "@mariozechner/pi-ai": "^0.63.0",
38
- "@mariozechner/pi-coding-agent": "^0.63.0",
36
+ "@mariozechner/pi-agent-core": "^0.70.2",
37
+ "@mariozechner/pi-ai": "^0.70.2",
38
+ "@mariozechner/pi-coding-agent": "^0.70.2",
39
39
  "@mozilla/readability": "^0.6.0",
40
- "@sinclair/typebox": "^0.34.0",
41
40
  "axios": "^1.7.0",
42
41
  "croner": "^9.1.0",
43
42
  "diff": "^8.0.2",
@@ -46,7 +45,8 @@
46
45
  "https-proxy-agent": "^7.0.6",
47
46
  "jsdom": "^26.1.0",
48
47
  "proxy-from-env": "^1.1.0",
49
- "socks-proxy-agent": "^8.0.5"
48
+ "socks-proxy-agent": "^8.0.5",
49
+ "typebox": "^1.1.34"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "2.3.5",