@archships/dim-agent-sdk 0.0.1 → 0.0.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.
Files changed (95) hide show
  1. package/README.md +115 -4
  2. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/Agent.d.ts +7 -1
  3. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/LoopRunner.d.ts +6 -0
  4. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/Session.d.ts +26 -1
  5. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/ToolExecutor.d.ts +11 -1
  6. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/agent-types.d.ts +5 -2
  7. package/dist/dim-agent-sdk/src/agent-core/approvals.d.ts +25 -0
  8. package/dist/dim-agent-sdk/src/agent-core/compaction.d.ts +22 -0
  9. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/errors.d.ts +1 -0
  10. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/index.d.ts +7 -0
  11. package/dist/dim-agent-sdk/src/agent-core/notifications.d.ts +25 -0
  12. package/dist/dim-agent-sdk/src/agent-core/plugin-state.d.ts +24 -0
  13. package/dist/{agent-core → dim-agent-sdk/src/agent-core}/session-state.d.ts +4 -1
  14. package/dist/dim-agent-sdk/src/agent-core/session-status.d.ts +4 -0
  15. package/dist/dim-agent-sdk/src/agent-core/subagent.d.ts +14 -0
  16. package/dist/dim-agent-sdk/src/context/compaction.d.ts +1 -0
  17. package/dist/dim-agent-sdk/src/context/index.d.ts +4 -0
  18. package/dist/dim-agent-sdk/src/contracts/compaction.d.ts +52 -0
  19. package/dist/{contracts → dim-agent-sdk/src/contracts}/event.d.ts +8 -1
  20. package/dist/{contracts → dim-agent-sdk/src/contracts}/index.d.ts +2 -0
  21. package/dist/dim-agent-sdk/src/contracts/plugin-state.d.ts +11 -0
  22. package/dist/dim-agent-sdk/src/contracts/state.d.ts +29 -0
  23. package/dist/{contracts → dim-agent-sdk/src/contracts}/tool.d.ts +6 -0
  24. package/dist/{index.d.ts → dim-agent-sdk/src/index.d.ts} +2 -1
  25. package/dist/{persistence → dim-agent-sdk/src/persistence}/SnapshotCodec.d.ts +3 -1
  26. package/dist/dim-agent-sdk/src/plugin-host/HookPipeline.d.ts +30 -0
  27. package/dist/dim-agent-sdk/src/plugin-host/PluginHost.d.ts +58 -0
  28. package/dist/dim-agent-sdk/src/plugin-host/helpers.d.ts +6 -0
  29. package/dist/dim-agent-sdk/src/plugin-host/index.d.ts +4 -0
  30. package/dist/dim-agent-sdk/src/plugin-host/types.d.ts +1 -0
  31. package/dist/{services → dim-agent-sdk/src/services}/PermissionGateway.d.ts +3 -0
  32. package/dist/{services → dim-agent-sdk/src/services}/types.d.ts +3 -1
  33. package/dist/dim-plugin-api/src/index.d.ts +602 -0
  34. package/dist/index.js +1194 -166
  35. package/package.json +12 -12
  36. package/dist/context/index.d.ts +0 -3
  37. package/dist/contracts/state.d.ts +0 -14
  38. package/dist/plugin-host/HookPipeline.d.ts +0 -7
  39. package/dist/plugin-host/PluginHost.d.ts +0 -36
  40. package/dist/plugin-host/helpers.d.ts +0 -3
  41. package/dist/plugin-host/index.d.ts +0 -4
  42. package/dist/plugin-host/types.d.ts +0 -1
  43. /package/dist/{agent-core → dim-agent-sdk/src/agent-core}/MessageFactory.d.ts +0 -0
  44. /package/dist/{agent-core → dim-agent-sdk/src/agent-core}/ModelTurnCollector.d.ts +0 -0
  45. /package/dist/{agent-core → dim-agent-sdk/src/agent-core}/TerminationPolicy.d.ts +0 -0
  46. /package/dist/{agent-core → dim-agent-sdk/src/agent-core}/createModel.d.ts +0 -0
  47. /package/dist/{agent-core → dim-agent-sdk/src/agent-core}/tool-call.d.ts +0 -0
  48. /package/dist/{context → dim-agent-sdk/src/context}/AutoContextManager.d.ts +0 -0
  49. /package/dist/{context → dim-agent-sdk/src/context}/types.d.ts +0 -0
  50. /package/dist/{contracts → dim-agent-sdk/src/contracts}/common.d.ts +0 -0
  51. /package/dist/{contracts → dim-agent-sdk/src/contracts}/content-normalize.d.ts +0 -0
  52. /package/dist/{contracts → dim-agent-sdk/src/contracts}/content.d.ts +0 -0
  53. /package/dist/{contracts → dim-agent-sdk/src/contracts}/message.d.ts +0 -0
  54. /package/dist/{contracts → dim-agent-sdk/src/contracts}/model.d.ts +0 -0
  55. /package/dist/{contracts → dim-agent-sdk/src/contracts}/tool-normalize.d.ts +0 -0
  56. /package/dist/{persistence → dim-agent-sdk/src/persistence}/FileStateStore.d.ts +0 -0
  57. /package/dist/{persistence → dim-agent-sdk/src/persistence}/InMemoryStateStore.d.ts +0 -0
  58. /package/dist/{persistence → dim-agent-sdk/src/persistence}/index.d.ts +0 -0
  59. /package/dist/{persistence → dim-agent-sdk/src/persistence}/store.d.ts +0 -0
  60. /package/dist/{providers → dim-agent-sdk/src/providers}/anthropic/adapter.d.ts +0 -0
  61. /package/dist/{providers → dim-agent-sdk/src/providers}/anthropic/mapper.d.ts +0 -0
  62. /package/dist/{providers → dim-agent-sdk/src/providers}/gemini/adapter.d.ts +0 -0
  63. /package/dist/{providers → dim-agent-sdk/src/providers}/gemini/mapper.d.ts +0 -0
  64. /package/dist/{providers → dim-agent-sdk/src/providers}/index.d.ts +0 -0
  65. /package/dist/{providers → dim-agent-sdk/src/providers}/openai/adapter.d.ts +0 -0
  66. /package/dist/{providers → dim-agent-sdk/src/providers}/openai/mapper.d.ts +0 -0
  67. /package/dist/{providers → dim-agent-sdk/src/providers}/openai-responses/adapter.d.ts +0 -0
  68. /package/dist/{providers → dim-agent-sdk/src/providers}/openai-responses/mapper.d.ts +0 -0
  69. /package/dist/{providers → dim-agent-sdk/src/providers}/shared/http-error.d.ts +0 -0
  70. /package/dist/{providers → dim-agent-sdk/src/providers}/shared/provider-state.d.ts +0 -0
  71. /package/dist/{providers → dim-agent-sdk/src/providers}/shared/reasoning.d.ts +0 -0
  72. /package/dist/{providers → dim-agent-sdk/src/providers}/shared/tool-call.d.ts +0 -0
  73. /package/dist/{providers → dim-agent-sdk/src/providers}/shared/usage.d.ts +0 -0
  74. /package/dist/{services → dim-agent-sdk/src/services}/ExecGateway.d.ts +0 -0
  75. /package/dist/{services → dim-agent-sdk/src/services}/FileSystemGateway.d.ts +0 -0
  76. /package/dist/{services → dim-agent-sdk/src/services}/GitGateway.d.ts +0 -0
  77. /package/dist/{services → dim-agent-sdk/src/services}/ModelGateway.d.ts +0 -0
  78. /package/dist/{services → dim-agent-sdk/src/services}/NetworkGateway.d.ts +0 -0
  79. /package/dist/{services → dim-agent-sdk/src/services}/activity.d.ts +0 -0
  80. /package/dist/{services → dim-agent-sdk/src/services}/index.d.ts +0 -0
  81. /package/dist/{services → dim-agent-sdk/src/services}/permissions.d.ts +0 -0
  82. /package/dist/{tools → dim-agent-sdk/src/tools}/BaseTool.d.ts +0 -0
  83. /package/dist/{tools → dim-agent-sdk/src/tools}/ToolRegistry.d.ts +0 -0
  84. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/EditTool.d.ts +0 -0
  85. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/ExecTool.d.ts +0 -0
  86. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/ReadTool.d.ts +0 -0
  87. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/WriteTool.d.ts +0 -0
  88. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/index.d.ts +0 -0
  89. /package/dist/{tools → dim-agent-sdk/src/tools}/builtins/utils.d.ts +0 -0
  90. /package/dist/{tools → dim-agent-sdk/src/tools}/index.d.ts +0 -0
  91. /package/dist/{tools → dim-agent-sdk/src/tools}/result.d.ts +0 -0
  92. /package/dist/{utils → dim-agent-sdk/src/utils}/guards.d.ts +0 -0
  93. /package/dist/{utils → dim-agent-sdk/src/utils}/id.d.ts +0 -0
  94. /package/dist/{utils → dim-agent-sdk/src/utils}/json.d.ts +0 -0
  95. /package/dist/{utils → dim-agent-sdk/src/utils}/usage.d.ts +0 -0
package/dist/index.js CHANGED
@@ -208,27 +208,60 @@ function isHookControlResult(value) {
208
208
  const candidate = value;
209
209
  return (candidate.type === "continue" || candidate.type === "replace" || candidate.type === "stop") && "payload" in candidate;
210
210
  }
211
+ function isToolBeforeExecuteControlResult(value) {
212
+ if (!value || typeof value !== "object")
213
+ return false;
214
+ const candidate = value;
215
+ return candidate.type === "allow" || candidate.type === "deny" || candidate.type === "ask" || candidate.type === "replaceToolCall" || candidate.type === "provideResult";
216
+ }
217
+ function isNotificationControlResult(value) {
218
+ if (!value || typeof value !== "object")
219
+ return false;
220
+ const candidate = value;
221
+ return candidate.type === "continue" || candidate.type === "replace" || candidate.type === "suppress";
222
+ }
223
+ function isRunStopControlResult(value) {
224
+ if (!value || typeof value !== "object")
225
+ return false;
226
+ const candidate = value;
227
+ return candidate.type === "continue" || candidate.type === "finalize";
228
+ }
211
229
 
212
230
  // src/plugin-host/HookPipeline.ts
