agentfootprint 7.13.0 → 7.14.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/core/Agent.js +79 -2
- package/dist/core/Agent.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +47 -0
- package/dist/esm/core/Agent.js +79 -2
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/hosting/envelope.d.ts +33 -0
- package/dist/esm/hosting/envelope.js +49 -0
- package/dist/esm/hosting/envelope.js.map +1 -0
- package/dist/esm/hosting/errors.d.ts +78 -0
- package/dist/esm/hosting/errors.js +119 -0
- package/dist/esm/hosting/errors.js.map +1 -0
- package/dist/esm/hosting/index.d.ts +53 -0
- package/dist/esm/hosting/index.js +52 -0
- package/dist/esm/hosting/index.js.map +1 -0
- package/dist/esm/hosting/memorySessions.d.ts +22 -0
- package/dist/esm/hosting/memorySessions.js +31 -0
- package/dist/esm/hosting/memorySessions.js.map +1 -0
- package/dist/esm/hosting/nodeHost.d.ts +64 -0
- package/dist/esm/hosting/nodeHost.js +245 -0
- package/dist/esm/hosting/nodeHost.js.map +1 -0
- package/dist/esm/hosting/standingAgent.d.ts +62 -0
- package/dist/esm/hosting/standingAgent.js +192 -0
- package/dist/esm/hosting/standingAgent.js.map +1 -0
- package/dist/esm/hosting/types.d.ts +206 -0
- package/dist/esm/hosting/types.js +21 -0
- package/dist/esm/hosting/types.js.map +1 -0
- package/dist/hosting/envelope.js +54 -0
- package/dist/hosting/envelope.js.map +1 -0
- package/dist/hosting/errors.js +126 -0
- package/dist/hosting/errors.js.map +1 -0
- package/dist/hosting/index.js +64 -0
- package/dist/hosting/index.js.map +1 -0
- package/dist/hosting/memorySessions.js +35 -0
- package/dist/hosting/memorySessions.js.map +1 -0
- package/dist/hosting/nodeHost.js +272 -0
- package/dist/hosting/nodeHost.js.map +1 -0
- package/dist/hosting/standingAgent.js +196 -0
- package/dist/hosting/standingAgent.js.map +1 -0
- package/dist/hosting/types.js +22 -0
- package/dist/hosting/types.js.map +1 -0
- package/dist/types/core/Agent.d.ts +47 -0
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/hosting/envelope.d.ts +34 -0
- package/dist/types/hosting/envelope.d.ts.map +1 -0
- package/dist/types/hosting/errors.d.ts +79 -0
- package/dist/types/hosting/errors.d.ts.map +1 -0
- package/dist/types/hosting/index.d.ts +54 -0
- package/dist/types/hosting/index.d.ts.map +1 -0
- package/dist/types/hosting/memorySessions.d.ts +23 -0
- package/dist/types/hosting/memorySessions.d.ts.map +1 -0
- package/dist/types/hosting/nodeHost.d.ts +65 -0
- package/dist/types/hosting/nodeHost.d.ts.map +1 -0
- package/dist/types/hosting/standingAgent.d.ts +63 -0
- package/dist/types/hosting/standingAgent.d.ts.map +1 -0
- package/dist/types/hosting/types.d.ts +207 -0
- package/dist/types/hosting/types.d.ts.map +1 -0
- package/package.json +14 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosting/types — the two ports an agent needs to stand up and stay up.
|
|
3
|
+
*
|
|
4
|
+
* `AgentHost` is "something can call me". `SessionLifecycle` is "the
|
|
5
|
+
* conversation outlives the request". Both are deliberately written in the
|
|
6
|
+
* vocabulary every transport and every store already has — an input, a reply,
|
|
7
|
+
* a session id, a stored blob — and in nothing else.
|
|
8
|
+
*
|
|
9
|
+
* **The rule these types are written under:** no runtime, product or protocol
|
|
10
|
+
* gets a field, a name or an assumption here. A port shaped around one
|
|
11
|
+
* provider's request envelope stops being a port and becomes that provider's
|
|
12
|
+
* SDK with extra steps, and every later adapter pays for it. If a decision only
|
|
13
|
+
* makes sense for one place you might deploy, it belongs in the adapter for
|
|
14
|
+
* that place, not in this file. `nodeHost` is the first adapter and it does not
|
|
15
|
+
* get special treatment either: its paths, its status codes and its JSON body
|
|
16
|
+
* shape all live in `nodeHost.ts`, and nothing in this file knows they exist.
|
|
17
|
+
*
|
|
18
|
+
* Pattern: Ports & adapters (hexagonal). Role: the port side, exclusively.
|
|
19
|
+
*/
|
|
20
|
+
import type { Agent } from '../core/Agent.js';
|
|
21
|
+
import type { AgentRunCheckpoint } from '../core/runCheckpoint.js';
|
|
22
|
+
/**
|
|
23
|
+
* Something a host can do BEYOND the baseline of "accept a request, deliver one
|
|
24
|
+
* reply". Read it from {@link AgentHost.capabilities} and branch on it — never
|
|
25
|
+
* assume it, and never infer it from the adapter's name.
|
|
26
|
+
*
|
|
27
|
+
* The union starts at exactly what a shipped adapter can honour today. A name
|
|
28
|
+
* is added when an adapter can actually keep the promise, never in anticipation
|
|
29
|
+
* of a transport that does not exist yet: a capability nobody implements is a
|
|
30
|
+
* promise the library cannot keep, and pre-minting one for an imagined future
|
|
31
|
+
* transport would bake that transport's assumptions in before it arrives.
|
|
32
|
+
*/
|
|
33
|
+
export type HostCapability = 'streaming';
|
|
34
|
+
/**
|
|
35
|
+
* One inbound request, as the transport described it.
|
|
36
|
+
*/
|
|
37
|
+
export interface HostRequest {
|
|
38
|
+
/** What the caller is asking. */
|
|
39
|
+
readonly input: string;
|
|
40
|
+
/**
|
|
41
|
+
* The conversation this request CLAIMS to belong to — caller data, exactly as
|
|
42
|
+
* the transport declared it (a JSON field, a header, a path segment).
|
|
43
|
+
*
|
|
44
|
+
* It is **not identity** and must never be trusted as identity on its own:
|
|
45
|
+
* anyone who can reach the host can put any string here, including someone
|
|
46
|
+
* else's. Authenticate the caller by your own means, then check that the
|
|
47
|
+
* authenticated principal is allowed this session, before you serve it.
|
|
48
|
+
*/
|
|
49
|
+
readonly sessionId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Transport headers with lower-cased names, as delivered. Present so a
|
|
52
|
+
* handler can map its own conventions (a correlation id, a tenant) without
|
|
53
|
+
* the port having to guess which ones matter.
|
|
54
|
+
*/
|
|
55
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
56
|
+
/** Aborted when the caller goes away. */
|
|
57
|
+
readonly signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The one reply a request gets. Exactly one of {@link HostReply.complete} or
|
|
61
|
+
* {@link HostReply.fail} ends it; a second call is ignored rather than allowed
|
|
62
|
+
* to corrupt the wire.
|
|
63
|
+
*/
|
|
64
|
+
export interface HostReply {
|
|
65
|
+
/** Deliver the final answer and end the reply. */
|
|
66
|
+
complete(output: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* A piece of the answer, as it is produced.
|
|
69
|
+
*
|
|
70
|
+
* Optional on the TYPE so a minimal adapter need not implement it — every
|
|
71
|
+
* shipped adapter does. Whether the caller SEES the pieces as they arrive is
|
|
72
|
+
* the whole difference between hosts, and that is what `'streaming'` in
|
|
73
|
+
* {@link AgentHost.capabilities} reports. A host without it buffers what it is
|
|
74
|
+
* handed and the authoritative `complete(output)` is what the caller
|
|
75
|
+
* receives; the buffer is settled by the completion, never sent alongside it,
|
|
76
|
+
* because a chunk is a preview of the same text and delivering both would
|
|
77
|
+
* hand the caller the answer twice.
|
|
78
|
+
*
|
|
79
|
+
* Handler code is identical either way: emit freely, complete once.
|
|
80
|
+
*/
|
|
81
|
+
emit?(chunk: string): void;
|
|
82
|
+
/** End the reply with a failure. */
|
|
83
|
+
fail(error: Error): void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What you hand {@link AgentHost.serve}. Throwing is treated exactly like
|
|
87
|
+
* calling `reply.fail(err)` — a handler that throws is a failed request, never
|
|
88
|
+
* a hung one.
|
|
89
|
+
*/
|
|
90
|
+
export type HostHandler = (request: HostRequest, reply: HostReply) => void | Promise<void>;
|
|
91
|
+
/** A live host. */
|
|
92
|
+
export interface HostHandle {
|
|
93
|
+
/**
|
|
94
|
+
* Stop taking new requests, let the in-flight ones finish, then release the
|
|
95
|
+
* transport. Idempotent, so a shutdown hook and an explicit close can
|
|
96
|
+
* coexist. Requests arriving after it are refused with a
|
|
97
|
+
* {@link HostClosedError} naming the adapter.
|
|
98
|
+
*/
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The port: something that can carry requests to one handler and carry its
|
|
103
|
+
* replies back.
|
|
104
|
+
*/
|
|
105
|
+
export interface AgentHost {
|
|
106
|
+
/**
|
|
107
|
+
* Which adapter this is. Every refusal names it, so an error tells you WHO
|
|
108
|
+
* refused rather than leaving you to guess which layer you are looking at.
|
|
109
|
+
*/
|
|
110
|
+
readonly name: string;
|
|
111
|
+
/** What this adapter can do beyond the baseline. Feature-detect; never assume. */
|
|
112
|
+
readonly capabilities: readonly HostCapability[];
|
|
113
|
+
/** Start serving. Resolves once the host is actually live. */
|
|
114
|
+
serve(handler: HostHandler): Promise<HostHandle>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A conversation packed for storage.
|
|
118
|
+
*
|
|
119
|
+
* `format` names WHAT is inside, so a reader that does not know the shape
|
|
120
|
+
* refuses BY NAME instead of restoring a conversation it cannot actually read.
|
|
121
|
+
* Formats are ADDED, never redefined: an old runtime meeting a new format says
|
|
122
|
+
* so and stops, which is the only safe thing it can do with a payload it cannot
|
|
123
|
+
* interpret.
|
|
124
|
+
*
|
|
125
|
+
* `'conversation-v1'` stores a conversation and only a conversation. A run that
|
|
126
|
+
* paused mid-flow is a conversation PLUS an engine checkpoint, and this format
|
|
127
|
+
* has nowhere to put the second half — which is why `standingAgent` refuses to
|
|
128
|
+
* store a paused run rather than storing half of it. Carrying a pause would be
|
|
129
|
+
* a NEW format name in this same envelope, read by a runtime that knows it and
|
|
130
|
+
* refused by name everywhere else. That is what the version field is for.
|
|
131
|
+
*/
|
|
132
|
+
export interface CheckpointEnvelope {
|
|
133
|
+
/** Names the shape of `data`. Unknown values are refused, never guessed at. */
|
|
134
|
+
readonly format: 'conversation-v1';
|
|
135
|
+
/** The conversation itself — an `AgentRunCheckpoint` for `'conversation-v1'`. */
|
|
136
|
+
readonly data: AgentRunCheckpoint;
|
|
137
|
+
/** Wall-clock when it was packed. Diagnostic. */
|
|
138
|
+
readonly savedAt: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Why a session is being woken.
|
|
142
|
+
*
|
|
143
|
+
* One member, because one thing in this release can actually fire it: a request
|
|
144
|
+
* arrived for that session. Naming reasons nothing can produce would be an
|
|
145
|
+
* interface describing a system that does not exist.
|
|
146
|
+
*/
|
|
147
|
+
export type WakeReason = 'invoke';
|
|
148
|
+
/**
|
|
149
|
+
* The port: where a conversation lives between requests.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately two required methods. Anything a real store also wants — a TTL,
|
|
152
|
+
* a scan, a delete — is that store's own API, not a demand this port makes of
|
|
153
|
+
* every store that will ever implement it.
|
|
154
|
+
*/
|
|
155
|
+
export interface SessionLifecycle {
|
|
156
|
+
/** The stored conversation, or `undefined` for a session that has none yet. */
|
|
157
|
+
hydrate(sessionId: string): Promise<CheckpointEnvelope | undefined>;
|
|
158
|
+
/** Store the conversation for this session. Last write wins. */
|
|
159
|
+
persist(sessionId: string, envelope: CheckpointEnvelope): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Called once per served request, before `hydrate`, for stores that need to
|
|
162
|
+
* spin something up before they can answer. Errors from it fail the request —
|
|
163
|
+
* a store that could not wake cannot be read from either.
|
|
164
|
+
*/
|
|
165
|
+
onWake?(sessionId: string, reason: WakeReason): void | Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* What to do when a request arrives for a session that already has a run in
|
|
169
|
+
* flight.
|
|
170
|
+
*
|
|
171
|
+
* - `'reject'` (default) — refuse it, naming the run that is already going.
|
|
172
|
+
* A user who double-submits gets one answer and one refusal, not two runs
|
|
173
|
+
* racing to write the same conversation.
|
|
174
|
+
* - `'enqueue'` — queue it. It starts after the active run has persisted, so
|
|
175
|
+
* the second turn sees the first turn's stored state rather than the state
|
|
176
|
+
* it was about to overwrite.
|
|
177
|
+
*
|
|
178
|
+
* This governs the SAME session only. A request for a different session is
|
|
179
|
+
* never refused — there is nothing wrong with it; it simply waits its turn.
|
|
180
|
+
*/
|
|
181
|
+
export type ConcurrentInvokePolicy = 'reject' | 'enqueue';
|
|
182
|
+
/**
|
|
183
|
+
* Options for {@link standingAgent}.
|
|
184
|
+
*
|
|
185
|
+
* Generic in the host's own handle type so composing does not cost you what
|
|
186
|
+
* the adapter told you. `nodeHost` hands back the URL it actually bound —
|
|
187
|
+
* which is the only way to find out when you asked for port `0` — and passing
|
|
188
|
+
* it through `standingAgent` keeps that, without the port having to know that
|
|
189
|
+
* "a URL" is a thing some adapters have.
|
|
190
|
+
*/
|
|
191
|
+
export interface StandingAgentOptions<TH extends HostHandle = HostHandle> {
|
|
192
|
+
/**
|
|
193
|
+
* The agent that answers. ONE instance, shared by every session — which is
|
|
194
|
+
* why the composer runs one request at a time (see
|
|
195
|
+
* {@link ConcurrentInvokePolicy}).
|
|
196
|
+
*/
|
|
197
|
+
readonly agent: Agent;
|
|
198
|
+
/** Where conversations live between requests. */
|
|
199
|
+
readonly sessions: SessionLifecycle;
|
|
200
|
+
/** What carries requests in. */
|
|
201
|
+
readonly host: AgentHost & {
|
|
202
|
+
serve(handler: HostHandler): Promise<TH>;
|
|
203
|
+
};
|
|
204
|
+
/** Default `'reject'`. See {@link ConcurrentInvokePolicy}. */
|
|
205
|
+
readonly onConcurrentInvoke?: ConcurrentInvokePolicy;
|
|
206
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosting/types — the two ports an agent needs to stand up and stay up.
|
|
3
|
+
*
|
|
4
|
+
* `AgentHost` is "something can call me". `SessionLifecycle` is "the
|
|
5
|
+
* conversation outlives the request". Both are deliberately written in the
|
|
6
|
+
* vocabulary every transport and every store already has — an input, a reply,
|
|
7
|
+
* a session id, a stored blob — and in nothing else.
|
|
8
|
+
*
|
|
9
|
+
* **The rule these types are written under:** no runtime, product or protocol
|
|
10
|
+
* gets a field, a name or an assumption here. A port shaped around one
|
|
11
|
+
* provider's request envelope stops being a port and becomes that provider's
|
|
12
|
+
* SDK with extra steps, and every later adapter pays for it. If a decision only
|
|
13
|
+
* makes sense for one place you might deploy, it belongs in the adapter for
|
|
14
|
+
* that place, not in this file. `nodeHost` is the first adapter and it does not
|
|
15
|
+
* get special treatment either: its paths, its status codes and its JSON body
|
|
16
|
+
* shape all live in `nodeHost.ts`, and nothing in this file knows they exist.
|
|
17
|
+
*
|
|
18
|
+
* Pattern: Ports & adapters (hexagonal). Role: the port side, exclusively.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/hosting/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hosting/envelope — pack a conversation for storage, and refuse to unpack one
|
|
4
|
+
* you cannot read.
|
|
5
|
+
*
|
|
6
|
+
* Two functions and one rule: **an unknown format is refused by name, never
|
|
7
|
+
* guessed at.** A store outlives the code that wrote to it. Somebody will
|
|
8
|
+
* deploy a newer runtime, it will write a newer format, and an older instance
|
|
9
|
+
* still running will read it. The only honest thing that older instance can do
|
|
10
|
+
* is say which format it found, which ones it knows, and stop — because
|
|
11
|
+
* "restore what I can and hope" means an agent answering from a conversation
|
|
12
|
+
* that is missing whatever the older reader did not understand.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.readEnvelope = exports.toEnvelope = void 0;
|
|
16
|
+
const runCheckpoint_js_1 = require("../core/runCheckpoint.js");
|
|
17
|
+
/** Every format this runtime can read. Add, never redefine. */
|
|
18
|
+
const KNOWN_FORMATS = ['conversation-v1'];
|
|
19
|
+
/**
|
|
20
|
+
* Pack a conversation checkpoint for storage.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const conversation = agent.checkpoint();
|
|
24
|
+
* if (conversation) await sessions.persist(sessionId, toEnvelope(conversation));
|
|
25
|
+
*/
|
|
26
|
+
function toEnvelope(checkpoint) {
|
|
27
|
+
return { format: 'conversation-v1', data: checkpoint, savedAt: Date.now() };
|
|
28
|
+
}
|
|
29
|
+
exports.toEnvelope = toEnvelope;
|
|
30
|
+
/**
|
|
31
|
+
* Unpack a stored envelope back into a conversation checkpoint.
|
|
32
|
+
*
|
|
33
|
+
* Takes `unknown` on purpose: what comes back from a store is bytes somebody
|
|
34
|
+
* else wrote, in a format this runtime may not know, and typing the parameter
|
|
35
|
+
* as the happy shape would be assuming the very thing that needs checking.
|
|
36
|
+
*
|
|
37
|
+
* @throws TypeError naming the format when it is one this runtime cannot read,
|
|
38
|
+
* and naming the missing field when the conversation inside is malformed.
|
|
39
|
+
*/
|
|
40
|
+
function readEnvelope(envelope) {
|
|
41
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
42
|
+
throw new TypeError(`[hosting] stored session is not an envelope (got ${envelope === null ? 'null' : typeof envelope}). ` + `Expected { format, data, savedAt } as written by toEnvelope().`);
|
|
43
|
+
}
|
|
44
|
+
const found = envelope.format;
|
|
45
|
+
if (typeof found !== 'string' || !KNOWN_FORMATS.includes(found)) {
|
|
46
|
+
throw new TypeError(`[hosting] unknown checkpoint format '${String(found)}'. ` +
|
|
47
|
+
`This runtime reads: ${KNOWN_FORMATS.join(', ')}. ` +
|
|
48
|
+
`Refusing rather than restoring a conversation it cannot read — a newer envelope ` +
|
|
49
|
+
`needs a runtime that knows the format that wrote it.`);
|
|
50
|
+
}
|
|
51
|
+
return (0, runCheckpoint_js_1.validateCheckpoint)(envelope.data);
|
|
52
|
+
}
|
|
53
|
+
exports.readEnvelope = readEnvelope;
|
|
54
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/hosting/envelope.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAEH,+DAAuF;AAGvF,+DAA+D;AAC/D,MAAM,aAAa,GAAsB,CAAC,iBAAiB,CAAC,CAAC;AAE7D;;;;;;GAMG;AACH,SAAgB,UAAU,CAAC,UAA8B;IACvD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAC9E,CAAC;AAFD,gCAEC;AAED;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,QAAiB;IAC5C,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,IAAI,SAAS,CACjB,oDACE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,QACtC,KAAK,GAAG,gEAAgE,CACzE,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAI,QAAwC,CAAC,MAAM,CAAC;IAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CACjB,wCAAwC,MAAM,CAAC,KAAK,CAAC,KAAK;YACxD,uBAAuB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACnD,kFAAkF;YAClF,sDAAsD,CACzD,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,qCAAkB,EAAE,QAA+B,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAlBD,oCAkBC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hosting/errors — the refusals, authored once so every adapter refuses in the
|
|
4
|
+
* same words.
|
|
5
|
+
*
|
|
6
|
+
* A refusal that varies by adapter is a refusal nobody can write a test or a
|
|
7
|
+
* runbook against. These three carry a stable `code`, name WHO refused, and say
|
|
8
|
+
* what the caller should do instead. Adapters map the codes onto whatever their
|
|
9
|
+
* transport uses to say "no" — that mapping is the adapter's business and lives
|
|
10
|
+
* in the adapter, never here.
|
|
11
|
+
*
|
|
12
|
+
* `requireCapability` is the fourth refusal and the only one that is a
|
|
13
|
+
* programming mistake rather than a runtime condition, so it throws a plain
|
|
14
|
+
* `Error`: nothing branches on "I forgot to feature-detect", it just needs to
|
|
15
|
+
* say so loudly and name the adapter it is talking about.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.requireCapability = exports.PauseNotCarriedError = exports.ConcurrentRunError = exports.HostClosedError = void 0;
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when a request arrives at a host that is shutting down or shut down.
|
|
21
|
+
*
|
|
22
|
+
* `close()` lets in-flight work finish and refuses everything after it; this is
|
|
23
|
+
* what "everything after it" receives.
|
|
24
|
+
*/
|
|
25
|
+
class HostClosedError extends Error {
|
|
26
|
+
code = 'ERR_HOST_CLOSED';
|
|
27
|
+
/** Which adapter refused. */
|
|
28
|
+
hostName;
|
|
29
|
+
constructor(hostName) {
|
|
30
|
+
super(`[hosting] the '${hostName}' host is closed and is not accepting new requests. ` +
|
|
31
|
+
`In-flight requests were allowed to finish; this one arrived after close() was called. ` +
|
|
32
|
+
`Serve again on a fresh host to accept new work.`);
|
|
33
|
+
this.name = 'HostClosedError';
|
|
34
|
+
this.hostName = hostName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.HostClosedError = HostClosedError;
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a request arrives for a session that already has a run in flight
|
|
40
|
+
* and the policy is `'reject'`.
|
|
41
|
+
*
|
|
42
|
+
* The refusal is about the SESSION, not about load: two turns of one
|
|
43
|
+
* conversation racing each other would each answer from the state the other is
|
|
44
|
+
* about to replace. A request for any OTHER session is never refused — it
|
|
45
|
+
* simply waits.
|
|
46
|
+
*/
|
|
47
|
+
class ConcurrentRunError extends Error {
|
|
48
|
+
code = 'ERR_CONCURRENT_RUN';
|
|
49
|
+
/** The session that already has a run going. */
|
|
50
|
+
sessionId;
|
|
51
|
+
/** The run that is already going, when it has announced itself. */
|
|
52
|
+
activeRunId;
|
|
53
|
+
constructor(sessionId, activeRunId) {
|
|
54
|
+
super(`[hosting] session '${sessionId}' already has a run in flight` +
|
|
55
|
+
(activeRunId ? ` (run '${activeRunId}')` : '') +
|
|
56
|
+
`. Refusing rather than running two turns of one conversation at once — ` +
|
|
57
|
+
`they would each answer from state the other is about to replace. ` +
|
|
58
|
+
`Wait for the active run, or build the standing agent with ` +
|
|
59
|
+
`onConcurrentInvoke: 'enqueue' to queue this turn behind it instead.`);
|
|
60
|
+
this.name = 'ConcurrentRunError';
|
|
61
|
+
this.sessionId = sessionId;
|
|
62
|
+
if (activeRunId !== undefined)
|
|
63
|
+
this.activeRunId = activeRunId;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.ConcurrentRunError = ConcurrentRunError;
|
|
67
|
+
/**
|
|
68
|
+
* Raised when a run paused to ask a person something and the reply cannot carry
|
|
69
|
+
* a pause.
|
|
70
|
+
*
|
|
71
|
+
* **The run did not fail.** A pause is unfinished work: the agent stopped to ask
|
|
72
|
+
* and is waiting for an answer. What cannot happen here is storing it — the
|
|
73
|
+
* `'conversation-v1'` envelope holds a conversation, and a paused run is a
|
|
74
|
+
* conversation plus an engine checkpoint. So nothing is written, and the
|
|
75
|
+
* session keeps exactly the conversation it had before this request.
|
|
76
|
+
*/
|
|
77
|
+
class PauseNotCarriedError extends Error {
|
|
78
|
+
code = 'ERR_PAUSE_NOT_CARRIED';
|
|
79
|
+
/** The tool that asked, when the run recorded which one it was. */
|
|
80
|
+
toolName;
|
|
81
|
+
/** The session whose stored conversation was left untouched. */
|
|
82
|
+
sessionId;
|
|
83
|
+
constructor(toolName, sessionId) {
|
|
84
|
+
super(`[hosting] the run paused to ask a person about ` +
|
|
85
|
+
(toolName ? `'${toolName}'` : 'a tool') +
|
|
86
|
+
` and this reply cannot carry a pause. ` +
|
|
87
|
+
`The run did not fail — it is unfinished, waiting on an answer. ` +
|
|
88
|
+
`Nothing was written: ` +
|
|
89
|
+
(sessionId ? `session '${sessionId}'` : 'the session') +
|
|
90
|
+
` still holds the conversation it held before this request. ` +
|
|
91
|
+
`The 'conversation-v1' envelope stores a conversation; a paused run is a ` +
|
|
92
|
+
`conversation plus an engine checkpoint, and storing half of it would be worse ` +
|
|
93
|
+
`than storing none. Carry the pause yourself with agent.run() / agent.resume().`);
|
|
94
|
+
this.name = 'PauseNotCarriedError';
|
|
95
|
+
if (toolName !== undefined)
|
|
96
|
+
this.toolName = toolName;
|
|
97
|
+
if (sessionId !== undefined)
|
|
98
|
+
this.sessionId = sessionId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.PauseNotCarriedError = PauseNotCarriedError;
|
|
102
|
+
/**
|
|
103
|
+
* Assert that a host can do something, and throw a corrective error naming the
|
|
104
|
+
* adapter when it cannot.
|
|
105
|
+
*
|
|
106
|
+
* This is the feature-detection law with teeth: capabilities are read, never
|
|
107
|
+
* assumed, and asking for one that is absent tells you which adapter you are
|
|
108
|
+
* actually holding rather than failing quietly somewhere downstream.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* requireCapability(host, 'streaming'); // throws unless this host streams
|
|
112
|
+
*
|
|
113
|
+
* // or branch instead of insisting:
|
|
114
|
+
* if (host.capabilities.includes('streaming')) { ... }
|
|
115
|
+
*/
|
|
116
|
+
function requireCapability(host, capability) {
|
|
117
|
+
if (host.capabilities.includes(capability))
|
|
118
|
+
return;
|
|
119
|
+
const has = host.capabilities.length > 0 ? host.capabilities.join(', ') : 'none';
|
|
120
|
+
throw new Error(`[hosting] the '${host.name}' host does not support '${capability}'. ` +
|
|
121
|
+
`It reports: ${has}. Feature-detect with ` +
|
|
122
|
+
`host.capabilities.includes('${capability}') and fall back, or serve on a host ` +
|
|
123
|
+
`that has it — capabilities are read from the adapter, never assumed from its name.`);
|
|
124
|
+
}
|
|
125
|
+
exports.requireCapability = requireCapability;
|
|
126
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/hosting/errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAIH;;;;;GAKG;AACH,MAAa,eAAgB,SAAQ,KAAK;IAC/B,IAAI,GAAG,iBAA0B,CAAC;IAC3C,6BAA6B;IACpB,QAAQ,CAAS;IAE1B,YAAY,QAAgB;QAC1B,KAAK,CACH,kBAAkB,QAAQ,sDAAsD;YAC9E,wFAAwF;YACxF,iDAAiD,CACpD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAdD,0CAcC;AAED;;;;;;;;GAQG;AACH,MAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,GAAG,oBAA6B,CAAC;IAC9C,gDAAgD;IACvC,SAAS,CAAS;IAC3B,mEAAmE;IAC1D,WAAW,CAAU;IAE9B,YAAY,SAAiB,EAAE,WAAoB;QACjD,KAAK,CACH,sBAAsB,SAAS,+BAA+B;YAC5D,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,WAAW,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9C,yEAAyE;YACzE,mEAAmE;YACnE,4DAA4D;YAC5D,qEAAqE,CACxE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAChE,CAAC;CACF;AApBD,gDAoBC;AAED;;;;;;;;;GASG;AACH,MAAa,oBAAqB,SAAQ,KAAK;IACpC,IAAI,GAAG,uBAAgC,CAAC;IACjD,mEAAmE;IAC1D,QAAQ,CAAU;IAC3B,gEAAgE;IACvD,SAAS,CAAU;IAE5B,YAAY,QAAiB,EAAE,SAAkB;QAC/C,KAAK,CACH,iDAAiD;YAC/C,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;YACvC,wCAAwC;YACxC,iEAAiE;YACjE,uBAAuB;YACvB,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,SAAS,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC;YACtD,6DAA6D;YAC7D,0EAA0E;YAC1E,gFAAgF;YAChF,gFAAgF,CACnF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS;YAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACrD,IAAI,SAAS,KAAK,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1D,CAAC;CACF;AAxBD,oDAwBC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAgB,iBAAiB,CAAC,IAAe,EAAE,UAA0B;IAC3E,IAAI,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACjF,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,CAAC,IAAI,4BAA4B,UAAU,KAAK;QACpE,eAAe,GAAG,wBAAwB;QAC1C,+BAA+B,UAAU,uCAAuC;QAChF,oFAAoF,CACvF,CAAC;AACJ,CAAC;AATD,8CASC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* agentfootprint/hosting — the two ports between an agent and the place it runs.
|
|
4
|
+
*
|
|
5
|
+
* An agent that answers one call in a script and an agent that has been up for
|
|
6
|
+
* a month differ in two things, and only two: something has to carry requests
|
|
7
|
+
* to it, and the conversation has to outlive the request. This subpath is those
|
|
8
|
+
* two things as **ports**, plus local adapters that prove the ports work, plus
|
|
9
|
+
* the composer that wires them together.
|
|
10
|
+
*
|
|
11
|
+
* `AgentHost` — something can call me.
|
|
12
|
+
* `SessionLifecycle` — the conversation outlives the request.
|
|
13
|
+
* `standingAgent` — hydrate → resume-or-fresh → persist → reply.
|
|
14
|
+
*
|
|
15
|
+
* ── Why the ports look like nothing in particular ────────────────────────────
|
|
16
|
+
* Deliberately. Not one field, name or assumption here comes from any hosting
|
|
17
|
+
* product, cloud or protocol. A port shaped around one provider's request
|
|
18
|
+
* envelope stops being a port and becomes that provider's SDK with extra steps,
|
|
19
|
+
* and every adapter after the first pays for the shortcut. So the ports carry
|
|
20
|
+
* an input, a reply, an optional session id, headers, a signal — the vocabulary
|
|
21
|
+
* every transport already has — and everything specific to one place you might
|
|
22
|
+
* deploy lives in the adapter for that place. `nodeHost` gets no special
|
|
23
|
+
* treatment: its paths, status codes and JSON body shape are all in
|
|
24
|
+
* `nodeHost.ts` and the port types do not know they exist. A test greps these
|
|
25
|
+
* source files for vendor names, crudely and on purpose.
|
|
26
|
+
*
|
|
27
|
+
* ── What ships here ──────────────────────────────────────────────────────────
|
|
28
|
+
* • `nodeHost({ port?, hostname?, invokePath?, healthPath? })` — plain
|
|
29
|
+
* `node:http`, zero dependencies. `POST /invoke`, `GET /health`, and
|
|
30
|
+
* Server-Sent Events when the caller asks for them.
|
|
31
|
+
* • `memorySessions()` — conversations in a Map, for tests and local dev.
|
|
32
|
+
* • `standingAgent({ agent, sessions, host })` — the composer.
|
|
33
|
+
* • `toEnvelope` / `readEnvelope` — pack a conversation, and refuse by name to
|
|
34
|
+
* unpack a format this runtime does not know.
|
|
35
|
+
* • `requireCapability` — feature-detection with teeth.
|
|
36
|
+
*
|
|
37
|
+
* @example An agent that stays up and remembers
|
|
38
|
+
* import { Agent } from 'agentfootprint';
|
|
39
|
+
* import { standingAgent, nodeHost, memorySessions } from 'agentfootprint/hosting';
|
|
40
|
+
*
|
|
41
|
+
* const handle = await standingAgent({
|
|
42
|
+
* agent: Agent.create({ provider, model }).system('You help customers.').build(),
|
|
43
|
+
* sessions: memorySessions(),
|
|
44
|
+
* host: nodeHost({ port: 8080 }),
|
|
45
|
+
* });
|
|
46
|
+
* process.on('SIGTERM', () => void handle.close());
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.PauseNotCarriedError = exports.ConcurrentRunError = exports.HostClosedError = exports.requireCapability = exports.standingAgent = exports.readEnvelope = exports.toEnvelope = exports.memorySessions = exports.nodeHost = void 0;
|
|
50
|
+
var nodeHost_js_1 = require("./nodeHost.js");
|
|
51
|
+
Object.defineProperty(exports, "nodeHost", { enumerable: true, get: function () { return nodeHost_js_1.nodeHost; } });
|
|
52
|
+
var memorySessions_js_1 = require("./memorySessions.js");
|
|
53
|
+
Object.defineProperty(exports, "memorySessions", { enumerable: true, get: function () { return memorySessions_js_1.memorySessions; } });
|
|
54
|
+
var envelope_js_1 = require("./envelope.js");
|
|
55
|
+
Object.defineProperty(exports, "toEnvelope", { enumerable: true, get: function () { return envelope_js_1.toEnvelope; } });
|
|
56
|
+
Object.defineProperty(exports, "readEnvelope", { enumerable: true, get: function () { return envelope_js_1.readEnvelope; } });
|
|
57
|
+
var standingAgent_js_1 = require("./standingAgent.js");
|
|
58
|
+
Object.defineProperty(exports, "standingAgent", { enumerable: true, get: function () { return standingAgent_js_1.standingAgent; } });
|
|
59
|
+
var errors_js_1 = require("./errors.js");
|
|
60
|
+
Object.defineProperty(exports, "requireCapability", { enumerable: true, get: function () { return errors_js_1.requireCapability; } });
|
|
61
|
+
Object.defineProperty(exports, "HostClosedError", { enumerable: true, get: function () { return errors_js_1.HostClosedError; } });
|
|
62
|
+
Object.defineProperty(exports, "ConcurrentRunError", { enumerable: true, get: function () { return errors_js_1.ConcurrentRunError; } });
|
|
63
|
+
Object.defineProperty(exports, "PauseNotCarriedError", { enumerable: true, get: function () { return errors_js_1.PauseNotCarriedError; } });
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hosting/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;;;AAEH,6CAAyC;AAAhC,uGAAA,QAAQ,OAAA;AAGjB,yDAAqD;AAA5C,mHAAA,cAAc,OAAA;AACvB,6CAAyD;AAAhD,yGAAA,UAAU,OAAA;AAAE,2GAAA,YAAY,OAAA;AACjC,uDAAmD;AAA1C,iHAAA,aAAa,OAAA;AAEtB,yCAKqB;AAJnB,8GAAA,iBAAiB,OAAA;AACjB,4GAAA,eAAe,OAAA;AACf,+GAAA,kBAAkB,OAAA;AAClB,iHAAA,oBAAoB,OAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hosting/memorySessions — conversations in a Map.
|
|
4
|
+
*
|
|
5
|
+
* The smallest thing that satisfies `SessionLifecycle`, for tests and for local
|
|
6
|
+
* development where a Redis is ceremony you have not earned yet. It is exactly
|
|
7
|
+
* as durable as the process: restart and every conversation is gone.
|
|
8
|
+
*
|
|
9
|
+
* That is not a shortcoming to apologise for, it is the point of the port. Swap
|
|
10
|
+
* this for a store that survives a restart and the standing agent is unchanged
|
|
11
|
+
* — which is also how the "a crashed process resumes the conversation" test is
|
|
12
|
+
* written: keep the store, throw away everything else, and watch the
|
|
13
|
+
* conversation come back through the envelope alone.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.memorySessions = void 0;
|
|
17
|
+
/**
|
|
18
|
+
* An in-process session store.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const sessions = memorySessions();
|
|
22
|
+
* await standingAgent({ agent, sessions, host: nodeHost({ port: 8080 }) });
|
|
23
|
+
*/
|
|
24
|
+
function memorySessions() {
|
|
25
|
+
const stored = new Map();
|
|
26
|
+
return {
|
|
27
|
+
hydrate: (sessionId) => Promise.resolve(stored.get(sessionId)),
|
|
28
|
+
persist: (sessionId, envelope) => {
|
|
29
|
+
stored.set(sessionId, envelope);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
exports.memorySessions = memorySessions;
|
|
35
|
+
//# sourceMappingURL=memorySessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memorySessions.js","sourceRoot":"","sources":["../../src/hosting/memorySessions.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAIH;;;;;;GAMG;AACH,SAAgB,cAAc;IAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAC;IACrD,OAAO;QACL,OAAO,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9D,OAAO,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE;YAC/B,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAChC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AATD,wCASC"}
|