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.
- package/README.md +41 -0
- package/dist/src/client/composition.d.ts +2 -0
- package/dist/src/client/composition.d.ts.map +1 -1
- package/dist/src/client/composition.js +14 -6
- package/dist/src/client/composition.js.map +1 -1
- package/dist/src/client/content.d.ts +58 -20
- package/dist/src/client/content.d.ts.map +1 -1
- package/dist/src/client/content.js +358 -105
- package/dist/src/client/content.js.map +1 -1
- package/dist/src/client/hooks.d.ts +4 -0
- package/dist/src/client/hooks.d.ts.map +1 -1
- package/dist/src/client/hooks.js +162 -54
- package/dist/src/client/hooks.js.map +1 -1
- package/dist/src/client/index.d.ts +1 -0
- package/dist/src/client/index.d.ts.map +1 -1
- package/dist/src/client/index.js +1 -0
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/client/types.d.ts +6 -3
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/client/types.js.map +1 -1
- package/dist/src/content-upload.d.ts +9 -10
- package/dist/src/content-upload.d.ts.map +1 -1
- package/dist/src/content-upload.js +8 -4
- package/dist/src/content-upload.js.map +1 -1
- package/dist/src/draft/generated.d.ts +73 -47
- package/dist/src/draft/generated.d.ts.map +1 -1
- package/dist/src/draft/generated.js +502 -251
- package/dist/src/draft/generated.js.map +1 -1
- package/dist/src/draft/index.d.ts +4 -1
- package/dist/src/draft/index.d.ts.map +1 -1
- package/dist/src/draft/index.js +2 -0
- package/dist/src/draft/index.js.map +1 -1
- package/dist/src/draft/schemas.d.ts +112 -40
- package/dist/src/draft/schemas.d.ts.map +1 -1
- package/dist/src/draft/schemas.js +158 -120
- package/dist/src/draft/schemas.js.map +1 -1
- package/dist/src/draft-runtime.d.ts +5 -5
- package/dist/src/draft-runtime.d.ts.map +1 -1
- package/dist/src/draft-runtime.js +3 -5
- package/dist/src/draft-runtime.js.map +1 -1
- package/dist/src/server/attachments.d.ts +2 -2
- package/dist/src/server/attachments.d.ts.map +1 -1
- package/dist/src/server/attachments.js +3 -3
- package/dist/src/server/attachments.js.map +1 -1
- package/dist/src/server/hooks.d.ts.map +1 -1
- package/dist/src/server/hooks.js +4 -2
- package/dist/src/server/hooks.js.map +1 -1
- package/dist/src/server/index.d.ts +3 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/composition.ts +15 -6
- package/src/client/content.ts +350 -113
- package/src/client/hooks.ts +188 -60
- package/src/client/index.ts +2 -0
- package/src/client/types.ts +6 -3
- package/src/content-upload.ts +14 -13
- package/src/draft/ahp-codegen.lock.json +14 -6
- package/src/draft/generated.ts +733 -427
- package/src/draft/index.ts +6 -1
- package/src/draft/schemas.ts +160 -120
- package/src/draft-runtime.ts +7 -9
- package/src/server/attachments.ts +5 -5
- package/src/server/hooks.ts +3 -2
- package/src/server/index.ts +5 -0
package/src/client/hooks.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
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
|
|
636
|
-
|
|
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) =>
|
|
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
|
-
.
|
|
776
|
-
|
|
777
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
891
|
-
|
|
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 (
|
|
957
|
+
if (retained.size > limit) {
|
|
958
|
+
void retained.release().catch(() => {});
|
|
894
959
|
throw new Error("Elicitation request exceeds retention budget");
|
|
895
|
-
|
|
896
|
-
|
|
960
|
+
}
|
|
961
|
+
if (
|
|
962
|
+
this.elicitationBytes + retained.size > limit ||
|
|
897
963
|
this.elicitations.size >= 128
|
|
898
|
-
)
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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):
|
|
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
|
-
|
|
911
|
-
copy.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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)
|
|
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
|
|
package/src/client/index.ts
CHANGED
package/src/client/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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>;
|
package/src/content-upload.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
/**
|
|
15
|
-
export function
|
|
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
|
-
):
|
|
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<
|
|
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
|
|
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?:
|
|
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<
|
|
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:
|
|
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 =
|
|
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": "
|
|
3
|
+
"sourceCommit": "05b4750cb2dccf17d75236182d6971f3cbe97e1f",
|
|
4
4
|
"schemaRevision": "draft",
|
|
5
5
|
"protocolVersion": "draft",
|
|
6
6
|
"generatorVersion": "0.1.0",
|
|
7
7
|
"language": "typescript",
|
|
8
|
-
"schemaManifestSha256": "
|
|
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": "
|
|
32
|
+
"sha256": "21ce30d451d6fe20eb56cf09108fe33500e40839669d2922911dd9b0b8cfe762"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "schema/draft/content-reference.schema.json",
|
|
36
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
100
|
+
"sha256": "770dd91480b4a0a0e8a3565e3724ab5d77bbede19faff223e10a8e442f926a62"
|
|
93
101
|
},
|
|
94
102
|
{
|
|
95
103
|
"path": "schema/draft/registration.schema.json",
|