agenthooksprotocol 0.1.1 → 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/dist/src/client/content.d.ts +58 -22
- package/dist/src/client/content.d.ts.map +1 -1
- package/dist/src/client/content.js +346 -104
- package/dist/src/client/content.js.map +1 -1
- package/dist/src/client/hooks.d.ts.map +1 -1
- package/dist/src/client/hooks.js +64 -43
- package/dist/src/client/hooks.js.map +1 -1
- package/dist/src/client/types.d.ts +4 -1
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/client/types.js.map +1 -1
- package/package.json +1 -1
- package/src/client/content.ts +342 -117
- package/src/client/hooks.ts +65 -46
- package/src/client/types.ts +4 -1
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";
|
|
@@ -127,7 +127,7 @@ export class Hooks {
|
|
|
127
127
|
private closed?: Promise<void>;
|
|
128
128
|
private readonly elicitations = new Map<
|
|
129
129
|
string,
|
|
130
|
-
{ event: any;
|
|
130
|
+
{ event: any; owner: Attachment; size: number; release: () => Promise<void> }
|
|
131
131
|
>();
|
|
132
132
|
private elicitationBytes = 0;
|
|
133
133
|
// Tokens exist only for active calls, including preparation before any bytes
|
|
@@ -440,8 +440,12 @@ export class Hooks {
|
|
|
440
440
|
: 64 * 1024 * 1024,
|
|
441
441
|
allowLoopback: true,
|
|
442
442
|
});
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
try {
|
|
444
|
+
manager.own([input, options.contentSources]);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
await manager.close().catch(() => {});
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
445
449
|
this.managers.add(manager);
|
|
446
450
|
const signal = combineSignals(this.lifetime.signal, options.signal);
|
|
447
451
|
const exchange: ElicitationLifetime | undefined =
|
|
@@ -454,6 +458,8 @@ export class Hooks {
|
|
|
454
458
|
: undefined;
|
|
455
459
|
if (exchange) this.activeElicitations.add(exchange);
|
|
456
460
|
let completed = false;
|
|
461
|
+
let returnedContent: BoundaryResult<K>["content"];
|
|
462
|
+
|
|
457
463
|
const work = (async () => {
|
|
458
464
|
// Calls after close are misuse. Active calls return interruption evidence.
|
|
459
465
|
this.lifetime.signal.throwIfAborted();
|
|
@@ -464,6 +470,10 @@ export class Hooks {
|
|
|
464
470
|
try {
|
|
465
471
|
const result = await work;
|
|
466
472
|
completed = true;
|
|
473
|
+
if (result.content && !result.interrupted) {
|
|
474
|
+
manager.transfer(result.content);
|
|
475
|
+
returnedContent = result.content;
|
|
476
|
+
}
|
|
467
477
|
return result;
|
|
468
478
|
} finally {
|
|
469
479
|
// Terminal exchanges retire even when validation or cancellation bypasses run.
|
|
@@ -478,9 +488,16 @@ export class Hooks {
|
|
|
478
488
|
if (type === "session.end")
|
|
479
489
|
this.retireSessionElicitations((input as any).session?.id);
|
|
480
490
|
if (exchange) this.activeElicitations.delete(exchange);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
}
|
|
484
501
|
}
|
|
485
502
|
}
|
|
486
503
|
|
|
@@ -547,8 +564,9 @@ export class Hooks {
|
|
|
547
564
|
interrupted: true,
|
|
548
565
|
} as BoundaryResult<K>;
|
|
549
566
|
const localSources = new Set<ReadableStream<Uint8Array>>();
|
|
550
|
-
|
|
551
|
-
|
|
567
|
+
let correlation: { event: Event; owner: Attachment; release: () => Promise<void> } | undefined;
|
|
568
|
+
let originalRequest: Event | undefined;
|
|
569
|
+
|
|
552
570
|
let effects: Effect[] = [];
|
|
553
571
|
let state = structuredClone(options.initialState);
|
|
554
572
|
// A supplied snapshot is input to the first receiver, not a newly accepted
|
|
@@ -595,6 +613,8 @@ export class Hooks {
|
|
|
595
613
|
advertised.modes.includes(r.subscription.mode),
|
|
596
614
|
);
|
|
597
615
|
try {
|
|
616
|
+
correlation = this.elicitationFor(event, manager);
|
|
617
|
+
originalRequest = correlation?.event;
|
|
598
618
|
// Validate a metadata-only view before any delivery (this does not consume bytes).
|
|
599
619
|
const metadata = await manager.prepare(
|
|
600
620
|
event,
|
|
@@ -668,11 +688,8 @@ export class Hooks {
|
|
|
668
688
|
projected.elicitation?.request?.selection === "body" &&
|
|
669
689
|
event.elicitation?.request?.body instanceof ReadableStream
|
|
670
690
|
) {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
signal,
|
|
674
|
-
);
|
|
675
|
-
this.rememberElicitation(event, bytes, exchange!);
|
|
691
|
+
const retained = await manager.retainBody(event.elicitation.request.body, signal);
|
|
692
|
+
this.rememberElicitation(event, retained, exchange!);
|
|
676
693
|
}
|
|
677
694
|
const request = {
|
|
678
695
|
jsonrpc: "2.0",
|
|
@@ -727,7 +744,9 @@ export class Hooks {
|
|
|
727
744
|
);
|
|
728
745
|
const staged = await raceAbort(
|
|
729
746
|
composeResponseAsync(event, effects, decoded.value, caps, {
|
|
730
|
-
readContent: (body) =>
|
|
747
|
+
readContent: (body) => originalRequest && body === (originalRequest as any).elicitation.request.body
|
|
748
|
+
? correlation!.owner.read(deadline)
|
|
749
|
+
: manager.readBody(body, deadline),
|
|
731
750
|
ownContent: (body) => {
|
|
732
751
|
manager.own(body);
|
|
733
752
|
localSources.add(body);
|
|
@@ -838,6 +857,7 @@ export class Hooks {
|
|
|
838
857
|
} as InterceptResponse;
|
|
839
858
|
return {
|
|
840
859
|
event: await resultEvent(event, manager, localSources),
|
|
860
|
+
content: manager.resultContent(event),
|
|
841
861
|
response,
|
|
842
862
|
state: snapshotState(state),
|
|
843
863
|
get permission() {
|
|
@@ -854,9 +874,11 @@ export class Hooks {
|
|
|
854
874
|
} catch (error) {
|
|
855
875
|
if (type === "user.elicitation.request")
|
|
856
876
|
this.forgetElicitation(requestId);
|
|
857
|
-
await manager.close();
|
|
877
|
+
await manager.close().catch(() => {});
|
|
858
878
|
this.managers.delete(manager);
|
|
859
879
|
throw error;
|
|
880
|
+
} finally {
|
|
881
|
+
await correlation?.release().catch(() => {});
|
|
860
882
|
}
|
|
861
883
|
}
|
|
862
884
|
|
|
@@ -924,20 +946,26 @@ export class Hooks {
|
|
|
924
946
|
|
|
925
947
|
private rememberElicitation(
|
|
926
948
|
event: any,
|
|
927
|
-
|
|
949
|
+
retained: { owner: Attachment; size: number; release: () => Promise<void> },
|
|
928
950
|
exchange: ElicitationLifetime,
|
|
929
951
|
): void {
|
|
930
|
-
if (exchange.retired || this.lifetime.signal.aborted)
|
|
931
|
-
|
|
952
|
+
if (exchange.retired || this.lifetime.signal.aborted || this.elicitations.has(event.id)) {
|
|
953
|
+
void retained.release().catch(() => {});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
932
956
|
const limit = this.options.maxContentBytes ?? 64 * 1024 * 1024;
|
|
933
|
-
if (
|
|
957
|
+
if (retained.size > limit) {
|
|
958
|
+
void retained.release().catch(() => {});
|
|
934
959
|
throw new Error("Elicitation request exceeds retention budget");
|
|
960
|
+
}
|
|
935
961
|
if (
|
|
936
|
-
this.elicitationBytes +
|
|
962
|
+
this.elicitationBytes + retained.size > limit ||
|
|
937
963
|
this.elicitations.size >= 128
|
|
938
|
-
)
|
|
964
|
+
) {
|
|
965
|
+
void retained.release().catch(() => {});
|
|
939
966
|
throw new Error("Active elicitation exchanges exceed retention budget");
|
|
940
|
-
|
|
967
|
+
}
|
|
968
|
+
// Pending exchanges retain correlation facts and a lease on the same owner.
|
|
941
969
|
const stored = {
|
|
942
970
|
type: event.type,
|
|
943
971
|
id: event.id,
|
|
@@ -949,22 +977,18 @@ export class Hooks {
|
|
|
949
977
|
request: { mediaType: event.elicitation.request.mediaType },
|
|
950
978
|
},
|
|
951
979
|
};
|
|
952
|
-
this.elicitations.set(event.id, { event: stored,
|
|
953
|
-
this.elicitationBytes +=
|
|
980
|
+
this.elicitations.set(event.id, { event: stored, ...retained });
|
|
981
|
+
this.elicitationBytes += retained.size;
|
|
954
982
|
}
|
|
955
|
-
private elicitationFor(event: any):
|
|
983
|
+
private elicitationFor(event: any, manager: ContentManager): {
|
|
984
|
+
event: Event; owner: Attachment; release: () => Promise<void>;
|
|
985
|
+
} | undefined {
|
|
956
986
|
if (event.type !== "user.elicitation.result") return undefined;
|
|
957
987
|
const entry = this.elicitations.get(event.parentEventId);
|
|
958
988
|
if (!entry) return undefined;
|
|
959
|
-
const copy = cloneInput(entry.event)
|
|
960
|
-
|
|
961
|
-
copy.
|
|
962
|
-
start(c) {
|
|
963
|
-
c.enqueue(bytes);
|
|
964
|
-
c.close();
|
|
965
|
-
},
|
|
966
|
-
});
|
|
967
|
-
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) };
|
|
968
992
|
}
|
|
969
993
|
/** End an abandoned host elicitation exchange without delivering a result. */
|
|
970
994
|
discardElicitation(requestEventId: string): void {
|
|
@@ -982,7 +1006,10 @@ export class Hooks {
|
|
|
982
1006
|
for (const exchange of this.activeElicitations)
|
|
983
1007
|
if (exchange.id === id) exchange.retired = true;
|
|
984
1008
|
const entry = this.elicitations.get(id);
|
|
985
|
-
if (entry)
|
|
1009
|
+
if (entry) {
|
|
1010
|
+
this.elicitationBytes -= entry.size;
|
|
1011
|
+
void entry.release().catch(() => {});
|
|
1012
|
+
}
|
|
986
1013
|
this.elicitations.delete(id);
|
|
987
1014
|
}
|
|
988
1015
|
|
|
@@ -1001,6 +1028,7 @@ export class Hooks {
|
|
|
1001
1028
|
await Promise.allSettled([...this.managers].map((m) => m.close()));
|
|
1002
1029
|
await Promise.allSettled([...this.pending]);
|
|
1003
1030
|
this.authContexts.clear();
|
|
1031
|
+
await Promise.allSettled([...this.elicitations.values()].map(entry => entry.release()));
|
|
1004
1032
|
this.elicitations.clear();
|
|
1005
1033
|
this.elicitationBytes = 0;
|
|
1006
1034
|
})();
|
|
@@ -1224,16 +1252,7 @@ async function resultEvent(
|
|
|
1224
1252
|
): Promise<any> {
|
|
1225
1253
|
if (value instanceof ContentSource) value = value.stream;
|
|
1226
1254
|
if (value instanceof ReadableStream) {
|
|
1227
|
-
|
|
1228
|
-
? await manager.readBody(value)
|
|
1229
|
-
: await manager.copySnapshot(value);
|
|
1230
|
-
if (!bytes) return undefined;
|
|
1231
|
-
return new ReadableStream<Uint8Array>({
|
|
1232
|
-
start(controller) {
|
|
1233
|
-
controller.enqueue(bytes);
|
|
1234
|
-
controller.close();
|
|
1235
|
-
},
|
|
1236
|
-
});
|
|
1255
|
+
return manager.resultBody(value, localSources.has(value));
|
|
1237
1256
|
}
|
|
1238
1257
|
if (Array.isArray(value))
|
|
1239
1258
|
return Promise.all(
|
package/src/client/types.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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,
|
|
@@ -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>;
|