@archships/dim-agent-sdk 0.0.4 → 0.0.5

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.
package/README.md CHANGED
@@ -34,8 +34,16 @@ const session = await agent.createSession({
34
34
  systemPrompt: 'You are a coding agent. Use tools when needed.',
35
35
  })
36
36
 
37
- const message = await session.send('Create hello.txt in the current directory and write hello world into it.')
38
- console.log(message.content)
37
+ const itemId = session.send('Create hello.txt in the current directory and write hello world into it.')
38
+
39
+ for await (const event of session.receive()) {
40
+ if (event.itemId !== itemId)
41
+ continue
42
+ if (event.type === 'text_delta')
43
+ process.stdout.write(event.delta)
44
+ if (event.type === 'done')
45
+ console.log(event.message.content)
46
+ }
39
47
  ```
40
48
 
41
49
  ## Manual
@@ -48,7 +56,8 @@ console.log(message.content)
48
56
 
49
57
  - Canonical content / message / tool / model / state contracts
50
58
  - `createAgent()` -> `Agent` -> `Session`
51
- - Session stream events: `text_delta`, optional `thinking_delta`, `tool_call`, `plugin_event`, `tool_result`, `done`
59
+ - Queue-first session flow: `send()`, `sendBatch()`, `steer()`, `receive()`, `getQueueStatus()`
60
+ - Session events: `text_delta`, optional `thinking_delta`, `tool_call`, `plugin_event`, `tool_result`, `done`, `error`
52
61
  - Provider adapters: `openai-compatible`, `openai-responses`, `anthropic`, `gemini`, `zenmux`, `aihubmix`, `aihubmix-responses`, `moonshotai`, `deepseek`, `xai`, `xai-responses`
53
62
  - Builtin tools: `read`, `write`, `edit`, `exec`
54
63
  - Hook-first plugin integration
@@ -1,5 +1,5 @@
1
1
  import type { DimPlugin } from '@archships/dim-plugin-api';
2
- import type { CompactionOptions, Message, MessageContentInput, ModelClient, Tool } from '../contracts';
2
+ import type { CompactionOptions, Message, MessageContentInput, MessageQueueConfig, ModelClient, Tool } from '../contracts';
3
3
  import type { ContextManager } from '../context';
4
4
  import type { StateStore } from '../persistence/store';
5
5
  import type { PermissionSpec } from '../services';
@@ -26,4 +26,5 @@ export interface CreateSessionOptions {
26
26
  metadata?: Record<string, unknown>;
27
27
  messages?: Message[];
28
28
  systemPrompt?: MessageContentInput;
29
+ messageQueueConfig?: Partial<MessageQueueConfig>;
29
30
  }
@@ -19,6 +19,7 @@ export interface LoopRunnerDependencies {
19
19
  export interface LoopRunnerRunOptions {
20
20
  signal?: AbortSignal;
21
21
  onDone?: (message: AssistantMessage) => Promise<void> | void;
22
+ persistentPromptSegments?: string[];
22
23
  }
23
24
  export declare class LoopRunner {
24
25
  private readonly messageFactory;
@@ -1,4 +1,4 @@
1
- import type { CompactionOptions, CompactionState, Message, ModelClient, ModelRef, PluginSessionStateEntry, Usage } from '../contracts';
1
+ import type { CompactionOptions, CompactionState, Message, MessageQueueSnapshot, ModelClient, ModelRef, PluginSessionStateEntry, Usage } from '../contracts';
2
2
  import type { ToolRegistry } from '../tools/tool-registry';
3
3
  export interface RuntimeSessionState {
4
4
  id: string;
@@ -13,6 +13,7 @@ export interface RuntimeSessionState {
13
13
  usage: Usage;
14
14
  compaction: CompactionState;
15
15
  pluginState: Record<string, PluginSessionStateEntry>;
16
+ messageQueue: MessageQueueSnapshot;
16
17
  compactionOptions?: CompactionOptions;
17
18
  createdAt: number;
18
19
  updatedAt: number;
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage, CompactionOptions, CompactionState, CompactionUpdate, Message, MessageContentInput, ModelClient, ModelRef, PluginSessionStateEntry, SessionStatus, SessionSnapshot, SessionStreamEvent, Usage } from '../contracts';
1
+ import type { CompactionOptions, CompactionState, CompactionUpdate, Message, MessageContentInput, MessageQueueConfig, MessageQueueSnapshot, MessageQueueStatus, PendingMessageKind, ModelClient, ModelRef, PluginSessionStateEntry, SessionStatus, SessionSnapshot, SessionStreamEvent, Usage } from '../contracts';
2
2
  import type { ContextManager } from '../context';
3
3
  import type { StateStore } from '../persistence/store';
4
4
  import type { PluginHost } from '../plugin-host/plugin-host';
@@ -22,6 +22,8 @@ export interface SessionOptions {
22
22
  usage?: Usage;
23
23
  compaction?: CompactionState;
24
24
  pluginState?: Record<string, PluginSessionStateEntry>;
25
+ messageQueue?: MessageQueueSnapshot;
26
+ messageQueueConfig?: Partial<MessageQueueConfig>;
25
27
  compactionOptions?: CompactionOptions;
26
28
  createdAt?: number;
27
29
  updatedAt?: number;
@@ -33,6 +35,15 @@ export interface SessionOptions {
33
35
  compactionService: DefaultCompactionService;
34
36
  pluginStateService: DefaultPluginStateService;
35
37
  }
38
+ interface SessionQueueSendOptions {
39
+ priority?: number;
40
+ }
41
+ interface SessionReceiveOptions {
42
+ signal?: AbortSignal;
43
+ }
44
+ interface ClearQueueFilter {
45
+ kind?: PendingMessageKind;
46
+ }
36
47
  export declare class Session {
37
48
  readonly id: string;
38
49
  readonly createdAt: number;
@@ -46,7 +57,14 @@ export declare class Session {
46
57
  private readonly compactionService;
47
58
  private readonly pluginStateService;
48
59
  private readonly pluginControllers;
60
+ private readonly bufferedEvents;
61
+ private readonly stateChangeWaiters;
49
62
  private activeRunCount;
63
+ private processingPromise;
64
+ private receiveActive;
65
+ private activeReceiveSignal?;
66
+ private haltProcessingUntilReceive;
67
+ private saveChain;
50
68
  constructor(options: SessionOptions);
51
69
  get messages(): Message[];
52
70
  get usage(): Usage;
@@ -58,16 +76,37 @@ export declare class Session {
58
76
  get updatedAt(): number;
59
77
  getCwd(): string | undefined;
60
78
  setCwd(cwd: string): void;
79
+ send(input: MessageContentInput, options?: SessionQueueSendOptions): string;
80
+ sendBatch(items: Array<{
81
+ input: MessageContentInput;
82
+ options?: SessionQueueSendOptions;
83
+ }>): string[];
84
+ steer(input: MessageContentInput, options?: SessionQueueSendOptions): string;
85
+ cancelQueuedItem(itemId: string): boolean;
86
+ getQueueStatus(): MessageQueueStatus;
87
+ clearQueue(filter?: ClearQueueFilter): number;
88
+ receive(options?: SessionReceiveOptions): AsyncGenerator<SessionStreamEvent, void, void>;
61
89
  compact(update: CompactionUpdate): Promise<void>;
62
90
  save(): Promise<void>;
91
+ private writeSnapshot;
63
92
  toSnapshot(): SessionSnapshot;
64
- send(input: MessageContentInput, options?: {
65
- signal?: AbortSignal;
66
- }): Promise<AssistantMessage>;
67
- stream(input: MessageContentInput, options?: {
68
- signal?: AbortSignal;
69
- }): AsyncGenerator<SessionStreamEvent, AssistantMessage, void>;
93
+ private enqueueItem;
94
+ private onQueueMutated;
95
+ private hasRunnableItems;
96
+ private ensureQueueProcessing;
97
+ private processQueue;
98
+ private dequeueNextRunnableItem;
99
+ private processQueuedItem;
100
+ private attachQueueContext;
101
+ private createErrorEvent;
102
+ private pushBufferedEvent;
103
+ private scheduleSave;
104
+ private persistStateQuietly;
105
+ private enqueueSave;
106
+ private waitForStateChange;
107
+ private notifyStateChange;
70
108
  private createLoopRunner;
71
109
  private readEstimatedInputTokens;
72
110
  private createHookContext;
73
111
  }
112
+ export {};
@@ -1,6 +1,12 @@
1
1
  import type { SessionErrorPayload, Usage } from './common';
2
2
  import type { AssistantMessage } from './message';
3
+ import type { PendingMessageKind } from './state';
3
4
  import type { CallToolResult, ToolCall } from './tool';
5
+ export interface SessionQueueEventContext {
6
+ itemId?: string;
7
+ itemKind?: PendingMessageKind;
8
+ priority?: number;
9
+ }
4
10
  export interface SessionPluginEvent {
5
11
  type: 'plugin_event';
6
12
  sessionId: string;
@@ -8,30 +14,30 @@ export interface SessionPluginEvent {
8
14
  event: string;
9
15
  data?: Record<string, unknown>;
10
16
  }
11
- export type SessionStreamEvent = {
17
+ export type SessionStreamEvent = ({
12
18
  type: 'text_delta';
13
19
  sessionId: string;
14
20
  delta: string;
15
- } | {
21
+ } & SessionQueueEventContext) | ({
16
22
  type: 'thinking_delta';
17
23
  sessionId: string;
18
24
  delta: string;
19
- } | {
25
+ } & SessionQueueEventContext) | ({
20
26
  type: 'tool_call';
21
27
  sessionId: string;
22
28
  toolCall: ToolCall;
23
- } | {
29
+ } & SessionQueueEventContext) | ({
24
30
  type: 'tool_result';
25
31
  sessionId: string;
26
32
  toolCallId: string;
27
33
  result: CallToolResult;
28
- } | SessionPluginEvent | {
34
+ } & SessionQueueEventContext) | (SessionPluginEvent & SessionQueueEventContext) | ({
29
35
  type: 'done';
30
36
  sessionId: string;
31
37
  message: AssistantMessage;
32
38
  usage?: Usage;
33
- } | {
39
+ } & SessionQueueEventContext) | ({
34
40
  type: 'error';
35
41
  sessionId: string;
36
42
  error: SessionErrorPayload;
37
- };
43
+ } & SessionQueueEventContext);
@@ -1,10 +1,40 @@
1
1
  import type { CompactionState } from './compaction';
2
+ import type { MessageContentInput } from './content';
2
3
  import type { PluginSessionStateEntry } from './plugin-state';
3
4
  import type { Usage } from './common';
4
5
  import type { Message } from './message';
5
6
  import type { ModelRef } from './model';
7
+ export interface MessageQueueConfig {
8
+ autoProcessQueue: boolean;
9
+ maxQueueSize?: number;
10
+ }
11
+ export type PendingMessageKind = 'user' | 'steer';
12
+ export interface PendingMessage {
13
+ id: string;
14
+ input: MessageContentInput;
15
+ kind: PendingMessageKind;
16
+ priority: number;
17
+ enqueuedAt: number;
18
+ }
19
+ export interface MessageQueueSnapshot {
20
+ items: PendingMessage[];
21
+ config: MessageQueueConfig;
22
+ }
23
+ export interface MessageQueueStatusItem {
24
+ id: string;
25
+ kind: PendingMessageKind;
26
+ priority: number;
27
+ enqueuedAt: number;
28
+ preview: string;
29
+ }
30
+ export interface MessageQueueStatus {
31
+ length: number;
32
+ items: MessageQueueStatusItem[];
33
+ isProcessing: boolean;
34
+ config: MessageQueueConfig;
35
+ }
6
36
  export interface SessionSnapshot {
7
- schemaVersion: 1;
37
+ schemaVersion: 1 | 2;
8
38
  sessionId: string;
9
39
  model?: ModelRef;
10
40
  cwd?: string;
@@ -15,6 +45,7 @@ export interface SessionSnapshot {
15
45
  createdAt: number;
16
46
  updatedAt: number;
17
47
  metadata?: Record<string, unknown>;
48
+ messageQueue?: MessageQueueSnapshot;
18
49
  }
19
50
  export interface SessionStatus {
20
51
  sessionId: string;
@@ -1,4 +1,4 @@
1
- export type { CallToolResult, CompactionBudgetInfo, CompactionCheckpoint, CompactionEstimator, CompactionEstimatorInput, CompactionOptions, CompactionService, CompactionState, CompactionTriggerReason, CompactionUpdate, ContentBlock, Message, MessageContentInput, ModelAdapter, ModelClient, ModelRequest, ModelStreamEvent, ModelStopReason, ModelRef, PluginSessionStateEntry, PluginStateService, SessionStatus, SessionPluginEvent, SessionSnapshot, SessionStreamEvent, Tool, ToolCall, ToolDefinition, ToolExecutionContext, ToolExecutionPluginEvent, ToolInputSchema, Usage, } from './contracts';
1
+ export type { CallToolResult, CompactionBudgetInfo, CompactionCheckpoint, CompactionEstimator, CompactionEstimatorInput, CompactionOptions, CompactionService, CompactionState, CompactionTriggerReason, CompactionUpdate, ContentBlock, Message, MessageContentInput, MessageQueueConfig, MessageQueueSnapshot, MessageQueueStatus, ModelAdapter, ModelClient, ModelRequest, ModelStreamEvent, ModelStopReason, ModelRef, PluginSessionStateEntry, PendingMessage, PendingMessageKind, PluginStateService, SessionStatus, SessionPluginEvent, SessionSnapshot, SessionStreamEvent, Tool, ToolCall, ToolDefinition, ToolExecutionContext, ToolExecutionPluginEvent, ToolInputSchema, Usage, } from './contracts';
2
2
  export { normalizeContent } from './contracts/content-normalize';
3
3
  export { Agent, createAgent } from './agent-core/agent';
4
4
  export type { CreateAgentOptions } from './agent-core/agent';
@@ -1,4 +1,4 @@
1
- import type { CompactionState, Message, ModelRef, PluginSessionStateEntry, SessionSnapshot, Usage } from '../contracts';
1
+ import type { CompactionState, Message, MessageQueueSnapshot, ModelRef, PluginSessionStateEntry, SessionSnapshot, Usage } from '../contracts';
2
2
  export interface SnapshotCodecInput {
3
3
  sessionId: string;
4
4
  model?: ModelRef;
@@ -10,6 +10,7 @@ export interface SnapshotCodecInput {
10
10
  createdAt: number;
11
11
  updatedAt: number;
12
12
  metadata?: Record<string, unknown>;
13
+ messageQueue?: MessageQueueSnapshot;
13
14
  }
14
15
  export interface SnapshotCodec {
15
16
  encode(input: SnapshotCodecInput): SessionSnapshot;
@@ -279,8 +279,24 @@ export interface Tool {
279
279
  readonly definition: ToolDefinition;
280
280
  execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<CallToolResult>;
281
281
  }
282
+ export interface MessageQueueConfig {
283
+ autoProcessQueue: boolean;
284
+ maxQueueSize?: number;
285
+ }
286
+ export type PendingMessageKind = 'user' | 'steer';
287
+ export interface PendingMessage {
288
+ id: string;
289
+ input: MessageContentInput;
290
+ kind: PendingMessageKind;
291
+ priority: number;
292
+ enqueuedAt: number;
293
+ }
294
+ export interface MessageQueueSnapshot {
295
+ items: PendingMessage[];
296
+ config: MessageQueueConfig;
297
+ }
282
298
  export interface SessionSnapshot {
283
- schemaVersion: 1;
299
+ schemaVersion: 1 | 2;
284
300
  sessionId: string;
285
301
  model?: ModelRef;
286
302
  cwd?: string;
@@ -291,6 +307,7 @@ export interface SessionSnapshot {
291
307
  createdAt: number;
292
308
  updatedAt: number;
293
309
  metadata?: Record<string, unknown>;
310
+ messageQueue?: MessageQueueSnapshot;
294
311
  }
295
312
  export interface SessionStatus {
296
313
  sessionId: string;
package/dist/src/index.js CHANGED
@@ -3257,7 +3257,7 @@ class DefaultNotificationBus {
3257
3257
  class JsonSnapshotCodec {
3258
3258
  encode(input) {
3259
3259
  return structuredClone({
3260
- schemaVersion: 1,
3260
+ schemaVersion: 2,
3261
3261
  sessionId: input.sessionId,
3262
3262
  model: input.model,
3263
3263
  cwd: input.cwd,
@@ -3267,14 +3267,15 @@ class JsonSnapshotCodec {
3267
3267
  pluginState: input.pluginState,
3268
3268
  createdAt: input.createdAt,
3269
3269
  updatedAt: input.updatedAt,
3270
- metadata: input.metadata
3270
+ metadata: input.metadata,
3271
+ messageQueue: input.messageQueue
3271
3272
  });
3272
3273
  }
3273
3274
  decode(value) {
3274
3275
  if (!value || typeof value !== "object")
3275
3276
  throw new TypeError("Session snapshot must be an object");
3276
3277
  const snapshot = value;
3277
- if (snapshot.schemaVersion !== 1)
3278
+ if (snapshot.schemaVersion !== 1 && snapshot.schemaVersion !== 2)
3278
3279
  throw new TypeError("Unsupported session snapshot schemaVersion");
3279
3280
  if (typeof snapshot.sessionId !== "string")
3280
3281
  throw new TypeError("Session snapshot sessionId is required");
@@ -3282,6 +3283,12 @@ class JsonSnapshotCodec {
3282
3283
  throw new TypeError("Session snapshot messages must be an array");
3283
3284
  if (typeof snapshot.createdAt !== "number" || typeof snapshot.updatedAt !== "number")
3284
3285
  throw new TypeError("Session snapshot timestamps are required");
3286
+ if (snapshot.schemaVersion === 1) {
3287
+ return structuredClone({
3288
+ ...snapshot,
3289
+ schemaVersion: 1
3290
+ });
3291
+ }
3285
3292
  return structuredClone(snapshot);
3286
3293
  }
3287
3294
  }
@@ -3305,6 +3312,25 @@ function addUsage(left, right) {
3305
3312
  };
3306
3313
  }
3307
3314
 
3315
+ // src/agent-core/errors.ts
3316
+ class SessionExecutionError extends Error {
3317
+ payload;
3318
+ payloadSummary;
3319
+ constructor(payload) {
3320
+ super(payload.message);
3321
+ this.name = "SessionExecutionError";
3322
+ this.payload = payload;
3323
+ this.payloadSummary = stringifyPayload(payload);
3324
+ }
3325
+ }
3326
+ function stringifyPayload(payload) {
3327
+ try {
3328
+ return JSON.stringify(payload, null, 2);
3329
+ } catch {
3330
+ return String(payload);
3331
+ }
3332
+ }
3333
+
3308
3334
  // src/agent-core/tool-call.ts
3309
3335
  function startToolCall(callId, toolName) {
3310
3336
  if (!callId.trim())
@@ -3337,25 +3363,6 @@ function finalizeToolCall(draft) {
3337
3363
  };
3338
3364
  }
3339
3365
 
3340
- // src/agent-core/errors.ts
3341
- class SessionExecutionError extends Error {
3342
- payload;
3343
- payloadSummary;
3344
- constructor(payload) {
3345
- super(payload.message);
3346
- this.name = "SessionExecutionError";
3347
- this.payload = payload;
3348
- this.payloadSummary = stringifyPayload(payload);
3349
- }
3350
- }
3351
- function stringifyPayload(payload) {
3352
- try {
3353
- return JSON.stringify(payload, null, 2);
3354
- } catch {
3355
- return String(payload);
3356
- }
3357
- }
3358
-
3359
3366
  // src/agent-core/model-turn-collector.ts
3360
3367
  class DefaultModelTurnCollector {
3361
3368
  async* collect(options) {
@@ -3536,6 +3543,7 @@ class LoopRunner {
3536
3543
  cwd: state.cwd
3537
3544
  }) ?? [];
3538
3545
  const promptSegments = [
3546
+ ...options.persistentPromptSegments ?? [],
3539
3547
  ...pendingContextSegments,
3540
3548
  ...this.contextManager.format(effectiveContext),
3541
3549
  ...pluginPromptSegments
@@ -3977,6 +3985,10 @@ class DefaultToolExecutor {
3977
3985
  }
3978
3986
 
3979
3987
  // src/agent-core/session.ts
3988
+ var DEFAULT_MESSAGE_QUEUE_CONFIG = {
3989
+ autoProcessQueue: true
3990
+ };
3991
+
3980
3992
  class Session {
3981
3993
  id;
3982
3994
  createdAt;
@@ -3990,7 +4002,14 @@ class Session {
3990
4002
  compactionService;
3991
4003
  pluginStateService;
3992
4004
  pluginControllers;
4005
+ bufferedEvents = [];
4006
+ stateChangeWaiters = [];
3993
4007
  activeRunCount = 0;
4008
+ processingPromise = null;
4009
+ receiveActive = false;
4010
+ activeReceiveSignal;
4011
+ haltProcessingUntilReceive = false;
4012
+ saveChain = Promise.resolve();
3994
4013
  constructor(options) {
3995
4014
  const now = Date.now();
3996
4015
  this.id = options.id ?? createId("session");
@@ -4016,10 +4035,16 @@ class Session {
4016
4035
  usage: options.usage ? { ...options.usage } : createEmptyUsage(),
4017
4036
  compaction: cloneCompactionState(options.compaction ?? createEmptyCompactionState()),
4018
4037
  pluginState: clonePluginStateMap(options.pluginState),
4038
+ messageQueue: cloneMessageQueueSnapshot(options.messageQueue ?? {
4039
+ items: [],
4040
+ config: createMessageQueueConfig(options.messageQueueConfig)
4041
+ }),
4019
4042
  compactionOptions: options.compactionOptions,
4020
4043
  createdAt: this.createdAt,
4021
4044
  updatedAt: options.updatedAt ?? now
4022
4045
  };
4046
+ if (options.messageQueueConfig)
4047
+ this.state.messageQueue.config = createMessageQueueConfig(options.messageQueueConfig, this.state.messageQueue.config);
4023
4048
  this.compactionService.registerSession(this.state);
4024
4049
  this.pluginStateService.registerSession(this.state);
4025
4050
  this.pluginControllers = this.pluginHost?.createSessionControllers({
@@ -4061,6 +4086,91 @@ class Session {
4061
4086
  this.state.cwd = cwd;
4062
4087
  touchRuntimeSessionState(this.state);
4063
4088
  }
4089
+ send(input, options = {}) {
4090
+ return this.enqueueItem("user", input, options);
4091
+ }
4092
+ sendBatch(items) {
4093
+ const maxQueueSize = this.state.messageQueue.config.maxQueueSize;
4094
+ if (maxQueueSize !== undefined && this.state.messageQueue.items.length + items.length > maxQueueSize)
4095
+ throw new SessionExecutionError(createQueueError("DIM_QUEUE_FULL", `Session queue is full (max ${maxQueueSize})`));
4096
+ const prepared = items.map(({ input, options }) => prepareQueueItem("user", input, options));
4097
+ for (const item of prepared)
4098
+ this.state.messageQueue.items.push(item);
4099
+ if (prepared.length > 0)
4100
+ this.onQueueMutated();
4101
+ return prepared.map((item) => item.id);
4102
+ }
4103
+ steer(input, options = {}) {
4104
+ return this.enqueueItem("steer", input, options);
4105
+ }
4106
+ cancelQueuedItem(itemId) {
4107
+ const index = this.state.messageQueue.items.findIndex((item) => item.id === itemId);
4108
+ if (index < 0)
4109
+ return false;
4110
+ this.state.messageQueue.items.splice(index, 1);
4111
+ this.onQueueMutated();
4112
+ return true;
4113
+ }
4114
+ getQueueStatus() {
4115
+ const items = [...this.state.messageQueue.items].sort(compareQueueItems).map((item) => ({
4116
+ id: item.id,
4117
+ kind: item.kind,
4118
+ priority: item.priority,
4119
+ enqueuedAt: item.enqueuedAt,
4120
+ preview: createQueuePreview(item.input)
4121
+ }));
4122
+ return {
4123
+ length: items.length,
4124
+ items,
4125
+ isProcessing: this.processingPromise !== null,
4126
+ config: cloneMessageQueueConfig(this.state.messageQueue.config)
4127
+ };
4128
+ }
4129
+ clearQueue(filter = {}) {
4130
+ const before = this.state.messageQueue.items.length;
4131
+ if (!filter.kind) {
4132
+ this.state.messageQueue.items = [];
4133
+ } else {
4134
+ this.state.messageQueue.items = this.state.messageQueue.items.filter((item) => item.kind !== filter.kind);
4135
+ }
4136
+ const cleared = before - this.state.messageQueue.items.length;
4137
+ if (cleared > 0)
4138
+ this.onQueueMutated();
4139
+ return cleared;
4140
+ }
4141
+ async* receive(options = {}) {
4142
+ if (this.receiveActive)
4143
+ throw new SessionExecutionError(createQueueError("DIM_QUEUE_BUSY", "Only one receive() call can be active for a session"));
4144
+ this.receiveActive = true;
4145
+ this.activeReceiveSignal = options.signal;
4146
+ this.haltProcessingUntilReceive = false;
4147
+ let deliveredEvent = null;
4148
+ try {
4149
+ while (true) {
4150
+ if (deliveredEvent) {
4151
+ deliveredEvent.ack();
4152
+ deliveredEvent = null;
4153
+ }
4154
+ this.ensureQueueProcessing("receive");
4155
+ const nextBufferedEvent = this.bufferedEvents.shift();
4156
+ if (nextBufferedEvent) {
4157
+ deliveredEvent = nextBufferedEvent;
4158
+ yield nextBufferedEvent.event;
4159
+ continue;
4160
+ }
4161
+ if (!this.processingPromise && !this.hasRunnableItems())
4162
+ return;
4163
+ await this.waitForStateChange(options.signal);
4164
+ }
4165
+ } finally {
4166
+ if (deliveredEvent)
4167
+ deliveredEvent.ack();
4168
+ this.receiveActive = false;
4169
+ if (this.activeReceiveSignal === options.signal)
4170
+ this.activeReceiveSignal = undefined;
4171
+ this.notifyStateChange();
4172
+ }
4173
+ }
4064
4174
  async compact(update) {
4065
4175
  const beforeState = this.getCompactionState();
4066
4176
  const estimatedInputTokens = await this.readEstimatedInputTokens();
@@ -4109,6 +4219,9 @@ class Session {
4109
4219
  });
4110
4220
  }
4111
4221
  async save() {
4222
+ return this.enqueueSave();
4223
+ }
4224
+ async writeSnapshot() {
4112
4225
  if (!this.stateStore)
4113
4226
  return;
4114
4227
  let snapshot = this.toSnapshot();
@@ -4143,37 +4256,181 @@ class Session {
4143
4256
  pluginState: this.listPluginStates(),
4144
4257
  createdAt: this.createdAt,
4145
4258
  updatedAt: this.updatedAt,
4146
- metadata: this.state.metadata ? { ...this.state.metadata } : undefined
4259
+ metadata: this.state.metadata ? { ...this.state.metadata } : undefined,
4260
+ messageQueue: cloneMessageQueueSnapshot(this.state.messageQueue)
4147
4261
  });
4148
4262
  }
4149
- async send(input, options) {
4150
- const stream = this.stream(input, options);
4151
- let doneMessage;
4263
+ enqueueItem(kind, input, options) {
4264
+ const maxQueueSize = this.state.messageQueue.config.maxQueueSize;
4265
+ if (maxQueueSize !== undefined && this.state.messageQueue.items.length >= maxQueueSize)
4266
+ throw new SessionExecutionError(createQueueError("DIM_QUEUE_FULL", `Session queue is full (max ${maxQueueSize})`));
4267
+ const item = prepareQueueItem(kind, input, options);
4268
+ this.state.messageQueue.items.push(item);
4269
+ this.onQueueMutated();
4270
+ return item.id;
4271
+ }
4272
+ onQueueMutated() {
4273
+ touchRuntimeSessionState(this.state);
4274
+ this.notifyStateChange();
4275
+ this.scheduleSave();
4276
+ this.ensureQueueProcessing("auto");
4277
+ }
4278
+ hasRunnableItems() {
4279
+ return this.state.messageQueue.items.some((item) => item.kind === "user");
4280
+ }
4281
+ ensureQueueProcessing(source) {
4282
+ if (this.processingPromise)
4283
+ return;
4284
+ if (!this.hasRunnableItems())
4285
+ return;
4286
+ if (this.haltProcessingUntilReceive && source !== "receive")
4287
+ return;
4288
+ const signal = source === "receive" ? this.activeReceiveSignal : undefined;
4289
+ this.processingPromise = this.processQueue(signal).finally(() => {
4290
+ this.processingPromise = null;
4291
+ this.notifyStateChange();
4292
+ if (!this.haltProcessingUntilReceive && this.state.messageQueue.config.autoProcessQueue)
4293
+ this.ensureQueueProcessing("auto");
4294
+ });
4295
+ this.notifyStateChange();
4296
+ }
4297
+ async processQueue(signal) {
4152
4298
  while (true) {
4153
- const next = await stream.next();
4154
- if (next.done) {
4155
- doneMessage = next.value;
4156
- break;
4299
+ const nextItem = this.dequeueNextRunnableItem();
4300
+ if (!nextItem)
4301
+ return;
4302
+ const completed = await this.processQueuedItem(nextItem, signal);
4303
+ await this.persistStateQuietly();
4304
+ if (!completed) {
4305
+ this.haltProcessingUntilReceive = true;
4306
+ return;
4157
4307
  }
4158
4308
  }
4159
- if (!doneMessage)
4160
- throw new Error("Session completed without a final assistant message");
4161
- return doneMessage;
4162
4309
  }
4163
- async* stream(input, options) {
4310
+ dequeueNextRunnableItem() {
4311
+ const rankedItems = [...this.state.messageQueue.items].sort(compareQueueItems);
4312
+ const userIndex = rankedItems.findIndex((item) => item.kind === "user");
4313
+ if (userIndex < 0)
4314
+ return null;
4315
+ const userItem = rankedItems[userIndex];
4316
+ const steerItems = rankedItems.slice(0, userIndex).filter((item) => item.kind === "steer");
4317
+ const consumedIds = new Set([userItem.id, ...steerItems.map((item) => item.id)]);
4318
+ this.state.messageQueue.items = this.state.messageQueue.items.filter((item) => !consumedIds.has(item.id));
4319
+ touchRuntimeSessionState(this.state);
4320
+ this.notifyStateChange();
4321
+ return {
4322
+ item: userItem,
4323
+ steerSegments: steerItems.map((item) => contentToText(normalizeContent(item.input)).trim()).filter((segment) => segment.length > 0)
4324
+ };
4325
+ }
4326
+ async processQueuedItem(input, signal) {
4164
4327
  const runner = this.createLoopRunner();
4328
+ let emittedError = false;
4165
4329
  this.activeRunCount += 1;
4166
4330
  try {
4167
- return yield* runner.run(this.state, input, {
4168
- signal: options?.signal,
4169
- onDone: async () => {
4170
- await this.save();
4171
- }
4331
+ const iterator = runner.run(this.state, input.item.input, {
4332
+ signal,
4333
+ persistentPromptSegments: input.steerSegments
4172
4334
  });
4335
+ while (true) {
4336
+ const next = await iterator.next();
4337
+ if (next.done)
4338
+ return true;
4339
+ const queuedEvent = this.attachQueueContext(next.value, input.item);
4340
+ if (queuedEvent.type === "error")
4341
+ emittedError = true;
4342
+ await this.pushBufferedEvent(queuedEvent);
4343
+ }
4344
+ } catch (error) {
4345
+ if (!emittedError)
4346
+ await this.pushBufferedEvent(this.createErrorEvent(input.item, error));
4347
+ return false;
4173
4348
  } finally {
4174
4349
  this.activeRunCount = Math.max(0, this.activeRunCount - 1);
4350
+ this.notifyStateChange();
4175
4351
  }
4176
4352
  }
4353
+ attachQueueContext(event, item) {
4354
+ return {
4355
+ ...event,
4356
+ itemId: item.id,
4357
+ itemKind: item.kind,
4358
+ priority: item.priority
4359
+ };
4360
+ }
4361
+ createErrorEvent(item, error) {
4362
+ const payload = error instanceof SessionExecutionError ? error.payload : {
4363
+ code: "runtime_error",
4364
+ message: error instanceof Error ? error.message : String(error)
4365
+ };
4366
+ return {
4367
+ type: "error",
4368
+ sessionId: this.id,
4369
+ error: payload,
4370
+ itemId: item.id,
4371
+ itemKind: item.kind,
4372
+ priority: item.priority
4373
+ };
4374
+ }
4375
+ async pushBufferedEvent(event) {
4376
+ let resolved = false;
4377
+ const ack = () => {
4378
+ if (resolved)
4379
+ return;
4380
+ resolved = true;
4381
+ this.notifyStateChange();
4382
+ };
4383
+ this.bufferedEvents.push({ event, ack });
4384
+ this.notifyStateChange();
4385
+ while (!resolved)
4386
+ await this.waitForStateChange();
4387
+ }
4388
+ scheduleSave() {
4389
+ if (!this.stateStore)
4390
+ return;
4391
+ this.enqueueSave().catch(() => {
4392
+ return;
4393
+ });
4394
+ }
4395
+ async persistStateQuietly() {
4396
+ try {
4397
+ await this.enqueueSave();
4398
+ } catch {}
4399
+ }
4400
+ enqueueSave() {
4401
+ if (!this.stateStore)
4402
+ return Promise.resolve();
4403
+ this.saveChain = this.saveChain.catch(() => {
4404
+ return;
4405
+ }).then(() => this.writeSnapshot());
4406
+ return this.saveChain;
4407
+ }
4408
+ waitForStateChange(signal) {
4409
+ if (signal?.aborted)
4410
+ return Promise.reject(new SessionExecutionError(createQueueError("session_receive_aborted", "Session receive aborted")));
4411
+ return new Promise((resolve, reject) => {
4412
+ const onAbort = () => {
4413
+ cleanup();
4414
+ reject(new SessionExecutionError(createQueueError("session_receive_aborted", "Session receive aborted")));
4415
+ };
4416
+ const resume = () => {
4417
+ cleanup();
4418
+ resolve();
4419
+ };
4420
+ const cleanup = () => {
4421
+ const index = this.stateChangeWaiters.indexOf(resume);
4422
+ if (index >= 0)
4423
+ this.stateChangeWaiters.splice(index, 1);
4424
+ signal?.removeEventListener("abort", onAbort);
4425
+ };
4426
+ this.stateChangeWaiters.push(resume);
4427
+ signal?.addEventListener("abort", onAbort, { once: true });
4428
+ });
4429
+ }
4430
+ notifyStateChange() {
4431
+ while (this.stateChangeWaiters.length > 0)
4432
+ this.stateChangeWaiters.shift()?.();
4433
+ }
4177
4434
  createLoopRunner() {
4178
4435
  return new LoopRunner({
4179
4436
  messageFactory: new DefaultMessageFactory,
@@ -4221,6 +4478,63 @@ class Session {
4221
4478
  };
4222
4479
  }
4223
4480
  }
4481
+ function createMessageQueueConfig(overrides, base = DEFAULT_MESSAGE_QUEUE_CONFIG) {
4482
+ return {
4483
+ autoProcessQueue: overrides?.autoProcessQueue ?? base.autoProcessQueue,
4484
+ maxQueueSize: overrides?.maxQueueSize ?? base.maxQueueSize
4485
+ };
4486
+ }
4487
+ function cloneMessageQueueConfig(config) {
4488
+ return {
4489
+ autoProcessQueue: config.autoProcessQueue,
4490
+ maxQueueSize: config.maxQueueSize
4491
+ };
4492
+ }
4493
+ function clonePendingMessage(item) {
4494
+ return {
4495
+ id: item.id,
4496
+ input: structuredClone(item.input),
4497
+ kind: item.kind,
4498
+ priority: item.priority,
4499
+ enqueuedAt: item.enqueuedAt
4500
+ };
4501
+ }
4502
+ function cloneMessageQueueSnapshot(snapshot) {
4503
+ return {
4504
+ items: snapshot.items.map(clonePendingMessage),
4505
+ config: cloneMessageQueueConfig(snapshot.config)
4506
+ };
4507
+ }
4508
+ function prepareQueueItem(kind, input, options = {}) {
4509
+ const normalizedInput = normalizeContent(input);
4510
+ if (kind === "steer" && contentToText(normalizedInput).trim().length === 0)
4511
+ throw new TypeError("Steer content must contain text");
4512
+ return {
4513
+ id: createId("item"),
4514
+ input: structuredClone(input),
4515
+ kind,
4516
+ priority: options.priority ?? 0,
4517
+ enqueuedAt: Date.now()
4518
+ };
4519
+ }
4520
+ function compareQueueItems(left, right) {
4521
+ if (left.priority !== right.priority)
4522
+ return right.priority - left.priority;
4523
+ if (left.enqueuedAt !== right.enqueuedAt)
4524
+ return left.enqueuedAt - right.enqueuedAt;
4525
+ return left.id.localeCompare(right.id);
4526
+ }
4527
+ function createQueuePreview(input) {
4528
+ const text = contentToText(normalizeContent(input)).trim();
4529
+ if (text.length === 0)
4530
+ return "[non-text content]";
4531
+ if (text.length <= 80)
4532
+ return text;
4533
+ return `${text.slice(0, 77)}...`;
4534
+ }
4535
+ function createQueueError(code, message) {
4536
+ return { code, message };
4537
+ }
4224
4538
 
4225
4539
  // src/agent-core/agent.ts
4226
4540
  class Agent {
@@ -4292,6 +4606,7 @@ class Agent {
4292
4606
  metadata: options.metadata ?? this.options.metadata,
4293
4607
  reasoning: this.options.reasoning,
4294
4608
  compactionOptions: this.options.compaction,
4609
+ messageQueueConfig: options.messageQueueConfig,
4295
4610
  messages,
4296
4611
  pluginHost: this.pluginHost,
4297
4612
  contextManager: this.contextManager,
@@ -4325,6 +4640,7 @@ class Agent {
4325
4640
  usage: snapshot.usage,
4326
4641
  compaction: snapshot.compaction,
4327
4642
  pluginState: snapshot.pluginState,
4643
+ messageQueue: snapshot.messageQueue,
4328
4644
  compactionOptions: this.options.compaction,
4329
4645
  createdAt: snapshot.createdAt,
4330
4646
  updatedAt: snapshot.updatedAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archships/dim-agent-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "An agent-first TypeScript SDK with provider adapters, sessions, hooks, plugins, and runtime gateways.",
5
5
  "type": "module",
6
6
  "exports": {