@oyasmi/pipiclaw 0.6.6-beta.1 → 0.6.6-beta.3

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.
@@ -25,6 +25,7 @@ export declare class ChannelRunner implements AgentRunner {
25
25
  private currentSkills;
26
26
  private firstTurnMemoryBootstrapPending;
27
27
  private acceptingBusyMessages;
28
+ private agentLoopStarted;
28
29
  private runState;
29
30
  constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
30
31
  run(ctx: DingTalkContext, store: ChannelStore): Promise<{
@@ -33,7 +34,6 @@ export declare class ChannelRunner implements AgentRunner {
33
34
  }>;
34
35
  handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
35
36
  queueSteer(text: string, userName?: string): Promise<void>;
36
- queueFollowUp(text: string, userName?: string): Promise<void>;
37
37
  flushMemoryForShutdown(): Promise<void>;
38
38
  getMemoryMaintenanceContext(): Promise<MemoryMaintenanceRuntimeContext>;
39
39
  abort(): Promise<void>;
@@ -54,6 +54,7 @@ export class ChannelRunner {
54
54
  constructor(sandboxConfig, channelId, channelDir) {
55
55
  this.firstTurnMemoryBootstrapPending = true;
56
56
  this.acceptingBusyMessages = false;
57
+ this.agentLoopStarted = false;
57
58
  // --- Per run ---
58
59
  this.runState = createEmptyRunState();
59
60
  this.sandboxConfig = sandboxConfig;
@@ -171,6 +172,7 @@ export class ChannelRunner {
171
172
  async run(ctx, store) {
172
173
  this.resetRunState(ctx, store);
173
174
  this.acceptingBusyMessages = true;
175
+ this.agentLoopStarted = false;
174
176
  const runQueue = createRunQueue(ctx);
175
177
  this.runState.queue = runQueue.queue;
176
178
  let promptSubmitted = false;
@@ -238,6 +240,7 @@ export class ChannelRunner {
238
240
  }
239
241
  finally {
240
242
  this.acceptingBusyMessages = false;
243
+ this.agentLoopStarted = false;
241
244
  if (!promptSubmitted) {
242
245
  const discarded = this.session.clearQueue();
243
246
  const discardedCount = discarded.steering.length + discarded.followUp.length;
@@ -353,10 +356,7 @@ export class ChannelRunner {
353
356
  }
354
357
  }
355
358
  async queueSteer(text, userName) {
356
- await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"), userName);
357
- }
358
- async queueFollowUp(text, userName) {
359
- await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
359
+ await this.queueBusyMessage(this.requireQueuedMessage(text, "steer"), userName);
360
360
  }
361
361
  async flushMemoryForShutdown() {
362
362
  await this.memoryLifecycle.flushForShutdown();
@@ -434,10 +434,13 @@ export class ChannelRunner {
434
434
  const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
435
435
  return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
436
436
  }
437
- async queueBusyMessage(delivery, text, userName) {
437
+ async queueBusyMessage(text, userName) {
438
438
  if (!this.acceptingBusyMessages) {
439
439
  throw new Error("No task is currently running.");
440
440
  }
441
+ if (this.agentLoopStarted && !this.session.isStreaming) {
442
+ throw new Error("No task is currently running.");
443
+ }
441
444
  await this.ensureSessionReady();
442
445
  const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
443
446
  if (clippedText !== text.trim()) {
@@ -448,13 +451,14 @@ export class ChannelRunner {
448
451
  if (!this.acceptingBusyMessages) {
449
452
  throw new Error("No task is currently running.");
450
453
  }
454
+ if (this.agentLoopStarted && !this.session.isStreaming) {
455
+ throw new Error("No task is currently running.");
456
+ }
451
457
  const queueMessage = async () => {
452
- if (delivery === "followUp") {
453
- await this.session.followUp(queuedMessage);
454
- }
455
- else {
456
- await this.session.steer(queuedMessage);
458
+ if (this.agentLoopStarted && !this.session.isStreaming) {
459
+ throw new Error("No task is currently running.");
457
460
  }
461
+ await this.session.steer(queuedMessage);
458
462
  };
459
463
  await this.sessionResourceGate.runPrompt(queueMessage);
460
464
  }
@@ -584,6 +588,9 @@ export class ChannelRunner {
584
588
  // === Session event subscription ===
585
589
  subscribeToSessionEvents() {
586
590
  this.session.subscribe(async (event) => {
591
+ if (isRecord(event) && event.type === "message_start") {
592
+ this.agentLoopStarted = true;
593
+ }
587
594
  if (isRecord(event) && "reason" in event && event.reason === "new") {
588
595
  this.firstTurnMemoryBootstrapPending = true;
589
596
  }
@@ -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>;
@@ -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) {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.6-beta.1",
3
+ "version": "0.6.6-beta.3",
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": {