213
231
  class HookPipeline {
214
232
  registrations = [];
215
- register(pluginId, priority, hooks) {
233
+ nextOrder = 0;
234
+ register(pluginId, pluginPriority, hooks, services) {
216
235
  if (!hooks)
217
236
  return;
218
- if (hooks["model.event"]?.middleware && hooks["model.event"]?.middleware.length > 0)
219
- throw new Error(`Plugin ${pluginId} cannot register middleware for model.event`);
220
- this.registrations.push({ pluginId, priority, hooks });
221
- this.registrations.sort((left, right) => right.priority - left.priority);
237
+ for (const entry of hooks) {
238
+ if (!entry.middleware?.length && !entry.observers?.length)
239
+ continue;
240
+ if (entry.descriptor.name === "model.event" && entry.middleware && entry.middleware.length > 0)
241
+ throw new Error(`Plugin ${pluginId} cannot register middleware for model.event`);
242
+ if (entry.descriptor.mode === "async" && entry.middleware && entry.middleware.length > 0)
243
+ throw new Error(`Plugin ${pluginId} cannot register async middleware for ${entry.descriptor.name}`);
244
+ this.registrations.push({
245
+ pluginId,
246
+ order: this.nextOrder++,
247
+ priority: entry.descriptor.priority ?? pluginPriority,
248
+ descriptor: entry.descriptor,
249
+ services,
250
+ middleware: [...entry.middleware ?? []],
251
+ observers: [...entry.observers ?? []]
252
+ });
253
+ }
254
+ this.registrations.sort((left, right) => {
255
+ if (left.priority !== right.priority)
256
+ return right.priority - left.priority;
257
+ return left.order - right.order;
258
+ });
222
259
  }
223
260
  async runMiddleware(name, payload, context) {
224
261
  let current = payload;
225
- for (const registration of this.registrations) {
226
- const middlewares = registration.hooks[name]?.middleware ?? [];
227
- for (const middleware of middlewares) {
228
- const result = await middleware({
229
- payload: current,
230
- context: { ...context, metadata: { ...context.metadata, pluginId: registration.pluginId } }
231
- });
262
+ for (const registration of this.matchingRegistrations(name, current)) {
263
+ for (const middleware of registration.middleware) {
264
+ const result = await this.runMiddlewareHandler(registration, middleware, current, context);
232
265
  if (result === undefined)
233
266
  continue;
234
267
  if (isHookControlResult(result)) {
@@ -237,22 +270,191 @@ class HookPipeline {
237
270
  return current;
238
271
  continue;
239
272
  }
273
+ if (isNotificationControlResult(result) || isRunStopControlResult(result) || isToolBeforeExecuteControlResult(result))
274
+ throw new Error(`Hook ${registration.descriptor.name} returned an unsupported control result`);
240
275
  current = result;
241
276
  }
242
277
  }
243
278
  return current;
244
279
  }
280
+ async runToolBeforeExecute(payload, context) {
281
+ let current = payload;
282
+ for (const registration of this.matchingRegistrations("tool.beforeExecute", current)) {
283
+ for (const middleware of registration.middleware) {
284
+ const result = await this.runMiddlewareHandler(registration, middleware, current, context);
285
+ if (result === undefined)
286
+ continue;
287
+ if (isHookControlResult(result)) {
288
+ current = result.payload;
289
+ if (result.type === "stop")
290
+ return { payload: current };
291
+ continue;
292
+ }
293
+ if (isToolBeforeExecuteControlResult(result)) {
294
+ if (result.type === "replaceToolCall") {
295
+ current = { ...current, toolCall: result.toolCall };
296
+ continue;
297
+ }
298
+ if (result.type === "ask" && result.toolCall)
299
+ current = { ...current, toolCall: result.toolCall };
300
+ return { payload: current, control: result };
301
+ }
302
+ current = result;
303
+ }
304
+ }
305
+ return { payload: current };
306
+ }
307
+ async runNotifyMessage(payload, context) {
308
+ let current = payload;
309
+ for (const registration of this.matchingRegistrations("notify.message", current)) {
310
+ for (const middleware of registration.middleware) {
311
+ const result = await this.runMiddlewareHandler(registration, middleware, current, context);
312
+ if (result === undefined)
313
+ continue;
314
+ if (isHookControlResult(result)) {
315
+ current = result.payload;
316
+ if (result.type === "stop")
317
+ return { payload: current };
318
+ continue;
319
+ }
320
+ if (isNotificationControlResult(result)) {
321
+ if (result.type === "replace") {
322
+ current = { ...current, notification: result.notification };
323
+ continue;
324
+ }
325
+ if (result.type === "suppress")
326
+ return { payload: current, suppressed: true };
327
+ continue;
328
+ }
329
+ current = result;
330
+ }
331
+ }
332
+ return { payload: current };
333
+ }
334
+ async runRunStop(payload, context) {
335
+ let current = payload;
336
+ for (const registration of this.matchingRegistrations("run.stop", current)) {
337
+ for (const middleware of registration.middleware) {
338
+ const result = await this.runMiddlewareHandler(registration, middleware, current, context);
339
+ if (result === undefined)
340
+ continue;
341
+ if (isHookControlResult(result)) {
342
+ current = result.payload;
343
+ if (result.type === "stop")
344
+ return { payload: current };
345
+ continue;
346
+ }
347
+ if (isRunStopControlResult(result))
348
+ return { payload: current, control: result };
349
+ current = result;
350
+ }
351
+ }
352
+ return { payload: current };
353
+ }
245
354
  async runObservers(name, payload, context) {
246
- for (const registration of this.registrations) {
247
- const observers = registration.hooks[name]?.observers ?? [];
248
- for (const observer of observers) {
249
- await observer({
250
- payload,
251
- context: { ...context, metadata: { ...context.metadata, pluginId: registration.pluginId } }
252
- });
355
+ for (const registration of this.matchingRegistrations(name, payload)) {
356
+ for (const observer of registration.observers) {
357
+ const task = this.runObserverHandler(registration, observer, payload, context);
358
+ if (registration.descriptor.mode === "async") {
359
+ task.catch(() => {});
360
+ continue;
361
+ }
362
+ await task.catch(() => {});
253
363
  }
254
364
  }
255
365
  }
366
+ matchingRegistrations(name, payload) {
367
+ return this.registrations.filter((registration) => {
368
+ return registration.descriptor.name === name && matchesHookDescriptor(registration.descriptor, payload);
369
+ });
370
+ }
371
+ async runMiddlewareHandler(registration, middleware, payload, context) {
372
+ const task = Promise.resolve(middleware({
373
+ payload,
374
+ context: this.withHandlerMetadata(context, registration)
375
+ }));
376
+ return await withTimeout(task, registration.descriptor.timeoutMs, () => {
377
+ return new Error(`Hook ${registration.descriptor.name} for plugin ${registration.pluginId} timed out after ${registration.descriptor.timeoutMs}ms`);
378
+ });
379
+ }
380
+ async runObserverHandler(registration, observer, payload, context) {
381
+ const task = Promise.resolve(observer({
382
+ payload,
383
+ context: this.withHandlerMetadata(context, registration)
384
+ }));
385
+ await withTimeout(task, registration.descriptor.timeoutMs, () => {
386
+ return new Error(`Hook ${registration.descriptor.name} for plugin ${registration.pluginId} timed out after ${registration.descriptor.timeoutMs}ms`);
387
+ });
388
+ }
389
+ withHandlerMetadata(context, registration) {
390
+ return {
391
+ ...context,
392
+ services: registration.services ?? context.services,
393
+ status: context.status ? structuredClone(context.status) : undefined,
394
+ metadata: {
395
+ ...context.metadata,
396
+ pluginId: registration.pluginId,
397
+ hookName: registration.descriptor.name
398
+ }
399
+ };
400
+ }
401
+ }
402
+ function matchesHookDescriptor(descriptor, payload) {
403
+ const matcher = descriptor.when;
404
+ if (!matcher)
405
+ return true;
406
+ if (matcher.toolName && !matchesValue(matcher.toolName, readToolName(payload)))
407
+ return false;
408
+ if (matcher.notificationType && !matchesValue(matcher.notificationType, readNotificationType(payload)))
409
+ return false;
410
+ if (matcher.stopReason && !matchesValue(matcher.stopReason, readStopReason(payload)))
411
+ return false;
412
+ if (matcher.source && !matchesValue(matcher.source, readSource(payload)))
413
+ return false;
414
+ return true;
415
+ }
416
+ function readToolName(payload) {
417
+ if ("toolCall" in payload)
418
+ return payload.toolCall.function.name;
419
+ return;
420
+ }
421
+ function readNotificationType(payload) {
422
+ if ("notification" in payload)
423
+ return payload.notification.type;
424
+ return;
425
+ }
426
+ function readStopReason(payload) {
427
+ if ("stopReason" in payload)
428
+ return payload.stopReason;
429
+ return;
430
+ }
431
+ function readSource(payload) {
432
+ if ("notification" in payload)
433
+ return payload.notification.source;
434
+ return;
435
+ }
436
+ function matchesValue(expected, actual) {
437
+ if (!actual)
438
+ return false;
439
+ if (Array.isArray(expected))
440
+ return expected.includes(actual);
441
+ return expected === actual;
442
+ }
443
+ async function withTimeout(promise, timeoutMs, createError) {
444
+ if (!timeoutMs || timeoutMs <= 0)
445
+ return await promise;
446
+ let timeoutHandle;
447
+ try {
448
+ return await Promise.race([
449
+ promise,
450
+ new Promise((_, reject) => {
451
+ timeoutHandle = setTimeout(() => reject(createError()), timeoutMs);
452
+ })
453
+ ]);
454
+ } finally {
455
+ if (timeoutHandle)
456
+ clearTimeout(timeoutHandle);
457
+ }
256
458
  }
257
459
 
258
460
  // src/plugin-host/PluginHost.ts
@@ -261,6 +463,7 @@ class PluginHost {
261
463
  contextContributors = [];
262
464
  promptContributors = [];
263
465
  tools = [];
466
+ sessionControllerFactories = [];
264
467
  services;
265
468
  cwd;
266
469
  agentId;
@@ -278,7 +481,7 @@ class PluginHost {
278
481
  manifest: plugin.manifest,
279
482
  services: scopedServices
280
483
  });
281
- this.pipeline.register(plugin.manifest.id, priority, setup?.hooks);
484
+ this.pipeline.register(plugin.manifest.id, priority, setup?.hooks, scopedServices);
282
485
  for (const tool of setup?.tools ?? []) {
283
486
  this.tools.push({
284
487
  pluginId: plugin.manifest.id,
@@ -289,6 +492,13 @@ class PluginHost {
289
492
  this.contextContributors.push({ pluginId: plugin.manifest.id, contributor });
290
493
  for (const contributor of setup?.promptContributors ?? [])
291
494
  this.promptContributors.push({ pluginId: plugin.manifest.id, contributor });
495
+ if (setup?.createSessionController) {
496
+ this.sessionControllerFactories.push({
497
+ pluginId: plugin.manifest.id,
498
+ factory: setup.createSessionController,
499
+ services: scopedServices
500
+ });
501
+ }
292
502
  }
293
503
  }
294
504
  pluginTools() {
@@ -297,9 +507,32 @@ class PluginHost {
297
507
  recentFiles(limit = 5) {
298
508
  return this.tracker?.recentFiles(limit) ?? [];
299
509
  }
510
+ createSessionControllers(input) {
511
+ const controllers = new Map;
512
+ for (const entry of this.sessionControllerFactories) {
513
+ controllers.set(entry.pluginId, entry.factory({
514
+ sessionId: input.sessionId,
515
+ metadata: input.metadata ? structuredClone(input.metadata) : undefined,
516
+ getStatus: input.getStatus,
517
+ pluginState: entry.services.pluginState,
518
+ save: input.save,
519
+ isRunning: input.isRunning
520
+ }));
521
+ }
522
+ return controllers;
523
+ }
300
524
  async runMiddleware(name, payload, context = {}) {
301
525
  return this.pipeline.runMiddleware(name, payload, this.createRuntimeContext(context));
302
526
  }
527
+ async runToolBeforeExecute(payload, context = {}) {
528
+ return this.pipeline.runToolBeforeExecute(payload, this.createRuntimeContext(context));
529
+ }
530
+ async runNotifyMessage(payload, context = {}) {
531
+ return this.pipeline.runNotifyMessage(payload, this.createRuntimeContext(context));
532
+ }
533
+ async runRunStop(payload, context = {}) {
534
+ return this.pipeline.runRunStop(payload, this.createRuntimeContext(context));
535
+ }
303
536
  async runObservers(name, payload, context = {}) {
304
537
  await this.pipeline.runObservers(name, payload, this.createRuntimeContext(context));
305
538
  }
@@ -332,6 +565,7 @@ class PluginHost {
332
565
  sessionId: context.sessionId,
333
566
  requestId: context.requestId,
334
567
  iteration: context.iteration,
568
+ status: context.status ? structuredClone(context.status) : undefined,
335
569
  metadata: context.metadata,
336
570
  services: context.services ?? this.services
337
571
  };
@@ -347,6 +581,10 @@ function wrapTool(pluginId, tool, services) {
347
581
  ...context.metadata,
348
582
  pluginId
349
583
  },
584
+ emitEvent: (event) => context.emitEvent?.({
585
+ ...event,
586
+ pluginId: event.pluginId ?? pluginId
587
+ }),
350
588
  services
351
589
  });
352
590
  }
@@ -678,14 +916,17 @@ function canWriteFs(permissions) {
678
916
  // src/services/PermissionGateway.ts
679
917
  class PermissionGateway {
680
918
  permissions;
919
+ compactionOwnerPluginId;
681
920
  constructor(options = {}) {
682
921
  this.permissions = normalizePermissions(options.permissions);
922
+ this.compactionOwnerPluginId = options.compactionOwnerPluginId;
683
923
  }
684
924
  get agentPermissions() {
685
925
  return { ...this.permissions };
686
926
  }
687
927
  scopeServices(services, pluginId, requested) {
688
928
  const effective = intersectPermissions(this.permissions, requested);
929
+ const pluginState = services.pluginState;
689
930
  return {
690
931
  fileSystem: {
691
932
  readText: (path3, options) => {
@@ -751,9 +992,33 @@ class PermissionGateway {
751
992
  throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to call the model gateway`);
752
993
  return services.model.stream(request);
753
994
  }
995
+ },
996
+ compaction: {
997
+ getState: (sessionId) => services.compaction.getState(sessionId),
998
+ apply: (update) => {
999
+ this.ensureCompactionOwner(pluginId);
1000
+ return services.compaction.apply(update);
1001
+ },
1002
+ clear: (sessionId) => {
1003
+ this.ensureCompactionOwner(pluginId);
1004
+ return services.compaction.clear(sessionId);
1005
+ }
1006
+ },
1007
+ pluginState: {
1008
+ get: (sessionId) => pluginState.getForPlugin(sessionId, pluginId),
1009
+ replace: (sessionId, entry) => pluginState.replaceForPlugin(sessionId, pluginId, entry),
1010
+ clear: (sessionId) => pluginState.clearForPlugin(sessionId, pluginId)
754
1011
  }
755
1012
  };
756
1013
  }
1014
+ ensureCompactionOwner(pluginId) {
1015
+ if (this.compactionOwnerPluginId === pluginId)
1016
+ return;
1017
+ const message = this.compactionOwnerPluginId ? `Plugin ${pluginId} is not the configured compaction owner (${this.compactionOwnerPluginId})` : `Plugin ${pluginId} cannot modify canonical compaction without compaction.ownerPluginId`;
1018
+ const error = new Error(message);
1019
+ error.code = "compaction_owner_required";
1020
+ throw error;
1021
+ }
757
1022
  }
758
1023
  function intersectPermissions(agent, requested) {
759
1024
  const normalized = requested ?? {};
@@ -1070,6 +1335,294 @@ class WriteTool extends BaseTool {
1070
1335
  });
1071
1336
  }
1072
1337
  }
1338
+ // src/agent-core/approvals.ts
1339
+ class DefaultApprovalManager {
1340
+ handler;
1341
+ notifications;
1342
+ constructor(options = {}) {
1343
+ this.handler = options.handler;
1344
+ this.notifications = options.notifications;
1345
+ }
1346
+ async requestApproval(request, context) {
1347
+ const normalizedRequest = {
1348
+ ...request,
1349
+ id: request.id ?? createId("approval")
1350
+ };
1351
+ await this.notifications?.emit({
1352
+ type: "approval.requested",
1353
+ source: "approval",
1354
+ level: "warning",
1355
+ message: normalizedRequest.message,
1356
+ metadata: {
1357
+ approvalId: normalizedRequest.id,
1358
+ toolName: normalizedRequest.toolCall.function.name
1359
+ }
1360
+ }, context);
1361
+ if (!this.handler) {
1362
+ const denied = {
1363
+ type: "denied",
1364
+ reason: "No approval handler configured"
1365
+ };
1366
+ await this.notifications?.emit({
1367
+ type: "approval.denied",
1368
+ source: "approval",
1369
+ level: "warning",
1370
+ message: denied.reason,
1371
+ metadata: {
1372
+ approvalId: normalizedRequest.id,
1373
+ toolName: normalizedRequest.toolCall.function.name
1374
+ }
1375
+ }, context);
1376
+ return denied;
1377
+ }
1378
+ const decision = await this.handler(normalizedRequest);
1379
+ await this.notifications?.emit({
1380
+ type: decision.type === "approved" ? "approval.approved" : "approval.denied",
1381
+ source: "approval",
1382
+ level: decision.type === "approved" ? "info" : "warning",
1383
+ message: decision.type === "approved" ? `Approved tool call: ${normalizedRequest.toolCall.function.name}` : decision.reason ?? `Denied tool call: ${normalizedRequest.toolCall.function.name}`,
1384
+ metadata: {
1385
+ approvalId: normalizedRequest.id,
1386
+ toolName: normalizedRequest.toolCall.function.name
1387
+ }
1388
+ }, context);
1389
+ return decision;
1390
+ }
1391
+ }
1392
+
1393
+ // src/agent-core/session-state.ts
1394
+ function touchRuntimeSessionState(state) {
1395
+ state.updatedAt = Date.now();
1396
+ }
1397
+
1398
+ // src/agent-core/compaction.ts
1399
+ function createEmptyCompactionState() {
1400
+ return {
1401
+ cursor: 0,
1402
+ systemSegments: [],
1403
+ checkpoints: []
1404
+ };
1405
+ }
1406
+ function cloneCompactionState(state) {
1407
+ if (!state)
1408
+ return createEmptyCompactionState();
1409
+ return structuredClone({
1410
+ cursor: state.cursor,
1411
+ systemSegments: [...state.systemSegments],
1412
+ checkpoints: state.checkpoints.map((checkpoint) => ({
1413
+ ...checkpoint,
1414
+ systemSegments: [...checkpoint.systemSegments],
1415
+ metadata: checkpoint.metadata ? structuredClone(checkpoint.metadata) : undefined
1416
+ }))
1417
+ });
1418
+ }
1419
+ function normalizeCompactionState(state, messages) {
1420
+ const current = cloneCompactionState(state);
1421
+ return {
1422
+ cursor: clampCursor(current.cursor, messages.length),
1423
+ systemSegments: current.systemSegments.filter(Boolean),
1424
+ checkpoints: current.checkpoints.map((checkpoint) => ({
1425
+ ...checkpoint,
1426
+ cursor: clampCursor(checkpoint.cursor, messages.length),
1427
+ systemSegments: checkpoint.systemSegments.filter(Boolean)
1428
+ }))
1429
+ };
1430
+ }
1431
+ function applyCompactionUpdate(state, update) {
1432
+ const nextCursor = clampCursor(update.cursor, state.messages.length);
1433
+ const nextSystemSegments = [...update.systemSegments ?? state.compaction.systemSegments].filter(Boolean);
1434
+ const nextCheckpoint = {
1435
+ id: createId("compact"),
1436
+ cursor: nextCursor,
1437
+ systemSegments: nextSystemSegments,
1438
+ summary: update.summary,
1439
+ reason: update.reason ?? "manual",
1440
+ createdAt: Date.now(),
1441
+ metadata: update.metadata ? structuredClone(update.metadata) : undefined
1442
+ };
1443
+ state.compaction = {
1444
+ cursor: nextCursor,
1445
+ systemSegments: nextSystemSegments,
1446
+ checkpoints: [...state.compaction.checkpoints, nextCheckpoint]
1447
+ };
1448
+ touchRuntimeSessionState(state);
1449
+ return cloneCompactionState(state.compaction);
1450
+ }
1451
+ function clearCompactionState(state) {
1452
+ state.compaction = createEmptyCompactionState();
1453
+ touchRuntimeSessionState(state);
1454
+ return cloneCompactionState(state.compaction);
1455
+ }
1456
+ function projectMessagesForRequest(messages, compaction) {
1457
+ const leadingSystemMessages = readLeadingSystemMessages(messages);
1458
+ const startIndex = Math.max(compaction.cursor, leadingSystemMessages.length);
1459
+ return [
1460
+ ...leadingSystemMessages,
1461
+ ...messages.slice(startIndex)
1462
+ ];
1463
+ }
1464
+ function buildCompactionPromptSegments(compaction) {
1465
+ return compaction.systemSegments.filter(Boolean);
1466
+ }
1467
+ function calculateThresholdTokens(options) {
1468
+ if (options.triggerTokens !== undefined)
1469
+ return options.triggerTokens;
1470
+ const ratio = options.triggerRatio ?? 0.8;
1471
+ return Math.max(1, Math.floor(options.maxInputTokens * ratio));
1472
+ }
1473
+ async function estimateCompactionBudget(options, input) {
1474
+ const estimatedInputTokens = options.estimator ? await options.estimator(input) : estimateByHeuristic(input);
1475
+ return {
1476
+ estimatedInputTokens,
1477
+ thresholdTokens: calculateThresholdTokens(options),
1478
+ maxInputTokens: options.maxInputTokens
1479
+ };
1480
+ }
1481
+ function isCompactionRequired(options, budget) {
1482
+ if (!options || options.enabled === false || !budget)
1483
+ return false;
1484
+ return budget.estimatedInputTokens >= budget.thresholdTokens;
1485
+ }
1486
+ function didCompactionStateChange(before, after) {
1487
+ return before.cursor !== after.cursor || JSON.stringify(before.systemSegments) !== JSON.stringify(after.systemSegments) || before.checkpoints.length !== after.checkpoints.length;
1488
+ }
1489
+
1490
+ class DefaultCompactionService {
1491
+ sessions = new Map;
1492
+ registerSession(state) {
1493
+ state.compaction = normalizeCompactionState(state.compaction, state.messages);
1494
+ this.sessions.set(state.id, state);
1495
+ }
1496
+ unregisterSession(sessionId) {
1497
+ this.sessions.delete(sessionId);
1498
+ }
1499
+ async getState(sessionId) {
1500
+ return cloneCompactionState(this.requireSession(sessionId).compaction);
1501
+ }
1502
+ async apply(update) {
1503
+ return applyCompactionUpdate(this.requireSession(update.sessionId), update);
1504
+ }
1505
+ async clear(sessionId) {
1506
+ return clearCompactionState(this.requireSession(sessionId));
1507
+ }
1508
+ requireSession(sessionId) {
1509
+ const state = this.sessions.get(sessionId);
1510
+ if (!state)
1511
+ throw new Error(`Unknown session for compaction: ${sessionId}`);
1512
+ return state;
1513
+ }
1514
+ }
1515
+ function clampCursor(cursor, max) {
1516
+ if (!Number.isFinite(cursor))
1517
+ return 0;
1518
+ return Math.max(0, Math.min(Math.floor(cursor), max));
1519
+ }
1520
+ function readLeadingSystemMessages(messages) {
1521
+ const systemMessages = [];
1522
+ for (const message of messages) {
1523
+ if (message.role !== "system")
1524
+ break;
1525
+ systemMessages.push(message);
1526
+ }
1527
+ return systemMessages;
1528
+ }
1529
+ function estimateByHeuristic(input) {
1530
+ const messageChars = input.messages.reduce((total, message) => {
1531
+ const contentChars = JSON.stringify(message.content).length;
1532
+ const metadataChars = message.metadata ? JSON.stringify(message.metadata).length : 0;
1533
+ return total + message.role.length + contentChars + metadataChars;
1534
+ }, 0);
1535
+ const toolChars = input.tools ? JSON.stringify(input.tools).length : 0;
1536
+ const modelChars = `${input.model.provider}:${input.model.modelId}`.length;
1537
+ return Math.max(1, Math.ceil((messageChars + toolChars + modelChars) / 4));
1538
+ }
1539
+
1540
+ // src/agent-core/plugin-state.ts
1541
+ var APP_PLUGIN_NAMESPACE = "__agent__";
1542
+ function clonePluginSessionStateEntry(entry) {
1543
+ if (!entry)
1544
+ return null;
1545
+ return structuredClone({
1546
+ version: entry.version,
1547
+ data: structuredClone(entry.data),
1548
+ updatedAt: entry.updatedAt
1549
+ });
1550
+ }
1551
+ function clonePluginStateMap(state) {
1552
+ if (!state)
1553
+ return {};
1554
+ return Object.fromEntries(Object.entries(state).map(([pluginId, entry]) => [pluginId, clonePluginSessionStateEntry(entry)]));
1555
+ }
1556
+ function normalizePluginStateMap(state) {
1557
+ if (!state)
1558
+ return {};
1559
+ return Object.fromEntries(Object.entries(state).map(([pluginId, entry]) => [pluginId, normalizePluginSessionStateEntry(entry, `Plugin session state for ${pluginId}`)]));
1560
+ }
1561
+
1562
+ class DefaultPluginStateService {
1563
+ sessions = new Map;
1564
+ registerSession(state) {
1565
+ state.pluginState = normalizePluginStateMap(state.pluginState);
1566
+ this.sessions.set(state.id, state);
1567
+ }
1568
+ unregisterSession(sessionId) {
1569
+ this.sessions.delete(sessionId);
1570
+ }
1571
+ async get(sessionId) {
1572
+ return this.getForPlugin(sessionId, APP_PLUGIN_NAMESPACE);
1573
+ }
1574
+ async replace(sessionId, entry) {
1575
+ return this.replaceForPlugin(sessionId, APP_PLUGIN_NAMESPACE, entry);
1576
+ }
1577
+ async clear(sessionId) {
1578
+ await this.clearForPlugin(sessionId, APP_PLUGIN_NAMESPACE);
1579
+ }
1580
+ async getForPlugin(sessionId, pluginId) {
1581
+ const state = this.requireSession(sessionId);
1582
+ return clonePluginSessionStateEntry(state.pluginState[pluginId]);
1583
+ }
1584
+ async replaceForPlugin(sessionId, pluginId, entry) {
1585
+ const state = this.requireSession(sessionId);
1586
+ const nextEntry = normalizePluginSessionStateEntry(entry, `Plugin session state for ${pluginId}`);
1587
+ state.pluginState = {
1588
+ ...state.pluginState,
1589
+ [pluginId]: nextEntry
1590
+ };
1591
+ touchRuntimeSessionState(state);
1592
+ return clonePluginSessionStateEntry(nextEntry);
1593
+ }
1594
+ async clearForPlugin(sessionId, pluginId) {
1595
+ const state = this.requireSession(sessionId);
1596
+ if (!(pluginId in state.pluginState))
1597
+ return;
1598
+ const nextState = { ...state.pluginState };
1599
+ delete nextState[pluginId];
1600
+ state.pluginState = nextState;
1601
+ touchRuntimeSessionState(state);
1602
+ }
1603
+ async list(sessionId) {
1604
+ return clonePluginStateMap(this.requireSession(sessionId).pluginState);
1605
+ }
1606
+ requireSession(sessionId) {
1607
+ const state = this.sessions.get(sessionId);
1608
+ if (!state)
1609
+ throw new Error(`Unknown session for plugin state: ${sessionId}`);
1610
+ return state;
1611
+ }
1612
+ }
1613
+ function normalizePluginSessionStateEntry(entry, label) {
1614
+ if (!Number.isFinite(entry.version))
1615
+ throw new TypeError(`${label} version must be a finite number`);
1616
+ if (!Number.isFinite(entry.updatedAt))
1617
+ throw new TypeError(`${label} updatedAt must be a finite number`);
1618
+ assertJsonSafeObject(entry.data, `${label} data`);
1619
+ return {
1620
+ version: Math.floor(entry.version),
1621
+ data: structuredClone(entry.data),
1622
+ updatedAt: entry.updatedAt
1623
+ };
1624
+ }
1625
+
1073
1626
  // src/agent-core/MessageFactory.ts
1074
1627
  class DefaultMessageFactory {
1075
1628
  createSystemMessage(input) {
@@ -1114,6 +1667,46 @@ class DefaultMessageFactory {
1114
1667
  }
1115
1668
  }
1116
1669
 
1670
+ // src/agent-core/notifications.ts
1671
+ class DefaultNotificationBus {
1672
+ pluginHost;
1673
+ services;
1674
+ constructor(options) {
1675
+ this.pluginHost = options.pluginHost;
1676
+ this.services = options.services;
1677
+ }
1678
+ async emit(notification, context = {}) {
1679
+ const normalized = {
1680
+ id: notification.id ?? createId("note"),
1681
+ level: notification.level ?? "info",
1682
+ ...notification
1683
+ };
1684
+ if (!this.pluginHost)
1685
+ return normalized;
1686
+ const result = await this.pluginHost.runNotifyMessage({ notification: normalized }, {
1687
+ sessionId: context.sessionId,
1688
+ requestId: context.requestId,
1689
+ iteration: context.iteration,
1690
+ cwd: context.cwd,
1691
+ status: context.status,
1692
+ metadata: context.metadata,
1693
+ services: this.services
1694
+ });
1695
+ if (result.suppressed)
1696
+ return;
1697
+ await this.pluginHost.runObservers("notify.message", result.payload, {
1698
+ sessionId: context.sessionId,
1699
+ requestId: context.requestId,
1700
+ iteration: context.iteration,
1701
+ cwd: context.cwd,
1702
+ status: context.status,
1703
+ metadata: context.metadata,
1704
+ services: this.services
1705
+ });
1706
+ return result.payload.notification;
1707
+ }
1708
+ }
1709
+
1117
1710
  // src/persistence/SnapshotCodec.ts
1118
1711
  class JsonSnapshotCodec {
1119
1712
  encode(input) {
@@ -1124,6 +1717,8 @@ class JsonSnapshotCodec {
1124
1717
  cwd: input.cwd,
1125
1718
  messages: input.messages,
1126
1719
  usage: input.usage,
1720
+ compaction: input.compaction,
1721
+ pluginState: input.pluginState,
1127
1722
  createdAt: input.createdAt,
1128
1723
  updatedAt: input.updatedAt,
1129
1724
  metadata: input.metadata
@@ -1199,10 +1794,19 @@ function finalizeToolCall(draft) {
1199
1794
  // src/agent-core/errors.ts
1200
1795
  class SessionExecutionError extends Error {
1201
1796
  payload;
1797
+ payloadSummary;
1202
1798
  constructor(payload) {
1203
1799
  super(payload.message);
1204
1800
  this.name = "SessionExecutionError";
1205
1801
  this.payload = payload;
1802
+ this.payloadSummary = stringifyPayload(payload);
1803
+ }
1804
+ }
1805
+ function stringifyPayload(payload) {
1806
+ try {
1807
+ return JSON.stringify(payload, null, 2);
1808
+ } catch {
1809
+ return String(payload);
1206
1810
  }
1207
1811
  }
1208
1812
 
@@ -1297,9 +1901,19 @@ class DefaultModelTurnCollector {
1297
1901
  }
1298
1902
  }
1299
1903
 
1300
- // src/agent-core/session-state.ts
1301
- function touchRuntimeSessionState(state) {
1302
- state.updatedAt = Date.now();
1904
+ // src/agent-core/session-status.ts
1905
+ function createSessionStatus(state) {
1906
+ return {
1907
+ sessionId: state.id,
1908
+ model: { ...state.modelRef },
1909
+ cwd: state.cwd,
1910
+ usage: { ...state.usage },
1911
+ compaction: cloneCompactionState(state.compaction),
1912
+ messageCount: state.messages.length,
1913
+ createdAt: state.createdAt,
1914
+ updatedAt: state.updatedAt,
1915
+ metadata: state.metadata ? structuredClone(state.metadata) : undefined
1916
+ };
1303
1917
  }
1304
1918
 
1305
1919
  // src/agent-core/LoopRunner.ts
@@ -1310,6 +1924,7 @@ class LoopRunner {
1310
1924
  terminationPolicy;
1311
1925
  contextManager;
1312
1926
  pluginHost;
1927
+ notificationBus;
1313
1928
  constructor(dependencies) {
1314
1929
  this.messageFactory = dependencies.messageFactory;
1315
1930
  this.turnCollector = dependencies.turnCollector;
@@ -1317,36 +1932,44 @@ class LoopRunner {
1317
1932
  this.terminationPolicy = dependencies.terminationPolicy;
1318
1933
  this.contextManager = dependencies.contextManager;
1319
1934
  this.pluginHost = dependencies.pluginHost;
1935
+ this.notificationBus = dependencies.notificationBus;
1320
1936
  }
1321
1937
  async* run(state, input, options = {}) {
1322
- const startPayload = await this.pluginHost?.runMiddleware("session.start", {
1938
+ const startPayload = await this.pluginHost?.runMiddleware("run.start", {
1323
1939
  sessionId: state.id,
1324
1940
  input
1325
- }, {
1326
- sessionId: state.id,
1327
- cwd: state.cwd,
1328
- metadata: state.metadata
1329
- });
1941
+ }, this.createHookContext(state));
1330
1942
  const initialInput = startPayload?.input ?? input;
1943
+ await this.pluginHost?.runObservers("run.start", {
1944
+ sessionId: state.id,
1945
+ input: initialInput
1946
+ }, this.createHookContext(state));
1331
1947
  const userMessage = this.messageFactory.createUserMessage(initialInput);
1332
1948
  state.messages.push(userMessage);
1333
1949
  touchRuntimeSessionState(state);
1950
+ await this.notificationBus.emit({
1951
+ type: "run.start",
1952
+ source: "runtime",
1953
+ message: `Run started for session ${state.id}`,
1954
+ metadata: {
1955
+ sessionId: state.id
1956
+ }
1957
+ }, {
1958
+ ...this.createHookContext(state)
1959
+ });
1334
1960
  let lastRequestId;
1961
+ let lastIteration = 0;
1962
+ let pendingContextSegments = [];
1335
1963
  try {
1336
1964
  for (let iteration = 0;iteration < state.maxIterations; iteration += 1) {
1965
+ lastIteration = iteration;
1337
1966
  const requestId = createId("req");
1338
1967
  lastRequestId = requestId;
1339
1968
  const turnStart = await this.pluginHost?.runMiddleware("turn.start", {
1340
1969
  requestId,
1341
1970
  iteration,
1342
1971
  messages: [...state.messages]
1343
- }, {
1344
- sessionId: state.id,
1345
- requestId,
1346
- iteration,
1347
- cwd: state.cwd,
1348
- metadata: state.metadata
1349
- });
1972
+ }, this.createHookContext(state, { requestId, iteration }));
1350
1973
  const contextItems = await this.contextManager.resolve({
1351
1974
  sessionId: state.id,
1352
1975
  input: initialInput,
@@ -1357,13 +1980,7 @@ class LoopRunner {
1357
1980
  contextItems,
1358
1981
  input: initialInput,
1359
1982
  messages: [...state.messages]
1360
- }, {
1361
- sessionId: state.id,
1362
- requestId,
1363
- iteration,
1364
- cwd: state.cwd,
1365
- metadata: state.metadata
1366
- });
1983
+ }, this.createHookContext(state, { requestId, iteration }));
1367
1984
  const effectiveContext = resolvedContext?.contextItems ?? contextItems;
1368
1985
  const pluginPromptSegments = await this.pluginHost?.collectPromptSegments({
1369
1986
  sessionId: state.id,
@@ -1373,37 +1990,31 @@ class LoopRunner {
1373
1990
  cwd: state.cwd
1374
1991
  }) ?? [];
1375
1992
  const promptSegments = [
1993
+ ...pendingContextSegments,
1376
1994
  ...this.contextManager.format(effectiveContext),
1377
1995
  ...pluginPromptSegments
1378
1996
  ];
1997
+ pendingContextSegments = [];
1379
1998
  const resolvedPrompt = await this.pluginHost?.runMiddleware("prompt.resolve", {
1380
1999
  promptSegments,
1381
2000
  contextItems: effectiveContext,
1382
2001
  messages: [...state.messages]
1383
- }, {
1384
- sessionId: state.id,
1385
- requestId,
1386
- iteration,
1387
- cwd: state.cwd,
1388
- metadata: state.metadata
1389
- });
2002
+ }, this.createHookContext(state, { requestId, iteration }));
1390
2003
  const effectivePromptSegments = resolvedPrompt?.promptSegments ?? promptSegments;
1391
2004
  const toolDefinitions = state.tools?.definitions() ?? [];
1392
2005
  const resolvedToolset = await this.pluginHost?.runMiddleware("toolset.resolve", {
1393
2006
  tools: toolDefinitions
1394
- }, {
1395
- sessionId: state.id,
2007
+ }, this.createHookContext(state, { requestId, iteration }));
2008
+ const effectiveTools = resolvedToolset?.tools ?? toolDefinitions;
2009
+ const requestMessages = await this.resolveRequestMessages({
2010
+ state,
1396
2011
  requestId,
1397
2012
  iteration,
1398
- cwd: state.cwd,
1399
- metadata: state.metadata
2013
+ baseMessages: [...turnStart?.messages ?? state.messages],
2014
+ runtimePromptSegments: effectivePromptSegments,
2015
+ contextItems: effectiveContext,
2016
+ tools: effectiveTools
1400
2017
  });
1401
- const effectiveTools = resolvedToolset?.tools ?? toolDefinitions;
1402
- const requestMessages = [...turnStart?.messages ?? state.messages];
1403
- if (effectivePromptSegments.length > 0)
1404
- requestMessages.unshift(this.messageFactory.createSystemMessage(effectivePromptSegments.join(`
1405
-
1406
- `)));
1407
2018
  const request = {
1408
2019
  requestId,
1409
2020
  model: state.modelRef,
@@ -1414,25 +2025,30 @@ class LoopRunner {
1414
2025
  };
1415
2026
  const resolvedRequest = await this.pluginHost?.runMiddleware("model.request", {
1416
2027
  request
1417
- }, {
1418
- sessionId: state.id,
1419
- requestId,
1420
- iteration,
1421
- cwd: state.cwd,
1422
- metadata: state.metadata
1423
- });
2028
+ }, this.createHookContext(state, { requestId, iteration }));
1424
2029
  const effectiveRequest = resolvedRequest?.request ?? request;
1425
2030
  const turn = yield* this.turnCollector.collect({
1426
2031
  sessionId: state.id,
1427
2032
  requestId,
1428
2033
  events: state.model.stream(effectiveRequest),
1429
2034
  onEvent: async (event) => {
2035
+ if (event.type === "error") {
2036
+ await this.notificationBus.emit({
2037
+ type: "provider.error",
2038
+ source: "provider",
2039
+ level: "error",
2040
+ message: event.error.message,
2041
+ metadata: {
2042
+ requestId,
2043
+ code: event.error.code,
2044
+ status: event.error.status
2045
+ }
2046
+ }, {
2047
+ ...this.createHookContext(state, { requestId, iteration })
2048
+ });
2049
+ }
1430
2050
  await this.pluginHost?.runObservers("model.event", { event }, {
1431
- sessionId: state.id,
1432
- requestId,
1433
- iteration,
1434
- cwd: state.cwd,
1435
- metadata: state.metadata
2051
+ ...this.createHookContext(state, { requestId, iteration })
1436
2052
  });
1437
2053
  }
1438
2054
  });
@@ -1445,53 +2061,85 @@ class LoopRunner {
1445
2061
  });
1446
2062
  const assistantPayload = await this.pluginHost?.runMiddleware("assistant.message", {
1447
2063
  message: assistantDraft
1448
- }, {
1449
- sessionId: state.id,
1450
- requestId,
1451
- iteration,
1452
- cwd: state.cwd,
1453
- metadata: state.metadata
1454
- });
1455
- const assistantMessage = assistantPayload?.message ?? assistantDraft;
2064
+ }, this.createHookContext(state, { requestId, iteration }));
2065
+ let assistantMessage = assistantPayload?.message ?? assistantDraft;
1456
2066
  state.messages.push(assistantMessage);
1457
2067
  state.usage = addUsage(state.usage, turn.usage);
1458
2068
  touchRuntimeSessionState(state);
1459
2069
  const baseDecision = this.terminationPolicy.afterAssistantTurn(turn.toolCalls);
1460
- const decisionPayload = await this.pluginHost?.runMiddleware("loop.decide", {
2070
+ const stopBasePayload = {
2071
+ assistantMessage,
1461
2072
  toolCalls: turn.toolCalls,
1462
2073
  stopReason: turn.stopReason,
1463
2074
  decision: baseDecision
1464
- }, {
1465
- sessionId: state.id,
1466
- requestId,
1467
- iteration,
1468
- cwd: state.cwd,
1469
- metadata: state.metadata
2075
+ };
2076
+ const stopResult = await this.pluginHost?.runRunStop(stopBasePayload, {
2077
+ ...this.createHookContext(state, { requestId, iteration })
1470
2078
  });
1471
- const decision = decisionPayload?.decision ?? baseDecision;
1472
- if (decision.type === "done") {
2079
+ const stopPayload = stopResult?.payload ?? stopBasePayload;
2080
+ const stopControl = stopResult?.control;
2081
+ await this.pluginHost?.runObservers("run.stop", {
2082
+ ...stopPayload,
2083
+ additionalContext: stopControl?.type === "finalize" ? stopControl.additionalContext ?? stopPayload.additionalContext : stopPayload.additionalContext,
2084
+ finalMessage: stopControl?.type === "finalize" ? stopControl.finalMessage ?? stopPayload.finalMessage : stopPayload.finalMessage
2085
+ }, this.createHookContext(state, { requestId, iteration }));
2086
+ const stopDecision = stopControl?.type === "finalize" ? { type: "done" } : stopPayload.decision;
2087
+ const additionalContext = stopControl?.type === "finalize" ? stopControl.additionalContext : stopPayload.additionalContext;
2088
+ const finalMessage = stopControl?.type === "finalize" ? stopControl.finalMessage ?? stopPayload.finalMessage ?? stopPayload.assistantMessage : stopPayload.finalMessage ?? stopPayload.assistantMessage;
2089
+ if (additionalContext && additionalContext.length > 0 && stopDecision.type === "continue")
2090
+ pendingContextSegments = [...pendingContextSegments, ...additionalContext];
2091
+ if (stopDecision.type === "done") {
2092
+ assistantMessage = finalMessage;
2093
+ state.messages[state.messages.length - 1] = assistantMessage;
2094
+ touchRuntimeSessionState(state);
2095
+ const endPayload = await this.pluginHost?.runMiddleware("run.end", {
2096
+ message: assistantMessage,
2097
+ usage: { ...state.usage }
2098
+ }, this.createHookContext(state, { requestId, iteration }));
2099
+ const completedMessage = endPayload?.message ?? assistantMessage;
2100
+ const completedUsage = endPayload?.usage ?? { ...state.usage };
2101
+ state.messages[state.messages.length - 1] = completedMessage;
2102
+ state.usage = { ...completedUsage };
2103
+ touchRuntimeSessionState(state);
2104
+ await this.notificationBus.emit({
2105
+ type: "run.end",
2106
+ source: "runtime",
2107
+ message: `Run completed for session ${state.id}`,
2108
+ metadata: {
2109
+ sessionId: state.id,
2110
+ requestId
2111
+ }
2112
+ }, {
2113
+ ...this.createHookContext(state, { requestId, iteration })
2114
+ });
1473
2115
  yield {
1474
2116
  type: "done",
1475
2117
  sessionId: state.id,
1476
- message: assistantMessage,
1477
- usage: { ...state.usage }
2118
+ message: completedMessage,
2119
+ usage: completedUsage
1478
2120
  };
1479
- await this.pluginHost?.runObservers("session.end", { message: assistantMessage }, {
1480
- sessionId: state.id,
1481
- requestId,
1482
- iteration,
1483
- cwd: state.cwd,
1484
- metadata: state.metadata
1485
- });
1486
- await options.onDone?.(assistantMessage);
1487
- return assistantMessage;
2121
+ await this.pluginHost?.runObservers("run.end", {
2122
+ message: completedMessage,
2123
+ usage: completedUsage
2124
+ }, this.createHookContext(state, { requestId, iteration }));
2125
+ await options.onDone?.(completedMessage);
2126
+ return completedMessage;
1488
2127
  }
1489
2128
  for (const toolCall of turn.toolCalls) {
1490
2129
  yield { type: "tool_call", sessionId: state.id, toolCall };
2130
+ const pluginEvents = [];
1491
2131
  const result = await this.toolExecutor.execute(toolCall, {
1492
2132
  signal: options.signal,
1493
- cwd: state.cwd
2133
+ cwd: state.cwd,
2134
+ requestId,
2135
+ iteration,
2136
+ status: createSessionStatus(state),
2137
+ onEvent: async (event) => {
2138
+ pluginEvents.push(event);
2139
+ }
1494
2140
  });
2141
+ for (const event of pluginEvents)
2142
+ yield event;
1495
2143
  const toolMessage = this.messageFactory.createToolMessage(toolCall, result);
1496
2144
  state.messages.push(toolMessage);
1497
2145
  touchRuntimeSessionState(state);
@@ -1500,25 +2148,151 @@ class LoopRunner {
1500
2148
  }
1501
2149
  } catch (error) {
1502
2150
  if (error instanceof SessionExecutionError) {
2151
+ await this.notificationBus.emit({
2152
+ type: error.payload.code.startsWith("openai_") || error.payload.code.startsWith("anthropic_") || error.payload.code.startsWith("gemini_") ? "provider.error" : "runtime.error",
2153
+ source: error.payload.code.includes("_http_") || error.payload.code.includes("_request_") ? "provider" : "runtime",
2154
+ level: "error",
2155
+ message: error.payload.message,
2156
+ metadata: {
2157
+ requestId: lastRequestId,
2158
+ code: error.payload.code
2159
+ }
2160
+ }, {
2161
+ ...this.createHookContext(state, { requestId: lastRequestId, iteration: lastIteration })
2162
+ });
1503
2163
  await this.pluginHost?.runObservers("session.error", { error: error.payload }, {
1504
- sessionId: state.id,
1505
- requestId: lastRequestId,
1506
- cwd: state.cwd,
1507
- metadata: state.metadata
2164
+ ...this.createHookContext(state, { requestId: lastRequestId })
1508
2165
  });
1509
2166
  }
1510
2167
  throw error;
1511
2168
  }
1512
2169
  const payload = this.terminationPolicy.onMaxIterationsExceeded(state.maxIterations, lastRequestId);
1513
2170
  yield { type: "error", sessionId: state.id, error: payload };
2171
+ await this.notificationBus.emit({
2172
+ type: "runtime.error",
2173
+ source: "runtime",
2174
+ level: "error",
2175
+ message: payload.message,
2176
+ metadata: {
2177
+ requestId: lastRequestId,
2178
+ code: payload.code
2179
+ }
2180
+ }, {
2181
+ ...this.createHookContext(state, { requestId: lastRequestId, iteration: lastIteration })
2182
+ });
1514
2183
  await this.pluginHost?.runObservers("session.error", { error: payload }, {
1515
- sessionId: state.id,
1516
- requestId: lastRequestId,
1517
- cwd: state.cwd,
1518
- metadata: state.metadata
2184
+ ...this.createHookContext(state, { requestId: lastRequestId })
1519
2185
  });
1520
2186
  throw new SessionExecutionError(payload);
1521
2187
  }
2188
+ async resolveRequestMessages(input) {
2189
+ const initialAttempt = await this.buildProjectedRequestMessages(input.state, input.baseMessages, input.runtimePromptSegments, input.tools);
2190
+ if (!isCompactionRequired(input.state.compactionOptions, initialAttempt.budget))
2191
+ return initialAttempt.messages;
2192
+ const budget = initialAttempt.budget;
2193
+ if (!budget)
2194
+ return initialAttempt.messages;
2195
+ await this.notificationBus.emit({
2196
+ type: "context.compaction.required",
2197
+ source: "runtime",
2198
+ level: "warning",
2199
+ message: `Context compaction required for session ${input.state.id}`,
2200
+ metadata: {
2201
+ requestId: input.requestId,
2202
+ estimatedInputTokens: budget.estimatedInputTokens,
2203
+ thresholdTokens: budget.thresholdTokens,
2204
+ maxInputTokens: budget.maxInputTokens,
2205
+ cursor: input.state.compaction.cursor
2206
+ }
2207
+ }, {
2208
+ ...this.createHookContext(input.state, { requestId: input.requestId, iteration: input.iteration })
2209
+ });
2210
+ const beforeState = cloneCompactionState(input.state.compaction);
2211
+ const compactPayload = await this.pluginHost?.runMiddleware("context.compact.before", {
2212
+ sessionId: input.state.id,
2213
+ messages: [...input.state.messages],
2214
+ cursor: beforeState.cursor,
2215
+ systemSegments: [...beforeState.systemSegments],
2216
+ contextItems: input.contextItems,
2217
+ estimatedInputTokens: budget.estimatedInputTokens,
2218
+ thresholdTokens: budget.thresholdTokens,
2219
+ maxInputTokens: budget.maxInputTokens,
2220
+ trigger: "threshold"
2221
+ }, {
2222
+ ...this.createHookContext(input.state, { requestId: input.requestId, iteration: input.iteration })
2223
+ });
2224
+ await this.pluginHost?.runObservers("context.compact.before", compactPayload ?? {
2225
+ sessionId: input.state.id,
2226
+ messages: [...input.state.messages],
2227
+ cursor: beforeState.cursor,
2228
+ systemSegments: [...beforeState.systemSegments],
2229
+ contextItems: input.contextItems,
2230
+ estimatedInputTokens: budget.estimatedInputTokens,
2231
+ thresholdTokens: budget.thresholdTokens,
2232
+ maxInputTokens: budget.maxInputTokens,
2233
+ trigger: "threshold"
2234
+ }, {
2235
+ ...this.createHookContext(input.state, { requestId: input.requestId, iteration: input.iteration })
2236
+ });
2237
+ input.state.compaction = normalizeCompactionState(input.state.compaction, input.state.messages);
2238
+ const afterState = cloneCompactionState(input.state.compaction);
2239
+ if (didCompactionStateChange(beforeState, afterState)) {
2240
+ const latestCheckpoint = afterState.checkpoints.at(-1);
2241
+ await this.notificationBus.emit({
2242
+ type: "context.compacted",
2243
+ source: "runtime",
2244
+ level: "info",
2245
+ message: `Context compacted for session ${input.state.id}`,
2246
+ metadata: {
2247
+ requestId: input.requestId,
2248
+ cursor: afterState.cursor,
2249
+ checkpointId: latestCheckpoint?.id,
2250
+ reason: latestCheckpoint?.reason
2251
+ }
2252
+ }, {
2253
+ ...this.createHookContext(input.state, { requestId: input.requestId, iteration: input.iteration })
2254
+ });
2255
+ }
2256
+ const compactedAttempt = await this.buildProjectedRequestMessages(input.state, input.baseMessages, input.runtimePromptSegments, input.tools);
2257
+ if (!isCompactionRequired(input.state.compactionOptions, compactedAttempt.budget))
2258
+ return compactedAttempt.messages;
2259
+ throw new SessionExecutionError({
2260
+ code: "context_compaction_required",
2261
+ message: `Context compaction required before continuing the run (estimated ${budget.estimatedInputTokens} tokens, threshold ${budget.thresholdTokens})`,
2262
+ requestId: input.requestId
2263
+ });
2264
+ }
2265
+ async buildProjectedRequestMessages(state, baseMessages, runtimePromptSegments, tools) {
2266
+ const projectedMessages = projectMessagesForRequest(baseMessages, state.compaction);
2267
+ const promptSegments = [
2268
+ ...buildCompactionPromptSegments(state.compaction),
2269
+ ...runtimePromptSegments
2270
+ ];
2271
+ const requestMessages = [...projectedMessages];
2272
+ if (promptSegments.length > 0)
2273
+ requestMessages.unshift(this.messageFactory.createSystemMessage(promptSegments.join(`
2274
+
2275
+ `)));
2276
+ const budget = state.compactionOptions && state.compactionOptions.enabled !== false ? await estimateCompactionBudget(state.compactionOptions, {
2277
+ model: state.modelRef,
2278
+ messages: requestMessages,
2279
+ tools
2280
+ }) : undefined;
2281
+ return {
2282
+ messages: requestMessages,
2283
+ budget
2284
+ };
2285
+ }
2286
+ createHookContext(state, input = {}) {
2287
+ return {
2288
+ sessionId: state.id,
2289
+ requestId: input.requestId,
2290
+ iteration: input.iteration,
2291
+ cwd: state.cwd,
2292
+ metadata: state.metadata,
2293
+ status: createSessionStatus(state)
2294
+ };
2295
+ }
1522
2296
  }
1523
2297
 
1524
2298
  // src/agent-core/TerminationPolicy.ts
@@ -1542,52 +2316,117 @@ class DefaultToolExecutor {
1542
2316
  metadata;
1543
2317
  services;
1544
2318
  pluginHost;
2319
+ approvalManager;
2320
+ notificationBus;
1545
2321
  constructor(options) {
1546
2322
  this.sessionId = options.sessionId;
1547
2323
  this.tools = options.tools;
1548
2324
  this.metadata = options.metadata;
1549
2325
  this.services = options.services;
1550
2326
  this.pluginHost = options.pluginHost;
2327
+ this.approvalManager = options.approvalManager;
2328
+ this.notificationBus = options.notificationBus;
1551
2329
  }
1552
2330
  async execute(toolCall, options = {}) {
1553
- const before = await this.pluginHost?.runMiddleware("tool.beforeExecute", {
1554
- toolCall
1555
- }, {
2331
+ const hookContext = {
1556
2332
  sessionId: this.sessionId,
2333
+ requestId: options.requestId,
2334
+ iteration: options.iteration,
1557
2335
  cwd: options.cwd,
2336
+ status: options.status,
1558
2337
  services: this.services,
1559
2338
  metadata: this.metadata
1560
- });
1561
- const currentToolCall = before?.toolCall ?? toolCall;
1562
- const tool = this.tools?.get(currentToolCall.function.name);
1563
- if (!tool)
1564
- return normalizeToolResult({ ...textToolResult(`Tool not found: ${currentToolCall.function.name}`), isError: true });
2339
+ };
2340
+ const before = await this.pluginHost?.runToolBeforeExecute({
2341
+ toolCall
2342
+ }, hookContext);
2343
+ let currentToolCall = before?.payload.toolCall ?? toolCall;
1565
2344
  let result;
1566
- try {
1567
- result = normalizeToolResult(await tool.execute(currentToolCall.function.arguments, {
1568
- sessionId: this.sessionId,
1569
- cwd: options.cwd,
1570
- signal: options.signal,
1571
- metadata: this.metadata,
1572
- services: this.services
1573
- }));
1574
- } catch (error) {
1575
- const message = error instanceof Error ? error.message : String(error);
1576
- result = normalizeToolResult({
1577
- ...textToolResult(message),
1578
- isError: true
1579
- });
2345
+ if (before?.control) {
2346
+ if (before.control.type === "provideResult") {
2347
+ result = normalizeToolResult(before.control.result);
2348
+ } else if (before.control.type === "deny") {
2349
+ result = normalizeToolResult(before.control.result ?? {
2350
+ ...textToolResult(before.control.reason ?? `Tool denied: ${currentToolCall.function.name}`),
2351
+ isError: true
2352
+ });
2353
+ await this.notificationBus.emit({
2354
+ type: "tool.denied",
2355
+ source: "permission",
2356
+ level: "warning",
2357
+ message: before.control.reason ?? `Tool denied: ${currentToolCall.function.name}`,
2358
+ metadata: {
2359
+ toolName: currentToolCall.function.name
2360
+ }
2361
+ }, hookContext);
2362
+ } else if (before.control.type === "ask") {
2363
+ currentToolCall = before.control.toolCall ?? currentToolCall;
2364
+ const approvalRequest = {
2365
+ id: undefined,
2366
+ kind: "tool_execution",
2367
+ toolCall: currentToolCall,
2368
+ message: before.control.request?.message ?? `Approval required to execute tool: ${currentToolCall.function.name}`,
2369
+ metadata: before.control.request?.metadata
2370
+ };
2371
+ const decision = await this.approvalManager.requestApproval(approvalRequest, hookContext);
2372
+ if (decision.type === "denied") {
2373
+ result = normalizeToolResult({
2374
+ ...textToolResult(decision.reason ?? `Tool approval denied: ${currentToolCall.function.name}`),
2375
+ isError: true
2376
+ });
2377
+ }
2378
+ }
2379
+ }
2380
+ if (!result) {
2381
+ const tool = this.tools?.get(currentToolCall.function.name);
2382
+ if (!tool) {
2383
+ result = normalizeToolResult({ ...textToolResult(`Tool not found: ${currentToolCall.function.name}`), isError: true });
2384
+ } else {
2385
+ try {
2386
+ result = normalizeToolResult(await tool.execute(currentToolCall.function.arguments, {
2387
+ sessionId: this.sessionId,
2388
+ cwd: options.cwd,
2389
+ signal: options.signal,
2390
+ metadata: this.metadata,
2391
+ services: this.services,
2392
+ emitEvent: async (event) => {
2393
+ const pluginId = event.pluginId;
2394
+ if (!pluginId)
2395
+ throw new Error(`Tool ${currentToolCall.function.name} emitted a plugin event without pluginId`);
2396
+ await options.onEvent?.({
2397
+ type: "plugin_event",
2398
+ sessionId: this.sessionId,
2399
+ pluginId,
2400
+ event: event.event,
2401
+ data: event.data ? structuredClone(event.data) : undefined
2402
+ });
2403
+ }
2404
+ }));
2405
+ } catch (error) {
2406
+ const message = error instanceof Error ? error.message : String(error);
2407
+ result = normalizeToolResult({
2408
+ ...textToolResult(message),
2409
+ isError: true
2410
+ });
2411
+ }
2412
+ }
1580
2413
  }
1581
2414
  const after = await this.pluginHost?.runMiddleware("tool.afterExecute", {
1582
2415
  toolCall: currentToolCall,
1583
2416
  result
1584
- }, {
1585
- sessionId: this.sessionId,
1586
- cwd: options.cwd,
1587
- services: this.services,
1588
- metadata: this.metadata
1589
- });
1590
- return normalizeToolResult(after?.result ?? result);
2417
+ }, hookContext);
2418
+ const finalResult = normalizeToolResult(after?.result ?? result);
2419
+ await this.notificationBus.emit({
2420
+ type: finalResult.isError ? "tool.failed" : "tool.completed",
2421
+ source: "tool",
2422
+ level: finalResult.isError ? "error" : "info",
2423
+ message: finalResult.isError ? `Tool failed: ${currentToolCall.function.name}` : `Tool completed: ${currentToolCall.function.name}`,
2424
+ metadata: {
2425
+ toolName: currentToolCall.function.name,
2426
+ isError: finalResult.isError === true
2427
+ }
2428
+ }, hookContext);
2429
+ return finalResult;
1591
2430
  }
1592
2431
  }
1593
2432
 
@@ -1600,6 +2439,12 @@ class Session {
1600
2439
  pluginHost;
1601
2440
  contextManager;
1602
2441
  services;
2442
+ approvalManager;
2443
+ notificationBus;
2444
+ compactionService;
2445
+ pluginStateService;
2446
+ pluginControllers;
2447
+ activeRunCount = 0;
1603
2448
  constructor(options) {
1604
2449
  const now = Date.now();
1605
2450
  this.id = options.id ?? createId("session");
@@ -1608,6 +2453,10 @@ class Session {
1608
2453
  this.pluginHost = options.pluginHost;
1609
2454
  this.contextManager = options.contextManager;
1610
2455
  this.services = options.services;
2456
+ this.approvalManager = options.approvalManager;
2457
+ this.notificationBus = options.notificationBus;
2458
+ this.compactionService = options.compactionService;
2459
+ this.pluginStateService = options.pluginStateService;
1611
2460
  this.state = {
1612
2461
  id: this.id,
1613
2462
  model: options.model,
@@ -1619,9 +2468,21 @@ class Session {
1619
2468
  reasoning: options.reasoning,
1620
2469
  messages: [...options.messages ?? []],
1621
2470
  usage: options.usage ? { ...options.usage } : createEmptyUsage(),
2471
+ compaction: cloneCompactionState(options.compaction ?? createEmptyCompactionState()),
2472
+ pluginState: clonePluginStateMap(options.pluginState),
2473
+ compactionOptions: options.compactionOptions,
1622
2474
  createdAt: this.createdAt,
1623
2475
  updatedAt: options.updatedAt ?? now
1624
2476
  };
2477
+ this.compactionService.registerSession(this.state);
2478
+ this.pluginStateService.registerSession(this.state);
2479
+ this.pluginControllers = this.pluginHost?.createSessionControllers({
2480
+ sessionId: this.id,
2481
+ metadata: this.state.metadata,
2482
+ getStatus: () => this.getStatus(),
2483
+ save: () => this.save(),
2484
+ isRunning: () => this.activeRunCount > 0
2485
+ }) ?? new Map;
1625
2486
  }
1626
2487
  get messages() {
1627
2488
  return [...this.state.messages];
@@ -1629,6 +2490,21 @@ class Session {
1629
2490
  get usage() {
1630
2491
  return { ...this.state.usage };
1631
2492
  }
2493
+ getStatus() {
2494
+ return createSessionStatus(this.state);
2495
+ }
2496
+ getCompactionState() {
2497
+ return cloneCompactionState(this.state.compaction);
2498
+ }
2499
+ getPluginState(pluginId) {
2500
+ return clonePluginSessionStateEntry(this.state.pluginState[pluginId]);
2501
+ }
2502
+ listPluginStates() {
2503
+ return clonePluginStateMap(this.state.pluginState);
2504
+ }
2505
+ getPlugin(pluginId) {
2506
+ return this.pluginControllers.get(pluginId) ?? null;
2507
+ }
1632
2508
  get updatedAt() {
1633
2509
  return this.state.updatedAt;
1634
2510
  }
@@ -1639,20 +2515,76 @@ class Session {
1639
2515
  this.state.cwd = cwd;
1640
2516
  touchRuntimeSessionState(this.state);
1641
2517
  }
2518
+ async compact(update) {
2519
+ const beforeState = this.getCompactionState();
2520
+ const estimatedInputTokens = await this.readEstimatedInputTokens();
2521
+ const thresholdTokens = this.state.compactionOptions?.triggerTokens ?? (this.state.compactionOptions ? Math.max(1, Math.floor(this.state.compactionOptions.maxInputTokens * (this.state.compactionOptions.triggerRatio ?? 0.8))) : 0);
2522
+ const maxInputTokens = this.state.compactionOptions?.maxInputTokens ?? 0;
2523
+ const payload = {
2524
+ sessionId: this.id,
2525
+ messages: this.messages,
2526
+ cursor: beforeState.cursor,
2527
+ systemSegments: [...beforeState.systemSegments],
2528
+ contextItems: undefined,
2529
+ estimatedInputTokens,
2530
+ thresholdTokens,
2531
+ maxInputTokens,
2532
+ trigger: "manual"
2533
+ };
2534
+ if (this.pluginHost) {
2535
+ const compactPayload = await this.pluginHost.runMiddleware("context.compact.before", payload, {
2536
+ ...this.createHookContext(),
2537
+ services: this.services
2538
+ });
2539
+ await this.pluginHost.runObservers("context.compact.before", compactPayload, {
2540
+ ...this.createHookContext(),
2541
+ services: this.services
2542
+ });
2543
+ }
2544
+ await this.compactionService.apply({
2545
+ sessionId: this.id,
2546
+ ...update,
2547
+ reason: update.reason ?? "manual"
2548
+ });
2549
+ const afterState = this.getCompactionState();
2550
+ const latestCheckpoint = afterState.checkpoints.at(-1);
2551
+ await this.notificationBus.emit({
2552
+ type: "context.compacted",
2553
+ source: "runtime",
2554
+ level: "info",
2555
+ message: `Context compacted for session ${this.id}`,
2556
+ metadata: {
2557
+ cursor: afterState.cursor,
2558
+ checkpointId: latestCheckpoint?.id,
2559
+ reason: latestCheckpoint?.reason
2560
+ }
2561
+ }, {
2562
+ ...this.createHookContext()
2563
+ });
2564
+ }
1642
2565
  async save() {
1643
2566
  if (!this.stateStore)
1644
2567
  return;
1645
2568
  let snapshot = this.toSnapshot();
1646
2569
  if (this.pluginHost) {
1647
- const payload = await this.pluginHost.runMiddleware("snapshot.encode", { snapshot }, {
1648
- sessionId: this.id,
1649
- cwd: this.state.cwd,
1650
- metadata: this.state.metadata,
2570
+ snapshot = await this.pluginHost.runMiddleware("snapshot.encode", { snapshot }, {
2571
+ ...this.createHookContext(),
1651
2572
  services: this.services
1652
- });
1653
- snapshot = payload.snapshot;
2573
+ }).then((payload) => payload.snapshot);
1654
2574
  }
1655
2575
  await this.stateStore.save(snapshot);
2576
+ await this.notificationBus.emit({
2577
+ type: "snapshot.saved",
2578
+ source: "persistence",
2579
+ level: "info",
2580
+ message: `Saved snapshot for session ${this.id}`,
2581
+ metadata: {
2582
+ sessionId: this.id,
2583
+ schemaVersion: snapshot.schemaVersion
2584
+ }
2585
+ }, {
2586
+ ...this.createHookContext()
2587
+ });
1656
2588
  }
1657
2589
  toSnapshot() {
1658
2590
  return sessionSnapshotCodec.encode({
@@ -1661,6 +2593,8 @@ class Session {
1661
2593
  cwd: this.state.cwd,
1662
2594
  messages: this.messages,
1663
2595
  usage: this.usage,
2596
+ compaction: this.getCompactionState(),
2597
+ pluginState: this.listPluginStates(),
1664
2598
  createdAt: this.createdAt,
1665
2599
  updatedAt: this.updatedAt,
1666
2600
  metadata: this.state.metadata ? { ...this.state.metadata } : undefined
@@ -1682,12 +2616,17 @@ class Session {
1682
2616
  }
1683
2617
  async* stream(input, options) {
1684
2618
  const runner = this.createLoopRunner();
1685
- return yield* runner.run(this.state, input, {
1686
- signal: options?.signal,
1687
- onDone: async () => {
1688
- await this.save();
1689
- }
1690
- });
2619
+ this.activeRunCount += 1;
2620
+ try {
2621
+ return yield* runner.run(this.state, input, {
2622
+ signal: options?.signal,
2623
+ onDone: async () => {
2624
+ await this.save();
2625
+ }
2626
+ });
2627
+ } finally {
2628
+ this.activeRunCount = Math.max(0, this.activeRunCount - 1);
2629
+ }
1691
2630
  }
1692
2631
  createLoopRunner() {
1693
2632
  return new LoopRunner({
@@ -1698,13 +2637,43 @@ class Session {
1698
2637
  tools: this.state.tools,
1699
2638
  metadata: this.state.metadata,
1700
2639
  services: this.services,
1701
- pluginHost: this.pluginHost
2640
+ pluginHost: this.pluginHost,
2641
+ approvalManager: this.approvalManager,
2642
+ notificationBus: this.notificationBus
1702
2643
  }),
1703
2644
  terminationPolicy: new DefaultTerminationPolicy,
1704
2645
  contextManager: this.contextManager,
1705
- pluginHost: this.pluginHost
2646
+ pluginHost: this.pluginHost,
2647
+ notificationBus: this.notificationBus
1706
2648
  });
1707
2649
  }
2650
+ async readEstimatedInputTokens() {
2651
+ if (!this.state.compactionOptions || this.state.compactionOptions.enabled === false)
2652
+ return 0;
2653
+ const projectedMessages = projectMessagesForRequest(this.state.messages, this.state.compaction);
2654
+ const promptSegments = buildCompactionPromptSegments(this.state.compaction);
2655
+ const requestMessages = [...projectedMessages];
2656
+ if (promptSegments.length > 0)
2657
+ requestMessages.unshift(new DefaultMessageFactory().createSystemMessage(promptSegments.join(`
2658
+
2659
+ `)));
2660
+ const budget = await estimateCompactionBudget(this.state.compactionOptions, {
2661
+ model: this.state.modelRef,
2662
+ messages: requestMessages,
2663
+ tools: this.state.tools?.definitions() ?? []
2664
+ });
2665
+ return budget.estimatedInputTokens;
2666
+ }
2667
+ createHookContext(input = {}) {
2668
+ return {
2669
+ sessionId: this.id,
2670
+ requestId: input.requestId,
2671
+ iteration: input.iteration,
2672
+ cwd: this.state.cwd,
2673
+ metadata: this.state.metadata,
2674
+ status: this.getStatus()
2675
+ };
2676
+ }
1708
2677
  }
1709
2678
 
1710
2679
  // src/agent-core/Agent.ts
@@ -1719,13 +2688,23 @@ class Agent {
1719
2688
  contextManager;
1720
2689
  tracker;
1721
2690
  permissionGateway;
2691
+ approvalManager;
2692
+ notificationBus;
2693
+ compactionService;
2694
+ pluginStateService;
1722
2695
  constructor(options) {
1723
2696
  this.id = createId("agent");
1724
2697
  this.options = options;
1725
2698
  this.permissions = options.permissions;
2699
+ validateCompactionOwner(options);
1726
2700
  this.tracker = new ActivityTracker;
1727
- this.services = createServices(options, this.tracker);
1728
- this.permissionGateway = new PermissionGateway({ permissions: options.permissions });
2701
+ this.compactionService = new DefaultCompactionService;
2702
+ this.pluginStateService = new DefaultPluginStateService;
2703
+ this.services = createServices(options, this.tracker, this.compactionService, this.pluginStateService);
2704
+ this.permissionGateway = new PermissionGateway({
2705
+ permissions: options.permissions,
2706
+ compactionOwnerPluginId: options.compaction?.ownerPluginId
2707
+ });
1729
2708
  this.pluginHost = new PluginHost({
1730
2709
  agentId: this.id,
1731
2710
  cwd: options.cwd,
@@ -1734,6 +2713,14 @@ class Agent {
1734
2713
  permissionGateway: this.permissionGateway,
1735
2714
  tracker: this.tracker
1736
2715
  });
2716
+ this.notificationBus = new DefaultNotificationBus({
2717
+ pluginHost: this.pluginHost,
2718
+ services: this.services
2719
+ });
2720
+ this.approvalManager = new DefaultApprovalManager({
2721
+ handler: options.approvalHandler,
2722
+ notifications: this.notificationBus
2723
+ });
1737
2724
  this.contextManager = options.contextManager ?? new AutoContextManager({
1738
2725
  services: this.services,
1739
2726
  pluginHost: this.pluginHost
@@ -1758,10 +2745,15 @@ class Agent {
1758
2745
  cwd: options.cwd ?? this.options.cwd,
1759
2746
  metadata: options.metadata ?? this.options.metadata,
1760
2747
  reasoning: this.options.reasoning,
2748
+ compactionOptions: this.options.compaction,
1761
2749
  messages,
1762
2750
  pluginHost: this.pluginHost,
1763
2751
  contextManager: this.contextManager,
1764
- services: this.services
2752
+ services: this.services,
2753
+ approvalManager: this.approvalManager,
2754
+ notificationBus: this.notificationBus,
2755
+ compactionService: this.compactionService,
2756
+ pluginStateService: this.pluginStateService
1765
2757
  });
1766
2758
  }
1767
2759
  async restoreSession(sessionId) {
@@ -1785,27 +2777,44 @@ class Agent {
1785
2777
  reasoning: this.options.reasoning,
1786
2778
  messages: snapshot.messages,
1787
2779
  usage: snapshot.usage,
2780
+ compaction: snapshot.compaction,
2781
+ pluginState: snapshot.pluginState,
2782
+ compactionOptions: this.options.compaction,
1788
2783
  createdAt: snapshot.createdAt,
1789
2784
  updatedAt: snapshot.updatedAt,
1790
2785
  pluginHost: this.pluginHost,
1791
2786
  contextManager: this.contextManager,
1792
- services: this.services
2787
+ services: this.services,
2788
+ approvalManager: this.approvalManager,
2789
+ notificationBus: this.notificationBus,
2790
+ compactionService: this.compactionService,
2791
+ pluginStateService: this.pluginStateService
1793
2792
  });
1794
2793
  }
1795
2794
  }
1796
2795
  function createAgent(options) {
1797
2796
  return new Agent(options);
1798
2797
  }
1799
- function createServices(options, tracker) {
2798
+ function createServices(options, tracker, compaction, pluginState) {
1800
2799
  const exec = new NodeExecGateway({ cwd: options.cwd, tracker });
1801
2800
  return {
1802
2801
  fileSystem: new NodeFileSystemGateway({ cwd: options.cwd, tracker }),
1803
2802
  exec,
1804
2803
  git: new DefaultGitGateway({ exec, cwd: options.cwd }),
1805
2804
  network: new DefaultNetworkGateway,
1806
- model: new DefaultModelGateway(options.model)
2805
+ model: new DefaultModelGateway(options.model),
2806
+ compaction,
2807
+ pluginState
1807
2808
  };
1808
2809
  }
2810
+ function validateCompactionOwner(options) {
2811
+ const ownerPluginId = options.compaction?.ownerPluginId;
2812
+ if (!ownerPluginId)
2813
+ return;
2814
+ const loadedPluginIds = new Set((options.plugins ?? []).map((plugin) => plugin.manifest.id));
2815
+ if (!loadedPluginIds.has(ownerPluginId))
2816
+ throw new Error(`Compaction owner plugin "${ownerPluginId}" is not loaded`);
2817
+ }
1809
2818
  function createToolRegistry(input) {
1810
2819
  const registry = input.tools instanceof ToolRegistry ? cloneRegistry(input.tools) : new ToolRegistry;
1811
2820
  if (input.includeBuiltinTools) {
@@ -2585,16 +3594,25 @@ function imageDataUrl(block) {
2585
3594
  return `data:${block.mimeType};base64,${block.data}`;
2586
3595
  }
2587
3596
  function messagesToOpenAI(messages) {
2588
- return messages.map((message) => {
3597
+ const systemSegments = [];
3598
+ const mappedMessages = [];
3599
+ for (const message of messages) {
3600
+ if (message.role === "system") {
3601
+ const text = contentToText(message.content);
3602
+ if (text)
3603
+ systemSegments.push(text);
3604
+ continue;
3605
+ }
2589
3606
  if (message.role === "tool") {
2590
- return {
3607
+ mappedMessages.push({
2591
3608
  role: "tool",
2592
3609
  tool_call_id: message.toolCallId,
2593
3610
  content: message.content.length > 0 ? contentToText(message.content) : JSON.stringify(message.structuredContent ?? {})
2594
- };
3611
+ });
3612
+ continue;
2595
3613
  }
2596
3614
  if (message.role === "assistant") {
2597
- return {
3615
+ mappedMessages.push({
2598
3616
  role: "assistant",
2599
3617
  content: contentToText(message.content),
2600
3618
  tool_calls: message.toolCalls?.map((toolCall) => ({
@@ -2605,14 +3623,23 @@ function messagesToOpenAI(messages) {
2605
3623
  arguments: JSON.stringify(toolCall.function.arguments)
2606
3624
  }
2607
3625
  }))
2608
- };
3626
+ });
3627
+ continue;
2609
3628
  }
2610
- return {
3629
+ mappedMessages.push({
2611
3630
  role: message.role,
2612
3631
  content: messageContentToOpenAI(message.content),
2613
3632
  ...message.role === "user" && message.name ? { name: message.name } : {}
2614
- };
2615
- });
3633
+ });
3634
+ }
3635
+ if (systemSegments.length === 0)
3636
+ return mappedMessages;
3637
+ return [{
3638
+ role: "system",
3639
+ content: systemSegments.join(`
3640
+
3641
+ `)
3642
+ }, ...mappedMessages];
2616
3643
  }
2617
3644
  function normalizeOpenAIToolCalls(value) {
2618
3645
  if (!value)
@@ -2990,6 +4017,7 @@ export {
2990
4017
  DefaultNetworkGateway,
2991
4018
  DefaultModelGateway,
2992
4019
  DefaultGitGateway,
4020
+ DefaultCompactionService,
2993
4021
  BaseTool,
2994
4022
  AutoContextManager,
2995
4023
  Agent,