agenthooksprotocol 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +41 -0
  2. package/dist/src/client/composition.d.ts +2 -0
  3. package/dist/src/client/composition.d.ts.map +1 -1
  4. package/dist/src/client/composition.js +14 -6
  5. package/dist/src/client/composition.js.map +1 -1
  6. package/dist/src/client/content.d.ts +58 -20
  7. package/dist/src/client/content.d.ts.map +1 -1
  8. package/dist/src/client/content.js +358 -105
  9. package/dist/src/client/content.js.map +1 -1
  10. package/dist/src/client/hooks.d.ts +4 -0
  11. package/dist/src/client/hooks.d.ts.map +1 -1
  12. package/dist/src/client/hooks.js +162 -54
  13. package/dist/src/client/hooks.js.map +1 -1
  14. package/dist/src/client/index.d.ts +1 -0
  15. package/dist/src/client/index.d.ts.map +1 -1
  16. package/dist/src/client/index.js +1 -0
  17. package/dist/src/client/index.js.map +1 -1
  18. package/dist/src/client/types.d.ts +6 -3
  19. package/dist/src/client/types.d.ts.map +1 -1
  20. package/dist/src/client/types.js.map +1 -1
  21. package/dist/src/content-upload.d.ts +9 -10
  22. package/dist/src/content-upload.d.ts.map +1 -1
  23. package/dist/src/content-upload.js +8 -4
  24. package/dist/src/content-upload.js.map +1 -1
  25. package/dist/src/draft/generated.d.ts +73 -47
  26. package/dist/src/draft/generated.d.ts.map +1 -1
  27. package/dist/src/draft/generated.js +502 -251
  28. package/dist/src/draft/generated.js.map +1 -1
  29. package/dist/src/draft/index.d.ts +4 -1
  30. package/dist/src/draft/index.d.ts.map +1 -1
  31. package/dist/src/draft/index.js +2 -0
  32. package/dist/src/draft/index.js.map +1 -1
  33. package/dist/src/draft/schemas.d.ts +112 -40
  34. package/dist/src/draft/schemas.d.ts.map +1 -1
  35. package/dist/src/draft/schemas.js +158 -120
  36. package/dist/src/draft/schemas.js.map +1 -1
  37. package/dist/src/draft-runtime.d.ts +5 -5
  38. package/dist/src/draft-runtime.d.ts.map +1 -1
  39. package/dist/src/draft-runtime.js +3 -5
  40. package/dist/src/draft-runtime.js.map +1 -1
  41. package/dist/src/server/attachments.d.ts +2 -2
  42. package/dist/src/server/attachments.d.ts.map +1 -1
  43. package/dist/src/server/attachments.js +3 -3
  44. package/dist/src/server/attachments.js.map +1 -1
  45. package/dist/src/server/hooks.d.ts.map +1 -1
  46. package/dist/src/server/hooks.js +4 -2
  47. package/dist/src/server/hooks.js.map +1 -1
  48. package/dist/src/server/index.d.ts +3 -1
  49. package/dist/src/server/index.d.ts.map +1 -1
  50. package/dist/src/server/index.js +1 -0
  51. package/dist/src/server/index.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/client/composition.ts +15 -6
  54. package/src/client/content.ts +350 -113
  55. package/src/client/hooks.ts +188 -60
  56. package/src/client/index.ts +2 -0
  57. package/src/client/types.ts +6 -3
  58. package/src/content-upload.ts +14 -13
  59. package/src/draft/ahp-codegen.lock.json +14 -6
  60. package/src/draft/generated.ts +733 -427
  61. package/src/draft/index.ts +6 -1
  62. package/src/draft/schemas.ts +160 -120
  63. package/src/draft-runtime.ts +7 -9
  64. package/src/server/attachments.ts +5 -5
  65. package/src/server/hooks.ts +3 -2
  66. package/src/server/index.ts +5 -0
@@ -23,7 +23,7 @@ import {
23
23
  validateInterceptResponse,
24
24
  } from "../draft/index.js";
