@holon-run/agentinbox 1.0.0-beta.3 → 1.0.0-beta.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.
@@ -147,6 +147,9 @@ class AgentInboxService {
147
147
  listSources() {
148
148
  return this.store.listSources();
149
149
  }
150
+ async listSourceViews() {
151
+ return this.attachSourceRuntimes(this.store.listSources());
152
+ }
150
153
  listHosts() {
151
154
  return this.store.listSourceHosts();
152
155
  }
@@ -191,8 +194,10 @@ class AgentInboxService {
191
194
  ? schema
192
195
  : (0, source_resolution_1.withResolvedIdentity)(source.sourceId, fallbackSchema, resolvedIdentity))
193
196
  : fallbackSchema;
197
+ const runtime = await this.adapters.getSourceRuntime(source);
194
198
  return {
195
199
  source,
200
+ runtime,
196
201
  host: source.hostId ? this.store.getSourceHost(source.hostId) : null,
197
202
  resolvedIdentity,
198
203
  ...(resolutionError ? { resolutionError } : {}),
@@ -299,6 +304,112 @@ class AgentInboxService {
299
304
  throw new Error(`preview failed: ${message}`);
300
305
  }
301
306
  }
307
+ async follow(input) {
308
+ const templateArgs = input.templateArgs ?? {};
309
+ const resolved = await this.resolveFollowTemplate(input, templateArgs);
310
+ validateFollowTemplateArgs(resolved.templateSpec.argsSchema ?? [], templateArgs, resolved.templateSpec.templateId);
311
+ const plan = await this.adapters.expandFollowTemplate(resolved.source, {
312
+ template: input.template,
313
+ args: templateArgs,
314
+ source: resolved.source,
315
+ });
316
+ if (!plan) {
317
+ throw new Error(`unknown follow template ${input.providerOrKind}.${input.template}`);
318
+ }
319
+ validateExpandedFollowPlan(plan, resolved.templateSpec.templateId);
320
+ const sourceRefs = new Map();
321
+ const sourceResults = [];
322
+ for (const ensuredSource of plan.sources) {
323
+ const result = await this.ensureFollowSource(ensuredSource);
324
+ sourceRefs.set(ensuredSource.logicalName, result.source);
325
+ sourceResults.push({
326
+ sourceId: result.source.sourceId,
327
+ streamKind: result.source.streamKind,
328
+ created: result.created,
329
+ });
330
+ }
331
+ const createdSubscriptions = [];
332
+ const subscriptionResults = [];
333
+ try {
334
+ for (const ensuredSubscription of plan.subscriptions) {
335
+ const source = sourceRefs.get(ensuredSubscription.sourceLogicalName);
336
+ if (!source) {
337
+ throw new Error(`follow template ${plan.templateId} references unknown ensured source ${ensuredSubscription.sourceLogicalName}`);
338
+ }
339
+ const result = await this.ensureSubscription({
340
+ agentId: input.agentId,
341
+ sourceId: source.sourceId,
342
+ filter: ensuredSubscription.filter,
343
+ trackedResourceRef: ensuredSubscription.trackedResourceRef ?? null,
344
+ cleanupPolicy: ensuredSubscription.cleanupPolicy ?? null,
345
+ startPolicy: ensuredSubscription.startPolicy ?? input.startPolicy,
346
+ startOffset: ensuredSubscription.startOffset ?? input.startOffset ?? null,
347
+ startTime: ensuredSubscription.startTime ?? input.startTime ?? null,
348
+ });
349
+ if (result.created) {
350
+ createdSubscriptions.push(result.subscription);
351
+ }
352
+ subscriptionResults.push({
353
+ subscriptionId: result.subscription.subscriptionId,
354
+ sourceId: result.subscription.sourceId,
355
+ created: result.created,
356
+ });
357
+ }
358
+ }
359
+ catch (error) {
360
+ await this.rollbackRegisteredSubscriptions(createdSubscriptions);
361
+ throw error;
362
+ }
363
+ return {
364
+ templateId: plan.templateId,
365
+ agentId: input.agentId,
366
+ sources: sourceResults,
367
+ subscriptions: subscriptionResults,
368
+ };
369
+ }
370
+ async resolveFollowTemplate(input, templateArgs) {
371
+ const previewInput = {
372
+ configRef: input.configRef ?? null,
373
+ config: mergeFollowPreviewConfig(input.config, templateArgs),
374
+ };
375
+ const candidates = this.adapters.followPreviewRefsForProviderOrKind(input.providerOrKind, previewInput.config);
376
+ let previewFailure = null;
377
+ for (const sourceRef of candidates) {
378
+ try {
379
+ const source = buildPreviewSource({
380
+ sourceRef,
381
+ configRef: previewInput.configRef,
382
+ config: previewInput.config,
383
+ });
384
+ const templateSpec = (await this.adapters.listFollowTemplates(source)).find((candidate) => (candidate.providerOrKind === input.providerOrKind || sourceRef === input.providerOrKind) &&
385
+ followTemplateName(candidate.templateId) === input.template);
386
+ if (!templateSpec) {
387
+ continue;
388
+ }
389
+ await this.adapters.sourceAdapterFor(source.sourceType).validateSource?.(source);
390
+ return { source, templateSpec };
391
+ }
392
+ catch (error) {
393
+ if (!previewFailure || sourceRef === input.providerOrKind || candidates.length === 1) {
394
+ previewFailure = error instanceof Error ? error : new Error(String(error));
395
+ }
396
+ }
397
+ }
398
+ if (previewFailure) {
399
+ throw new Error(`follow preview failed: ${previewFailure.message}`);
400
+ }
401
+ throw new Error(`unknown follow template ${input.providerOrKind}.${input.template}`);
402
+ }
403
+ async ensureFollowSource(input) {
404
+ const existing = this.store.getSourceByKey(input.sourceType, input.sourceKey);
405
+ if (existing) {
406
+ return { source: existing, created: false };
407
+ }
408
+ return {
409
+ source: await this.registerSource(input),
410
+ created: true,
411
+ };
412
+ }
302
413
  async removeSource(sourceId, options = {}) {
303
414
  const source = this.store.getSource(sourceId);
304
415
  if (!source) {
@@ -724,6 +835,38 @@ class AgentInboxService {
724
835
  }
725
836
  return [await this.registerSingleSubscription(input)];
726
837
  }
838
+ async ensureSubscription(input) {
839
+ if (input.shortcut) {
840
+ throw new Error("follow ensureSubscription does not support shortcuts");
841
+ }
842
+ const source = this.store.getSource(input.sourceId);
843
+ if (!source) {
844
+ throw new Error(`unknown source: ${input.sourceId}`);
845
+ }
846
+ const normalized = await this.normalizeSubscriptionRegistrationInput(input);
847
+ const existing = this.store.listSubscriptionsForAgent(input.agentId).find((subscription) => subscription.sourceId === source.sourceId &&
848
+ subscription.startPolicy === normalized.startPolicy &&
849
+ (subscription.startOffset ?? null) === normalized.startOffset &&
850
+ (subscription.startTime ?? null) === normalized.startTime &&
851
+ stableJson(subscription.filter) === stableJson(normalized.filter) &&
852
+ normalizeTrackedResourceRef(subscription.trackedResourceRef) === normalized.trackedResourceRef &&
853
+ stableJson(subscription.cleanupPolicy) === stableJson(normalized.cleanupPolicy));
854
+ if (existing) {
855
+ return { subscription: existing, created: false };
856
+ }
857
+ return {
858
+ subscription: await this.registerSingleSubscription({
859
+ ...input,
860
+ filter: normalized.filter,
861
+ trackedResourceRef: normalized.trackedResourceRef,
862
+ cleanupPolicy: normalized.cleanupPolicy,
863
+ startPolicy: normalized.startPolicy,
864
+ startOffset: normalized.startOffset,
865
+ startTime: normalized.startTime,
866
+ }),
867
+ created: true,
868
+ };
869
+ }
727
870
  async expandShortcutRegistration(input) {
728
871
  if (!input.shortcut) {
729
872
  return null;
@@ -799,8 +942,7 @@ class AgentInboxService {
799
942
  if (!source) {
800
943
  throw new Error(`unknown source: ${input.sourceId}`);
801
944
  }
802
- await (0, filter_1.validateSubscriptionFilter)(input.filter ?? {});
803
- const cleanupPolicy = normalizeCleanupPolicy(input.cleanupPolicy ?? null);
945
+ const normalized = await this.normalizeSubscriptionRegistrationInput(input);
804
946
  const idleState = this.store.getSourceIdleState(source.sourceId);
805
947
  if (idleState?.autoPausedAt) {
806
948
  await this.resumeSource(source.sourceId);
@@ -812,12 +954,12 @@ class AgentInboxService {
812
954
  subscriptionId: (0, util_1.generateCanonicalId)("sub"),
813
955
  agentId: input.agentId,
814
956
  sourceId: input.sourceId,
815
- filter: input.filter ?? {},
816
- trackedResourceRef: normalizeTrackedResourceRef(input.trackedResourceRef),
817
- cleanupPolicy,
818
- startPolicy: input.startPolicy ?? "latest",
819
- startOffset: input.startOffset ?? null,
820
- startTime: input.startTime ?? null,
957
+ filter: normalized.filter,
958
+ trackedResourceRef: normalized.trackedResourceRef,
959
+ cleanupPolicy: normalized.cleanupPolicy,
960
+ startPolicy: normalized.startPolicy,
961
+ startOffset: normalized.startOffset,
962
+ startTime: normalized.startTime,
821
963
  createdAt: (0, util_1.nowIso)(),
822
964
  };
823
965
  this.ensureInboxForAgent(subscription.agentId);
@@ -842,6 +984,22 @@ class AgentInboxService {
842
984
  throw error;
843
985
  }
844
986
  }
987
+ async normalizeSubscriptionRegistrationInput(input) {
988
+ await (0, filter_1.validateSubscriptionFilter)(input.filter ?? {});
989
+ const start = normalizeSubscriptionStartPosition({
990
+ startPolicy: input.startPolicy ?? "latest",
991
+ startOffset: input.startOffset ?? null,
992
+ startTime: input.startTime ?? null,
993
+ });
994
+ return {
995
+ filter: input.filter ?? {},
996
+ trackedResourceRef: normalizeTrackedResourceRef(input.trackedResourceRef),
997
+ cleanupPolicy: normalizeCleanupPolicy(input.cleanupPolicy ?? null),
998
+ startPolicy: start.startPolicy,
999
+ startOffset: start.startOffset,
1000
+ startTime: start.startTime,
1001
+ };
1002
+ }
845
1003
  async removeSubscription(subscriptionId) {
846
1004
  const subscription = this.getSubscription(subscriptionId);
847
1005
  await this.backend.deleteConsumer({ subscriptionId });
@@ -889,18 +1047,16 @@ class AgentInboxService {
889
1047
  }
890
1048
  async resetSubscription(input) {
891
1049
  this.getSubscription(input.subscriptionId);
892
- if (!SUBSCRIPTION_START_POLICIES.has(input.startPolicy)) {
893
- throw new Error(`unsupported start policy: ${input.startPolicy}`);
894
- }
1050
+ const normalizedStart = normalizeSubscriptionStartPosition(input);
895
1051
  const consumer = await this.backend.getConsumer({ subscriptionId: input.subscriptionId });
896
1052
  if (!consumer) {
897
1053
  throw new Error(`unknown consumer for subscription: ${input.subscriptionId}`);
898
1054
  }
899
1055
  const reset = await this.backend.reset({
900
1056
  consumerId: consumer.consumerId,
901
- startPolicy: input.startPolicy,
902
- startOffset: input.startOffset ?? null,
903
- startTime: input.startTime ?? null,
1057
+ startPolicy: normalizedStart.startPolicy,
1058
+ startOffset: normalizedStart.startOffset,
1059
+ startTime: normalizedStart.startTime,
904
1060
  });
905
1061
  return {
906
1062
  subscription: this.getSubscription(input.subscriptionId),
@@ -1474,7 +1630,8 @@ class AgentInboxService {
1474
1630
  const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
1475
1631
  return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
1476
1632
  }
1477
- status() {
1633
+ async status() {
1634
+ const sources = await this.listSourceViews();
1478
1635
  return {
1479
1636
  retention: {
1480
1637
  ackedInboxItemsMs: this.ackedRetentionMs,
@@ -1483,7 +1640,7 @@ class AgentInboxService {
1483
1640
  },
1484
1641
  counts: this.store.getCounts(),
1485
1642
  agents: this.store.listAgents(),
1486
- sources: this.store.listSources(),
1643
+ sources,
1487
1644
  subscriptions: this.store.listSubscriptions(),
1488
1645
  inboxes: this.store.listInboxes(),
1489
1646
  activationTargets: this.store.listActivationTargets(),
@@ -1500,6 +1657,13 @@ class AgentInboxService {
1500
1657
  },
1501
1658
  };
1502
1659
  }
1660
+ async attachSourceRuntimes(sources) {
1661
+ const runtimes = await this.adapters.listSourceRuntimes(sources);
1662
+ return sources.map((source) => ({
1663
+ ...source,
1664
+ runtime: runtimes.get(source.sourceId) ?? null,
1665
+ }));
1666
+ }
1503
1667
  ensureInboxForAgent(agentId) {
1504
1668
  const existing = this.store.getInboxByAgentId(agentId);
1505
1669
  if (existing) {
@@ -2183,6 +2347,7 @@ class AgentInboxService {
2183
2347
  targetKind: target.kind,
2184
2348
  subscriptionIds: input.subscriptionIds,
2185
2349
  sourceIds: input.sourceIds,
2350
+ latestEntryId: latestEntryIdForNotification(input.entries),
2186
2351
  newEntryCount: input.entries.length,
2187
2352
  newItemCount: input.newItemCount,
2188
2353
  summary,
@@ -2315,6 +2480,7 @@ class AgentInboxService {
2315
2480
  totalUnackedCount,
2316
2481
  summary,
2317
2482
  preview,
2483
+ latestEntryId: latestEntryIdForNotification(entries),
2318
2484
  });
2319
2485
  return {
2320
2486
  prompt,
@@ -2766,6 +2932,12 @@ function buildPreviewSource(input) {
2766
2932
  updatedAt: now,
2767
2933
  };
2768
2934
  }
2935
+ function mergeFollowPreviewConfig(config, templateArgs) {
2936
+ return {
2937
+ ...(config ?? {}),
2938
+ ...templateArgs,
2939
+ };
2940
+ }
2769
2941
  function sourceTypeForPreviewRef(sourceRef) {
2770
2942
  if (sourceRef === "local_event" || sourceRef === "remote_source" || sourceRef === "github_repo" || sourceRef === "github_repo_ci" || sourceRef === "feishu_bot") {
2771
2943
  return sourceRef;
@@ -2775,6 +2947,67 @@ function sourceTypeForPreviewRef(sourceRef) {
2775
2947
  }
2776
2948
  throw new Error(`unknown source kind or type for preview: ${sourceRef}`);
2777
2949
  }
2950
+ function followTemplateName(templateId) {
2951
+ const trimmed = templateId.trim();
2952
+ const separator = trimmed.lastIndexOf(".");
2953
+ return separator === -1 ? trimmed : trimmed.slice(separator + 1);
2954
+ }
2955
+ function validateFollowTemplateArgs(argsSchema, args, templateId) {
2956
+ for (const field of argsSchema) {
2957
+ const value = args[field.name];
2958
+ if (field.required && value === undefined) {
2959
+ throw new Error(`follow template ${templateId} requires argument ${field.name}`);
2960
+ }
2961
+ if (value === undefined) {
2962
+ continue;
2963
+ }
2964
+ if (!matchesFollowArgType(value, field.type)) {
2965
+ throw new Error(`follow template ${templateId} expected ${field.name} to be ${field.type}`);
2966
+ }
2967
+ }
2968
+ }
2969
+ function matchesFollowArgType(value, type) {
2970
+ if (type === "string") {
2971
+ return typeof value === "string";
2972
+ }
2973
+ if (type === "number") {
2974
+ return typeof value === "number" && Number.isFinite(value);
2975
+ }
2976
+ if (type === "boolean") {
2977
+ return typeof value === "boolean";
2978
+ }
2979
+ if (type === "string[]") {
2980
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
2981
+ }
2982
+ if (type === "number[]") {
2983
+ return Array.isArray(value) && value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
2984
+ }
2985
+ return true;
2986
+ }
2987
+ function validateExpandedFollowPlan(plan, expectedTemplateId) {
2988
+ if (plan.templateId !== expectedTemplateId) {
2989
+ throw new Error(`follow template ${expectedTemplateId} expanded to unexpected templateId ${plan.templateId}`);
2990
+ }
2991
+ if (plan.sources.length === 0) {
2992
+ throw new Error(`follow template ${plan.templateId} did not produce any sources`);
2993
+ }
2994
+ const logicalNames = new Set();
2995
+ for (const source of plan.sources) {
2996
+ const logicalName = source.logicalName.trim();
2997
+ if (logicalName.length === 0) {
2998
+ throw new Error(`follow template ${plan.templateId} returned an empty source logicalName`);
2999
+ }
3000
+ if (logicalNames.has(logicalName)) {
3001
+ throw new Error(`follow template ${plan.templateId} returned duplicate source logicalName ${logicalName}`);
3002
+ }
3003
+ logicalNames.add(logicalName);
3004
+ }
3005
+ for (const subscription of plan.subscriptions) {
3006
+ if (!logicalNames.has(subscription.sourceLogicalName)) {
3007
+ throw new Error(`follow template ${plan.templateId} references unknown ensured source ${subscription.sourceLogicalName}`);
3008
+ }
3009
+ }
3010
+ }
2778
3011
  function normalizeTrackedResourceRef(value) {
2779
3012
  if (value == null) {
2780
3013
  return null;
@@ -2782,6 +3015,38 @@ function normalizeTrackedResourceRef(value) {
2782
3015
  const trimmed = value.trim();
2783
3016
  return trimmed.length > 0 ? trimmed : null;
2784
3017
  }
3018
+ function normalizeSubscriptionStartPosition(input) {
3019
+ if (!SUBSCRIPTION_START_POLICIES.has(input.startPolicy)) {
3020
+ throw new Error(`unsupported start policy: ${input.startPolicy}`);
3021
+ }
3022
+ const startOffset = input.startOffset ?? null;
3023
+ const startTime = input.startTime ?? null;
3024
+ if (input.startPolicy === "at_offset") {
3025
+ if (startOffset == null || !Number.isInteger(startOffset) || startOffset < 1) {
3026
+ throw new Error("subscriptions requires positive integer startOffset when startPolicy=at_offset");
3027
+ }
3028
+ return {
3029
+ startPolicy: input.startPolicy,
3030
+ startOffset,
3031
+ startTime: null,
3032
+ };
3033
+ }
3034
+ if (input.startPolicy === "at_time") {
3035
+ if (!startTime || !isValidIsoTimestamp(startTime)) {
3036
+ throw new Error("subscriptions requires valid ISO8601 startTime when startPolicy=at_time");
3037
+ }
3038
+ return {
3039
+ startPolicy: input.startPolicy,
3040
+ startOffset: null,
3041
+ startTime: canonicalIsoTimestamp(startTime),
3042
+ };
3043
+ }
3044
+ return {
3045
+ startPolicy: input.startPolicy,
3046
+ startOffset: null,
3047
+ startTime: null,
3048
+ };
3049
+ }
2785
3050
  function normalizeCleanupPolicy(input) {
2786
3051
  if (!input) {
2787
3052
  return { mode: "manual" };
@@ -2828,6 +3093,23 @@ function normalizeGracePeriodSecs(value) {
2828
3093
  }
2829
3094
  return value;
2830
3095
  }
3096
+ function stableJson(value) {
3097
+ return JSON.stringify(sortJsonValue(value));
3098
+ }
3099
+ function sortJsonValue(value) {
3100
+ if (Array.isArray(value)) {
3101
+ return value.map((entry) => sortJsonValue(entry));
3102
+ }
3103
+ if (!value || typeof value !== "object") {
3104
+ return value;
3105
+ }
3106
+ return Object.keys(value)
3107
+ .sort()
3108
+ .reduce((acc, key) => {
3109
+ acc[key] = sortJsonValue(value[key]);
3110
+ return acc;
3111
+ }, {});
3112
+ }
2831
3113
  function normalizeLifecycleSignal(signal, fallbackOccurredAt) {
2832
3114
  const ref = typeof signal.ref === "string" ? signal.ref.trim() : "";
2833
3115
  if (ref.length === 0) {
@@ -3130,6 +3412,9 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
3130
3412
  }
3131
3413
  return `${newItemCount} new ${itemWord} in ${inboxId}`;
3132
3414
  }
3415
+ function latestEntryIdForNotification(entries) {
3416
+ return entries.length > 0 ? entries[entries.length - 1]?.entryId ?? null : null;
3417
+ }
3133
3418
  function latestSummary(entries) {
3134
3419
  for (let index = entries.length - 1; index >= 0; index -= 1) {
3135
3420
  const summary = entries[index]?.summary;
@@ -65,6 +65,9 @@ async function resolveSourceSchema(source, context = {}) {
65
65
  shortcuts: typeof module?.listSubscriptionShortcuts === "function"
66
66
  ? module.listSubscriptionShortcuts(moduleInputSource(source))
67
67
  : [],
68
+ followTemplates: typeof module?.listFollowTemplates === "function"
69
+ ? module.listFollowTemplates(moduleInputSource(source))
70
+ : undefined,
68
71
  }
69
72
  : undefined);
70
73
  }
@@ -87,6 +90,13 @@ function withResolvedIdentity(sourceId, schema, identity, capabilityMetadata) {
87
90
  shortcuts: capabilityMetadata.shortcuts ?? [],
88
91
  },
89
92
  } : {}),
93
+ ...(capabilityMetadata?.followTemplates && capabilityMetadata.followTemplates.length > 0
94
+ ? {
95
+ followSchema: {
96
+ templates: capabilityMetadata.followTemplates,
97
+ },
98
+ }
99
+ : {}),
90
100
  };
91
101
  }
92
102
  function moduleInputSource(source) {
@@ -88,6 +88,7 @@ const SOURCE_SCHEMAS = {
88
88
  { name: "conclusion", type: "string|null", description: "Workflow run conclusion when completed." },
89
89
  { name: "event", type: "string|null", description: "GitHub trigger event for the workflow run." },
90
90
  { name: "headBranch", type: "string|null", description: "Head branch for the workflow run." },
91
+ { name: "headRepositoryFullName", type: "string|null", description: "Head repository full name for the workflow run when available." },
91
92
  { name: "headSha", type: "string|null", description: "Head commit SHA for the workflow run." },
92
93
  { name: "pullRequestNumbers", type: "number[]", description: "Pull request numbers associated with the workflow run." },
93
94
  { name: "actor", type: "string|null", description: "Actor login for the workflow run." },
@@ -102,6 +103,7 @@ const SOURCE_SCHEMAS = {
102
103
  conclusion: "failure",
103
104
  event: "pull_request",
104
105
  head_branch: "main",
106
+ head_repository: { full_name: "holon-run/agentinbox" },
105
107
  pull_requests: [{ number: 93 }],
106
108
  actor: "jolestar",
107
109
  head_commit: { message: "fix ci" },