@gajae-code/agent-core 0.2.5 → 0.3.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.
@@ -34,6 +34,8 @@ export interface AgentOptions {
34
34
  * - "wait": defer steering until the current turn completes
35
35
  */
36
36
  interruptMode?: "immediate" | "wait";
37
+ /** Cooperative pause checkpoint passed through to AgentLoopConfig.shouldPause. */
38
+ shouldPause?: AgentLoopConfig["shouldPause"];
37
39
  /**
38
40
  * API format for Kimi Code provider: "openai" or "anthropic" (default: "anthropic")
39
41
  */
@@ -289,6 +291,7 @@ export declare class Agent {
289
291
  setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
290
292
  setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
291
293
  setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
294
+ setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void;
292
295
  emitExternalEvent(event: AgentEvent): void;
293
296
  createExternalEventEmitterForCurrentRun(): ((event: AgentEvent) => void) | undefined;
294
297
  setSystemPrompt(v: string[]): void;
@@ -318,6 +321,21 @@ export declare class Agent {
318
321
  clearFollowUpQueue(): void;
319
322
  clearAllQueues(): void;
320
323
  hasQueuedMessages(): boolean;
324
+ hasQueuedSteering(): boolean;
325
+ /**
326
+ * Snapshot the steering queue without mutating it. Used to preserve queued
327
+ * steering across maintenance ops (compaction/handoff) that call reset().
328
+ */
329
+ snapshotSteering(): AgentMessage[];
330
+ /**
331
+ * Restore previously snapshotted steering messages ahead of any newly
332
+ * queued ones. No-op for an empty snapshot.
333
+ */
334
+ restoreSteering(messages: AgentMessage[]): void;
335
+ /** Snapshot the follow-up queue without mutating it. */
336
+ snapshotFollowUp(): AgentMessage[];
337
+ /** Restore previously snapshotted follow-up messages ahead of any newly queued ones. */
338
+ restoreFollowUp(messages: AgentMessage[]): void;
321
339
  /**
322
340
  * Remove and return the last steering message from the queue (LIFO).
323
341
  * Used by dequeue keybinding.
@@ -103,6 +103,15 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
103
103
  * continues with another turn.
104
104
  */
105
105
  getFollowUpMessages?: () => Promise<AgentMessage[]>;
106
+ /**
107
+ * Cooperative pause checkpoint evaluated at safe loop boundaries.
108
+ *
109
+ * Called after completed tool execution has been emitted and before the loop
110
+ * polls steering/follow-up queues or schedules another assistant response.
111
+ * Returning true ends the current loop with `agent_end.stopReason === "paused"`
112
+ * without aborting any in-flight model or tool work.
113
+ */
114
+ shouldPause?: () => boolean;
106
115
  /**
107
116
  * Hook fired right before the loop would exit.
108
117
  *
@@ -375,6 +384,8 @@ export type AgentEvent = {
375
384
  } | {
376
385
  type: "agent_end";
377
386
  messages: AgentMessage[];
387
+ /** Indicates whether the loop ended normally or suspended at a pause checkpoint. */
388
+ stopReason?: "completed" | "paused";
378
389
  /** Present iff `AgentTelemetryConfig` was supplied on this run. */
379
390
  telemetry?: AgentRunSummary;
380
391
  coverage?: AgentRunCoverage;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.2.5",
4
+ "version": "0.3.0",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -35,9 +35,9 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@gajae-code/ai": "0.2.5",
39
- "@gajae-code/natives": "0.2.5",
40
- "@gajae-code/utils": "0.2.5",
38
+ "@gajae-code/ai": "0.3.0",
39
+ "@gajae-code/natives": "0.3.0",
40
+ "@gajae-code/utils": "0.3.0",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -203,13 +203,15 @@ function buildAgentEndEvent(
203
203
  messages: AgentMessage[],
204
204
  telemetry: AgentTelemetry | undefined,
205
205
  stepCount: number,
206
+ stopReason: "completed" | "paused" = "completed",
206
207
  ): Extract<AgentEvent, { type: "agent_end" }> {
207
- if (!telemetry) return { type: "agent_end", messages };
208
+ const base = { type: "agent_end" as const, messages, stopReason };
209
+ if (!telemetry) return base;
208
210
  const snapshot = telemetry.collector.snapshot({ stepCount });
209
211
  if (telemetry.collector.markRunEnded()) {
210
212
  fireOnRunEnd(telemetry, snapshot.summary, snapshot.coverage);
211
213
  }
212
- return { type: "agent_end", messages, telemetry: snapshot.summary, coverage: snapshot.coverage };
214
+ return { ...base, telemetry: snapshot.summary, coverage: snapshot.coverage };
213
215
  }
214
216
 
215
217
  /**
@@ -585,11 +587,26 @@ async function runLoopBody(
585
587
 
586
588
  stream.push({ type: "turn_end", message, toolResults });
587
589
 
588
- pendingMessages = steeringMessagesFromExecution ?? ((await config.getSteeringMessages?.()) || []);
590
+ if (steeringMessagesFromExecution && steeringMessagesFromExecution.length > 0) {
591
+ pendingMessages = steeringMessagesFromExecution;
592
+ continue;
593
+ }
594
+ pendingMessages = (await config.getSteeringMessages?.()) || [];
595
+ if (pendingMessages.length > 0) continue;
596
+ if (config.shouldPause?.()) {
597
+ stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count, "paused"));
598
+ stream.end(newMessages);
599
+ return;
600
+ }
589
601
  }
590
602
 
591
603
  // Agent would stop here. Check for follow-up messages.
592
604
  await config.onBeforeYield?.();
605
+ if (config.shouldPause?.()) {
606
+ stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count, "paused"));
607
+ stream.end(newMessages);
608
+ return;
609
+ }
593
610
  const followUpMessages = (await config.getFollowUpMessages?.()) || [];
594
611
  if (followUpMessages.length > 0) {
595
612
  // Set as pending so inner loop processes them
package/src/agent.ts CHANGED
@@ -99,6 +99,8 @@ export interface AgentOptions {
99
99
  * - "wait": defer steering until the current turn completes
100
100
  */
101
101
  interruptMode?: "immediate" | "wait";
102
+ /** Cooperative pause checkpoint passed through to AgentLoopConfig.shouldPause. */
103
+ shouldPause?: AgentLoopConfig["shouldPause"];
102
104
 
103
105
  /**
104
106
  * API format for Kimi Code provider: "openai" or "anthropic" (default: "anthropic")
@@ -309,6 +311,7 @@ export class Agent {
309
311
  #onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
310
312
  #onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
311
313
  #onBeforeYield?: () => Promise<void> | void;
314
+ #shouldPause?: AgentLoopConfig["shouldPause"];
312
315
  #telemetry?: AgentLoopConfig["telemetry"];
313
316
  #appendOnlyContext?: AppendOnlyContextManager;
314
317
 
@@ -371,6 +374,7 @@ export class Agent {
371
374
  this.#getToolChoice = opts.getToolChoice;
372
375
  this.#onAssistantMessageEvent = opts.onAssistantMessageEvent;
373
376
  this.#onHarmonyLeak = opts.onHarmonyLeak;
377
+ this.#shouldPause = opts.shouldPause;
374
378
  this.beforeToolCall = opts.beforeToolCall;
375
379
  this.afterToolCall = opts.afterToolCall;
376
380
  this.#telemetry = opts.telemetry;
@@ -625,6 +629,10 @@ export class Agent {
625
629
  this.#onBeforeYield = fn;
626
630
  }
627
631
 
632
+ setShouldPause(fn: AgentLoopConfig["shouldPause"] | undefined): void {
633
+ this.#shouldPause = fn;
634
+ }
635
+
628
636
  emitExternalEvent(event: AgentEvent) {
629
637
  switch (event.type) {
630
638
  case "message_start":
@@ -859,6 +867,38 @@ export class Agent {
859
867
  return this.#steeringQueue.length > 0 || this.#followUpQueue.length > 0;
860
868
  }
861
869
 
870
+ hasQueuedSteering(): boolean {
871
+ return this.#steeringQueue.length > 0;
872
+ }
873
+
874
+ /**
875
+ * Snapshot the steering queue without mutating it. Used to preserve queued
876
+ * steering across maintenance ops (compaction/handoff) that call reset().
877
+ */
878
+ snapshotSteering(): AgentMessage[] {
879
+ return this.#steeringQueue.slice();
880
+ }
881
+
882
+ /**
883
+ * Restore previously snapshotted steering messages ahead of any newly
884
+ * queued ones. No-op for an empty snapshot.
885
+ */
886
+ restoreSteering(messages: AgentMessage[]): void {
887
+ if (messages.length === 0) return;
888
+ this.#steeringQueue = [...messages, ...this.#steeringQueue];
889
+ }
890
+
891
+ /** Snapshot the follow-up queue without mutating it. */
892
+ snapshotFollowUp(): AgentMessage[] {
893
+ return this.#followUpQueue.slice();
894
+ }
895
+
896
+ /** Restore previously snapshotted follow-up messages ahead of any newly queued ones. */
897
+ restoreFollowUp(messages: AgentMessage[]): void {
898
+ if (messages.length === 0) return;
899
+ this.#followUpQueue = [...messages, ...this.#followUpQueue];
900
+ }
901
+
862
902
  #dequeueSteeringMessages(): AgentMessage[] {
863
903
  if (this.#steeringMode === "one-at-a-time") {
864
904
  if (this.#steeringQueue.length > 0) {
@@ -1196,6 +1236,10 @@ export class Agent {
1196
1236
  if (this.#activeRunId !== runId) return;
1197
1237
  await this.#onBeforeYield?.();
1198
1238
  },
1239
+ shouldPause: () => {
1240
+ if (this.#activeRunId !== runId) return false;
1241
+ return this.#shouldPause?.() === true;
1242
+ },
1199
1243
  telemetry: this.#telemetry,
1200
1244
  };
1201
1245
 
package/src/types.ts CHANGED
@@ -132,6 +132,15 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
132
132
  * continues with another turn.
133
133
  */
134
134
  getFollowUpMessages?: () => Promise<AgentMessage[]>;
135
+ /**
136
+ * Cooperative pause checkpoint evaluated at safe loop boundaries.
137
+ *
138
+ * Called after completed tool execution has been emitted and before the loop
139
+ * polls steering/follow-up queues or schedules another assistant response.
140
+ * Returning true ends the current loop with `agent_end.stopReason === "paused"`
141
+ * without aborting any in-flight model or tool work.
142
+ */
143
+ shouldPause?: () => boolean;
135
144
  /**
136
145
  * Hook fired right before the loop would exit.
137
146
  *
@@ -458,6 +467,8 @@ export type AgentEvent =
458
467
  | {
459
468
  type: "agent_end";
460
469
  messages: AgentMessage[];
470
+ /** Indicates whether the loop ended normally or suspended at a pause checkpoint. */
471
+ stopReason?: "completed" | "paused";
461
472
  /** Present iff `AgentTelemetryConfig` was supplied on this run. */
462
473
  telemetry?: AgentRunSummary;
463
474
  coverage?: AgentRunCoverage;