25
25
  import { BackendTransport } from "./transport.js";
26
- import { ContentManager, ContentSource } from "./content.js";
26
+ import { Attachment, ContentManager, ContentSource } from "./content.js";
27
27
  import { composeResponseAsync, normalizeEffects } from "./composition.js";
28
28
  import { auth } from "./auth.js";
29
29
  import { validateWire } from "./validation.js";
@@ -107,6 +107,12 @@ const matches = (selector: string, type: string) =>
107
107
  selector === type ||
108
108
  (selector.endsWith(".*") && selector.slice(0, -2) === type.split(".")[0]);
109
109
 
110
+ type ElicitationLifetime = {
111
+ id: string;
112
+ sessionId: string | undefined;
113
+ retired: boolean;
114
+ };
115
+
110
116
  /** Configuration-driven, harness-facing AHP client. No host operation is executed. */
111
117
  export class Hooks {
112
118
  readonly initialized: Promise<void>;
@@ -121,9 +127,12 @@ export class Hooks {
121
127
  private closed?: Promise<void>;
122
128
  private readonly elicitations = new Map<
123
129
  string,
124
- { event: any; bytes: Uint8Array }
130
+ { event: any; owner: Attachment; size: number; release: () => Promise<void> }
125
131
  >();
126
132
  private elicitationBytes = 0;
133
+ // Tokens exist only for active calls, including preparation before any bytes
134
+ // can be retained. Retirement invalidates those calls without tombstones.
135
+ private readonly activeElicitations = new Set<ElicitationLifetime>();
127
136
 
128
137
  constructor(config: unknown, options: HooksOptions) {
129
138
  this.options = {
@@ -431,23 +440,64 @@ export class Hooks {
431
440
  : 64 * 1024 * 1024,
432
441
  allowLoopback: true,
433
442
  });
434
- manager.own(input);
435
- manager.own(options.contentSources);
443
+ try {
444
+ manager.own([input, options.contentSources]);
445
+ } catch (error) {
446
+ await manager.close().catch(() => {});
447
+ throw error;
448
+ }
436
449
  this.managers.add(manager);
437
450
  const signal = combineSignals(this.lifetime.signal, options.signal);
451
+ const exchange: ElicitationLifetime | undefined =
452
+ type === "user.elicitation.request"
453
+ ? {
454
+ id: input.id ?? randomUUID(),
455
+ sessionId: (input as any).session?.id,
456
+ retired: false,
457
+ }
458
+ : undefined;
459
+ if (exchange) this.activeElicitations.add(exchange);
460
+ let completed = false;
461
+ let returnedContent: BoundaryResult<K>["content"];
462
+
438
463
  const work = (async () => {
439
464
  // Calls after close are misuse. Active calls return interruption evidence.
440
465
  this.lifetime.signal.throwIfAborted();
441
466
  await this.initialized;
442
- return this.run(type, input, options, signal, manager);
467
+ return this.run(type, input, options, signal, manager, exchange);
443
468
  })();
444
469
  this.pending.add(work);
445
470
  try {
446
- return await work;
471
+ const result = await work;
472
+ completed = true;
473
+ if (result.content && !result.interrupted) {
474
+ manager.transfer(result.content);
475
+ returnedContent = result.content;
476
+ }
477
+ return result;
447
478
  } finally {
448
- await manager.close();
449
- this.managers.delete(manager);
450
- this.pending.delete(work);
479
+ // Terminal exchanges retire even when validation or cancellation bypasses run.
480
+ if (type === "user.elicitation.result")
481
+ this.forgetElicitation((input as any).parentEventId);
482
+ if (
483
+ type === "user.elicitation.request" &&
484
+ (!completed || signal.aborted) &&
485
+ input.id
486
+ )
487
+ this.forgetElicitation(input.id);
488
+ if (type === "session.end")
489
+ this.retireSessionElicitations((input as any).session?.id);
490
+ if (exchange) this.activeElicitations.delete(exchange);
491
+ try {
492
+ if (completed) await manager.close();
493
+ else await manager.close().catch(() => {});
494
+ } catch (error) {
495
+ await returnedContent?.close().catch(() => {});
496
+ throw error;
497
+ } finally {
498
+ this.managers.delete(manager);
499
+ this.pending.delete(work);
500
+ }
451
501
  }
452
502
  }
453
503
 
@@ -457,12 +507,13 @@ export class Hooks {
457
507
  options: BoundaryOptions,
458
508
  signal: AbortSignal,
459
509
  manager: ContentManager,
510
+ exchange?: ElicitationLifetime,
460
511
  ): Promise<BoundaryResult<K>> {
461
512
  let event: any = cloneInput({
462
513
  ...input,
463
514
  type,
464
515
  source: this.options.source,
465
- id: input.id ?? randomUUID(),
516
+ id: exchange?.id ?? input.id ?? randomUUID(),
466
517
  time: input.time ?? new Date().toISOString(),
467
518
  });
468
519
  bindContentSources(type, event, options.contentSources);
@@ -494,7 +545,7 @@ export class Hooks {
494
545
  };
495
546
  if (signal.aborted)
496
547
  return {
497
- event,
548
+ event: await resultEvent(event, manager),
498
549
  response: {
499
550
  jsonrpc: "2.0",
500
551
  id: requestId,
@@ -512,7 +563,10 @@ export class Hooks {
512
563
  observations: Promise.resolve([]),
513
564
  interrupted: true,
514
565
  } as BoundaryResult<K>;
515
- const originalRequest = this.elicitationFor(event);
566
+ const localSources = new Set<ReadableStream<Uint8Array>>();
567
+ let correlation: { event: Event; owner: Attachment; release: () => Promise<void> } | undefined;
568
+ let originalRequest: Event | undefined;
569
+
516
570
  let effects: Effect[] = [];
517
571
  let state = structuredClone(options.initialState);
518
572
  // A supplied snapshot is input to the first receiver, not a newly accepted
@@ -559,6 +613,8 @@ export class Hooks {
559
613
  advertised.modes.includes(r.subscription.mode),
560
614
  );
561
615
  try {
616
+ correlation = this.elicitationFor(event, manager);
617
+ originalRequest = correlation?.event;
562
618
  // Validate a metadata-only view before any delivery (this does not consume bytes).
563
619
  const metadata = await manager.prepare(
564
620
  event,
@@ -632,11 +688,8 @@ export class Hooks {
632
688
  projected.elicitation?.request?.selection === "body" &&
633
689
  event.elicitation?.request?.body instanceof ReadableStream
634
690
  ) {
635
- const bytes = await manager.readBody(
636
- event.elicitation.request.body,
637
- signal,
638
- );
639
- this.rememberElicitation(event, bytes);
691
+ const retained = await manager.retainBody(event.elicitation.request.body, signal);
692
+ this.rememberElicitation(event, retained, exchange!);
640
693
  }
641
694
  const request = {
642
695
  jsonrpc: "2.0",
@@ -691,7 +744,13 @@ export class Hooks {
691
744
  );
692
745
  const staged = await raceAbort(
693
746
  composeResponseAsync(event, effects, decoded.value, caps, {
694
- readContent: (body) => manager.readBody(body, deadline),
747
+ readContent: (body) => originalRequest && body === (originalRequest as any).elicitation.request.body
748
+ ? correlation!.owner.read(deadline)
749
+ : manager.readBody(body, deadline),
750
+ ownContent: (body) => {
751
+ manager.own(body);
752
+ localSources.add(body);
753
+ },
695
754
  selectedEvent: projected,
696
755
  state,
697
756
  ...(originalRequest
@@ -771,40 +830,34 @@ export class Hooks {
771
830
  const jobs = observations.map((route) =>
772
831
  this.observe(route, snapshot, manager, signal),
773
832
  );
774
- observed = Promise.all(jobs)
775
- .then((results) =>
776
- results.filter(
777
- (result): result is DeliveryError => result !== undefined,
778
- ),
779
- )
780
- .finally(async () => {
781
- await manager.close();
782
- this.managers.delete(manager);
783
- });
833
+ observed = Promise.all(jobs).then((results) =>
834
+ results.filter(
835
+ (result): result is DeliveryError => result !== undefined,
836
+ ),
837
+ );
784
838
  await observed;
785
- } else {
786
- await manager.close();
787
- this.managers.delete(manager);
788
839
  }
789
840
  interrupted ||= signal.aborted;
790
841
  if (type === "user.elicitation.result")
791
842
  this.forgetElicitation(event.parentEventId);
792
843
  if (
793
844
  type === "user.elicitation.request" &&
794
- (interrupted || effects.some((e) => e.type === "deny"))
845
+ (interrupted ||
846
+ errors.length > 0 ||
847
+ (await observed).length > 0 ||
848
+ effects.some((e) => e.type === "deny"))
795
849
  )
796
850
  this.forgetElicitation(event.id);
797
851
  if (type === "session.end")
798
- for (const [id, entry] of this.elicitations)
799
- if (entry.event.session?.id === event.session?.id)
800
- this.forgetElicitation(id);
852
+ this.retireSessionElicitations(event.session?.id);
801
853
  const response = {
802
854
  jsonrpc: "2.0",
803
855
  id: requestId,
804
856
  result: { protocolVersion: "draft", effects },
805
857
  } as InterceptResponse;
806
858
  return {
807
- event,
859
+ event: await resultEvent(event, manager, localSources),
860
+ content: manager.resultContent(event),
808
861
  response,
809
862
  state: snapshotState(state),
810
863
  get permission() {
@@ -819,9 +872,13 @@ export class Hooks {
819
872
  interrupted,
820
873
  } as BoundaryResult<K>;
821
874
  } catch (error) {
822
- await manager.close();
875
+ if (type === "user.elicitation.request")
876
+ this.forgetElicitation(requestId);
877
+ await manager.close().catch(() => {});
823
878
  this.managers.delete(manager);
824
879
  throw error;
880
+ } finally {
881
+ await correlation?.release().catch(() => {});
825
882
  }
826
883
  }
827
884
 
@@ -887,44 +944,79 @@ export class Hooks {
887
944
  }
888
945
  }
889
946
 
890
- private rememberElicitation(event: any, bytes: Uint8Array): void {
891
- if (this.elicitations.has(event.id)) return;
947
+ private rememberElicitation(
948
+ event: any,
949
+ retained: { owner: Attachment; size: number; release: () => Promise<void> },
950
+ exchange: ElicitationLifetime,
951
+ ): void {
952
+ if (exchange.retired || this.lifetime.signal.aborted || this.elicitations.has(event.id)) {
953
+ void retained.release().catch(() => {});
954
+ return;
955
+ }
892
956
  const limit = this.options.maxContentBytes ?? 64 * 1024 * 1024;
893
- if (bytes.byteLength > limit)
957
+ if (retained.size > limit) {
958
+ void retained.release().catch(() => {});
894
959
  throw new Error("Elicitation request exceeds retention budget");
895
- while (
896
- this.elicitationBytes + bytes.byteLength > limit ||
960
+ }
961
+ if (
962
+ this.elicitationBytes + retained.size > limit ||
897
963
  this.elicitations.size >= 128
898
- )
899
- this.forgetElicitation(this.elicitations.keys().next().value!);
900
- const stored = cloneInput(event);
901
- delete stored.elicitation.request.body;
902
- this.elicitations.set(event.id, { event: stored, bytes });
903
- this.elicitationBytes += bytes.byteLength;
964
+ ) {
965
+ void retained.release().catch(() => {});
966
+ throw new Error("Active elicitation exchanges exceed retention budget");
967
+ }
968
+ // Pending exchanges retain correlation facts and a lease on the same owner.
969
+ const stored = {
970
+ type: event.type,
971
+ id: event.id,
972
+ source: event.source,
973
+ session: event.session ? { id: event.session.id } : undefined,
974
+ elicitation: {
975
+ mode: event.elicitation.mode,
976
+ server: event.elicitation.server,
977
+ request: { mediaType: event.elicitation.request.mediaType },
978
+ },
979
+ };
980
+ this.elicitations.set(event.id, { event: stored, ...retained });
981
+ this.elicitationBytes += retained.size;
904
982
  }
905
- private elicitationFor(event: any): Event | undefined {
983
+ private elicitationFor(event: any, manager: ContentManager): {
984
+ event: Event; owner: Attachment; release: () => Promise<void>;
985
+ } | undefined {
906
986
  if (event.type !== "user.elicitation.result") return undefined;
907
987
  const entry = this.elicitations.get(event.parentEventId);
908
988
  if (!entry) return undefined;
909
- const copy = cloneInput(entry.event),
910
- bytes = entry.bytes.slice();
911
- copy.elicitation.request.body = new ReadableStream<Uint8Array>({
912
- start(c) {
913
- c.enqueue(bytes);
914
- c.close();
915
- },
916
- });
917
- return copy;
989
+ const copy = cloneInput(entry.event);
990
+ copy.elicitation.request.body = entry.owner.stream;
991
+ return { event: copy, owner: entry.owner, release: manager.lease(entry.owner) };
992
+ }
993
+ /** End an abandoned host elicitation exchange without delivering a result. */
994
+ discardElicitation(requestEventId: string): void {
995
+ this.forgetElicitation(requestEventId);
996
+ }
997
+
998
+ private retireSessionElicitations(sessionId: string | undefined): void {
999
+ for (const exchange of this.activeElicitations)
1000
+ if (exchange.sessionId === sessionId) exchange.retired = true;
1001
+ for (const [id, entry] of this.elicitations)
1002
+ if (entry.event.session?.id === sessionId) this.forgetElicitation(id);
918
1003
  }
1004
+
919
1005
  private forgetElicitation(id: string): void {
1006
+ for (const exchange of this.activeElicitations)
1007
+ if (exchange.id === id) exchange.retired = true;
920
1008
  const entry = this.elicitations.get(id);
921
- if (entry) this.elicitationBytes -= entry.bytes.byteLength;
1009
+ if (entry) {
1010
+ this.elicitationBytes -= entry.size;
1011
+ void entry.release().catch(() => {});
1012
+ }
922
1013
  this.elicitations.delete(id);
923
1014
  }
924
1015
 
925
1016
  /** Cancel pending work and release SDK-owned resources; injected stores remain owned by the host. */
926
1017
  close(): Promise<void> {
927
1018
  if (!this.closed) {
1019
+ for (const exchange of this.activeElicitations) exchange.retired = true;
928
1020
  this.lifetime.abort(new Error("Hooks closed"));
929
1021
  this.closed = (async () => {
930
1022
  await this.initialized.catch(() => {});
@@ -936,6 +1028,7 @@ export class Hooks {
936
1028
  await Promise.allSettled([...this.managers].map((m) => m.close()));
937
1029
  await Promise.allSettled([...this.pending]);
938
1030
  this.authContexts.clear();
1031
+ await Promise.allSettled([...this.elicitations.values()].map(entry => entry.release()));
939
1032
  this.elicitations.clear();
940
1033
  this.elicitationBytes = 0;
941
1034
  })();
@@ -1150,6 +1243,39 @@ export class Hooks {
1150
1243
  }
1151
1244
  }
1152
1245
 
1246
+ // Return independent copies of prepared or synthesized payloads. Never read an
1247
+ // unselected producer stream solely to construct a result.
1248
+ async function resultEvent(
1249
+ value: any,
1250
+ manager: ContentManager,
1251
+ localSources = new Set<ReadableStream<Uint8Array>>(),
1252
+ ): Promise<any> {
1253
+ if (value instanceof ContentSource) value = value.stream;
1254
+ if (value instanceof ReadableStream) {
1255
+ return manager.resultBody(value, localSources.has(value));
1256
+ }
1257
+ if (Array.isArray(value))
1258
+ return Promise.all(
1259
+ value.map((child) => resultEvent(child, manager, localSources)),
1260
+ );
1261
+ if (value && typeof value === "object") {
1262
+ const entries = await Promise.all(
1263
+ Object.entries(value).map(async ([key, child]) =>
1264
+ [key, await resultEvent(child, manager, localSources)] as const),
1265
+ );
1266
+ const copy = Object.fromEntries(
1267
+ entries.filter(([, child]) => child !== undefined),
1268
+ );
1269
+ if (
1270
+ (value.body instanceof ReadableStream || value.body instanceof ContentSource) &&
1271
+ !copy.body
1272
+ )
1273
+ copy.selection = "metadata";
1274
+ return copy;
1275
+ }
1276
+ return value;
1277
+ }
1278
+
1153
1279
  function cloneInput(value: any): any {
1154
1280
  if (value instanceof ContentSource) return value.stream;
1155
1281
  if (value instanceof ReadableStream) return value;
@@ -1217,13 +1343,15 @@ function combineSignals(first: AbortSignal, second?: AbortSignal): AbortSignal {
1217
1343
  return second ? AbortSignal.any([first, second]) : first;
1218
1344
  }
1219
1345
  function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
1220
- if (signal.aborted) return Promise.reject(signal.reason);
1221
1346
  return new Promise((resolve, reject) => {
1222
1347
  const abort = () => reject(signal.reason);
1223
1348
  signal.addEventListener("abort", abort, { once: true });
1224
1349
  promise
1225
1350
  .then(resolve, reject)
1226
1351
  .finally(() => signal.removeEventListener("abort", abort));
1352
+ // The operation already exists: own its rejection even if cancellation
1353
+ // occurred synchronously while constructing it.
1354
+ if (signal.aborted) abort();
1227
1355
  });
1228
1356
  }
1229
1357
 
@@ -26,3 +26,5 @@ export {
26
26
  CapabilityBuilder,
27
27
  } from "../draft/generated.js";
28
28
  export type * from "../draft/generated.js";
29
+
30
+ export { effectNames, supports } from "../draft/generated.js";
@@ -5,19 +5,19 @@ import type {
5
5
  ContentSourceBinding,
6
6
  DeliveryDiagnosticCode,
7
7
  } from "../draft/generated.js";
8
- import type { ContentSource } from "./content.js";
8
+ import type { AttachmentContent, ContentSource } from "./content.js";
9
9
  import type {
10
10
  Capabilities,
11
11
  ContentReference,
12
12
  ExecutionEventContextCompactBefore,
13
13
  InterceptResponse,
14
14
  InterceptRequest,
15
- ObserveNotification,
15
+ Event,
16
16
  StaticCapabilityManifest,
17
17
  } from "../draft/generated.js";
18
18
  import type { AuthProvider, DeliveryAuthProvider } from "./auth.js";
19
19
 
20
- export type Event = ObserveNotification["params"]["event"];
20
+ export type { Event } from "../draft/generated.js";
21
21
  export type { EventType } from "../draft/generated.js";
22
22
  /** Remove generated JSON extension index signatures while preserving named fields. */
23
23
  type Fields<T> = {
@@ -139,6 +139,9 @@ export type BoundaryState = ReadonlyState<
139
139
  NonNullable<InterceptRequest["params"]["state"]>
140
140
  >;
141
141
  export interface BoundaryResult<K extends EventType = EventType> {
142
+ /** Present for owned Attachments. Effective local bodies by content item id,
143
+ * including unread lazy sources. Close explicitly, independently of Hooks. */
144
+ readonly content?: AttachmentContent;
142
145
  /** Accepted effective input, with occurrence identity and static envelope supplied.
143
146
  * Body streams are owned delivery resources, not reusable output streams. */
144
147
  event: HarnessEvent<K>;
@@ -1,3 +1,5 @@
1
+ import type { ContentReference, ContentUploadReceipt } from "./draft/generated.js";
2
+ export type { ContentReference, ContentUploadReceipt } from "./draft/generated.js";
1
3
  /** Normative HTTP content upload. Event authentication is deliberately not an input. */
2
4
  export interface UploadConfiguration {
3
5
  endpoint: string;
@@ -6,17 +8,16 @@ export interface UploadConfiguration {
6
8
  maxBytes?: number;
7
9
  [key: string]: unknown;
8
10
  }
9
- export interface BodyReference {
10
- ref: string;
11
- size: number;
12
- sha256: string;
11
+ /** Explicitly discard upload confirmation metadata before publishing a reference. */
12
+ export function contentReference(receipt: ContentUploadReceipt): ContentReference {
13
+ return { ref: receipt.ref };
13
14
  }
14
- /** Check the receiver descriptor before exposing a body reference. */
15
- export function validateBodyReference(
15
+ /** Verify the upload receipt against the exact sent bytes before publication. */
16
+ export function validateUploadReceipt(
16
17
  value: unknown,
17
18
  size: number,
18
19
  sha256: string,
19
- ): BodyReference {
20
+ ): ContentUploadReceipt {
20
21
  if (!value || typeof value !== "object" || Array.isArray(value))
21
22
  throw Error("Invalid upload descriptor");
22
23
  const body = value as Record<string, unknown>;
@@ -40,7 +41,7 @@ export async function uploadContent(
40
41
  resolveToken?: (name: string) => string | undefined;
41
42
  allowLoopback?: boolean;
42
43
  } = {},
43
- ): Promise<BodyReference> {
44
+ ): Promise<ContentUploadReceipt> {
44
45
  if (
45
46
  !config ||
46
47
  typeof config.endpoint !== "string" ||
@@ -107,7 +108,7 @@ export async function uploadContent(
107
108
  await response.body?.cancel();
108
109
  throw Error(`Content upload failed: HTTP ${response.status}`);
109
110
  }
110
- return validateBodyReference(await response.json(), snapshot.length, sha256);
111
+ return validateUploadReceipt(await response.json(), snapshot.length, sha256);
111
112
  }
112
113
  export interface NormalizedContentInput {
113
114
  id: string;
@@ -129,7 +130,7 @@ export interface NormalizedContentView {
129
130
  parentItemId?: string;
130
131
  synthesized?: boolean;
131
132
  selection: "body" | "metadata" | "omit";
132
- body?: BodyReference;
133
+ body?: ContentReference;
133
134
  gap?: { reason: string };
134
135
  }
135
136
  /** Select once per subscriber. Return no body reference until upload confirms availability. */
@@ -139,7 +140,7 @@ export async function prepareWireContent(
139
140
  subscription?: string;
140
141
  selection: Record<string, "body" | "metadata" | "omit">;
141
142
  authorized: (item: NormalizedContentInput) => boolean;
142
- upload: (bytes: Uint8Array) => Promise<BodyReference>;
143
+ upload: (bytes: Uint8Array) => Promise<ContentUploadReceipt>;
143
144
  mode: "intercept" | "observe";
144
145
  failClosed: boolean;
145
146
  timeoutMs?: number;
@@ -174,7 +175,7 @@ export async function prepareWireContent(
174
175
  try {
175
176
  const bytes = new Uint8Array(item.bytes);
176
177
  let timer: ReturnType<typeof setTimeout> | undefined;
177
- let body: BodyReference;
178
+ let body: ContentUploadReceipt;
178
179
  try {
179
180
  body = await Promise.race([
180
181
  options.upload(bytes.slice()),
@@ -192,7 +193,7 @@ export async function prepareWireContent(
192
193
  new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
193
194
  (byte) => byte.toString(16).padStart(2, "0"),
194
195
  ).join("");
195
- view.body = validateBodyReference(body, bytes.length, hash);
196
+ view.body = contentReference(validateUploadReceipt(body, bytes.length, hash));
196
197
  } catch {
197
198
  view.gap = { reason: "transfer_failed" };
198
199
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "sourceRepository": "agenthooksprotocol/agent-hooks-protocol",
3
- "sourceCommit": "472c16e97cfd456d98cefa0c2a3d23df2f57dbcf",
3
+ "sourceCommit": "05b4750cb2dccf17d75236182d6971f3cbe97e1f",
4
4
  "schemaRevision": "draft",
5
5
  "protocolVersion": "draft",
6
6
  "generatorVersion": "0.1.0",
7
7
  "language": "typescript",
8
- "schemaManifestSha256": "050852ec662d5a29c80d8951252b54a5fa48bb5e6c7ce08d88c45e614964a60a",
8
+ "schemaManifestSha256": "b397a3c4d613a67396f1727190052ce2f3bb208cdfa03e066f0c045525f25451",
9
9
  "documents": [
10
10
  {
11
11
  "path": "schema/draft/capabilities-request.schema.json",
@@ -29,16 +29,20 @@
29
29
  },
30
30
  {
31
31
  "path": "schema/draft/content-item.schema.json",
32
- "sha256": "ef15f47f3f57f3004e89046a79429944b5361e302719f4f45bbf819667a13a38"
32
+ "sha256": "21ce30d451d6fe20eb56cf09108fe33500e40839669d2922911dd9b0b8cfe762"
33
33
  },
34
34
  {
35
35
  "path": "schema/draft/content-reference.schema.json",
36
- "sha256": "24221c52d428c6104ce6f526f278d000169777f866adbc2c2de90a635469d692"
36
+ "sha256": "32b345818648f2d28dd0175705a85b22569ea535867349262e055bb3e3999e87"
37
37
  },
38
38
  {
39
39
  "path": "schema/draft/content-selection.schema.json",
40
40
  "sha256": "fe57bce4a7a6c7a8d51728634349621ba327b5ef118bc60b1c330260392e2a62"
41
41
  },
42
+ {
43
+ "path": "schema/draft/content-upload-receipt.schema.json",
44
+ "sha256": "02e04375b2e4764eea0e70299a9ae29f0d49b407dca797f49299a3cb2d7613dc"
45
+ },
42
46
  {
43
47
  "path": "schema/draft/content-upload.schema.json",
44
48
  "sha256": "a405adb54b8de11f3814840f7cbcd54d76cdcc2e25d00942f1c1755eb05c6753"
@@ -51,6 +55,10 @@
51
55
  "path": "schema/draft/effect.schema.json",
52
56
  "sha256": "1448151926563244194ed56b15e3074ddfc8ec8c10889f2e42ab49d66be2d031"
53
57
  },
58
+ {
59
+ "path": "schema/draft/event.schema.json",
60
+ "sha256": "7b19aa60dc775b23b74d78aad2cdc634e07c454c68f1a60b125c3e4ff391f1f9"
61
+ },
54
62
  {
55
63
  "path": "schema/draft/execution-event.schema.json",
56
64
  "sha256": "d9b13cf28e20eced629cfe8eb822eb6dfb2ac453fe14e6d2334c2f4b2793512a"
@@ -73,7 +81,7 @@
73
81
  },
74
82
  {
75
83
  "path": "schema/draft/intercept-request.schema.json",
76
- "sha256": "fbd5ed20681d918c8647382de6a2bc07de14a7ac57f756e68d2f3293043d534e"
84
+ "sha256": "179c6afd7092bcb1fa91522e6fbe6f63aa28d5dfbef35c9aa661304028ba48c9"
77
85
  },
78
86
  {
79
87
  "path": "schema/draft/intercept-response.schema.json",
@@ -89,7 +97,7 @@
89
97
  },
90
98
  {
91
99
  "path": "schema/draft/observe-notification.schema.json",
92
- "sha256": "27255ccd1a6d0fa788161825f4df74f7e0b3c074a8dc7c8cdeb7e07df11d55cf"
100
+ "sha256": "770dd91480b4a0a0e8a3565e3724ab5d77bbede19faff223e10a8e442f926a62"
93
101
  },
94
102
  {
95
103
  "path": "schema/draft/registration.schema.json",