@lionhummer/constellation 0.1.3 → 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 -132
- package/dist/index.d.ts +24 -48
- package/dist/index.js +22 -59
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,172 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lionhummer/constellation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript client for Constellation's durable request API. Run it on a trusted server with an execution key. Constellation routes model calls and stores the execution trace; your application supplies messages, tool implementations, optional policy decisions, and billing.
|
|
4
4
|
|
|
5
|
-
```
|
|
5
|
+
```bash
|
|
6
6
|
npm install @lionhummer/constellation
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import { Constellation, uuid7
|
|
10
|
+
import { Constellation, uuid7 } from "@lionhummer/constellation"
|
|
11
11
|
|
|
12
12
|
const client = new Constellation({
|
|
13
13
|
baseUrl: "https://your-constellation-host.example",
|
|
14
14
|
apiKey: process.env.CONSTELLATION_EXECUTION_KEY!,
|
|
15
|
-
})
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Keep keys on a trusted server. One key may have execution, management and telemetry permissions; the method namespaces below map to those API permissions. An `AbortSignal` stops the current SDK operation, not the durable request. Use `requests.cancel(id)` to cancel execution.
|
|
19
|
-
|
|
20
|
-
## Choose an execution kind
|
|
21
|
-
|
|
22
|
-
| Kind | Model steps | Caller participation |
|
|
23
|
-
| --- | --- | --- |
|
|
24
|
-
| `generate` | Exactly one. Tools are not accepted. | Observe the result. |
|
|
25
|
-
| `run` | Continues through model steps and caller tools. | Implement tools; there is no review gate before tool dispatch. |
|
|
26
|
-
| `agent` | First step executes from the complete submitted input; later steps use fresh complete inputs. | Review model output and usage before tools, review after tools, and decide whether to continue. |
|
|
27
|
-
|
|
28
|
-
All three use `client.requests.create(input)` and return `{ id }`. Persist the UUIDv7 `idempotencyKey` before submitting. If the POST outcome is uncertain, retry with that same key **and identical input**. `externalId` groups separate requests and can hint at prior-provider affinity; it does not retrieve conversation history.
|
|
29
|
-
|
|
30
|
-
### One-step generation
|
|
15
|
+
})
|
|
31
16
|
|
|
32
|
-
```ts
|
|
33
17
|
const { id } = await client.requests.create({
|
|
34
|
-
kind: "generate",
|
|
35
18
|
idempotencyKey: uuid7(),
|
|
36
19
|
workspaceId: "workspace-123",
|
|
20
|
+
externalId: "conversation-456",
|
|
37
21
|
model: "catalog/example-model",
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
22
|
+
instructions: "Answer accurately.",
|
|
23
|
+
messages: [{ role: "user", text: "Look this up." }],
|
|
24
|
+
tools: [{
|
|
25
|
+
name: "lookup",
|
|
26
|
+
inputSchema: { type: "object", properties: { key: { type: "string" } } },
|
|
27
|
+
}],
|
|
28
|
+
events: { afterStep: "wait", outputDelta: "notify", terminal: "notify" },
|
|
29
|
+
})
|
|
46
30
|
```
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
The first model step uses the submitted input. Keep the idempotency key for retries; reusing it with different input is rejected. `externalId` connects independent requests and may supply a routing affinity hint; it does not load conversation history.
|
|
49
33
|
|
|
50
|
-
|
|
34
|
+
## Events and caller work
|
|
51
35
|
|
|
52
|
-
|
|
36
|
+
All event modes default to `off`. `beforeModel`, `afterModel`, and `afterStep` accept `off`, `notify`, or `wait`. `outputDelta` and `terminal` accept `off` or `notify`. Notifications are delivered to the execution key's configured webhook. A wait creates a durable input that your application must claim and answer. Tool calls always create durable inputs, including parallel calls.
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
const handlers = {
|
|
56
|
-
consumerId: "stable-worker-id",
|
|
57
|
-
tools: {
|
|
58
|
-
lookup: async (args, { inputId, signal }) => {
|
|
59
|
-
// Deduplicate external effects by inputId: callbacks can be delivered again.
|
|
60
|
-
return lookupOnce(args, inputId, signal);
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
} satisfies InputHandlers;
|
|
64
|
-
|
|
65
|
-
// From one verified input webhook:
|
|
66
|
-
await client.inputs.handle(requestId, inputId, handlers);
|
|
67
|
-
|
|
68
|
-
// Or run a polling consumer until terminal:
|
|
69
|
-
await client.inputs.poll(requestId, handlers);
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`inputs.handle()` processes only the named input and returns. `inputs.poll()` repeatedly inspects and handles inputs until the request is terminal. Neither silently switches delivery modes. For custom consumers, `inputs.claim()` and `inputs.respond()` expose the underlying claim and response operations.
|
|
73
|
-
|
|
74
|
-
### Caller-controlled agent
|
|
75
|
-
|
|
76
|
-
An agent submission contains the **complete first step**. Do not submit a placeholder message expecting an initial preparation callback. The SDK sets the server's `prepareStep` protocol flag for `agent` requests.
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const { id } = await client.requests.create({
|
|
80
|
-
kind: "agent",
|
|
81
|
-
idempotencyKey: uuid7(),
|
|
82
|
-
workspaceId: "workspace-123",
|
|
83
|
-
externalId: "conversation-456",
|
|
84
|
-
model: "catalog/example-model",
|
|
85
|
-
instructions: "Work carefully and use tools when needed.",
|
|
86
|
-
messages: fullFirstStepHistory,
|
|
87
|
-
tools: toolDefinitions,
|
|
88
|
-
parameters: {},
|
|
89
|
-
maxOutputTokens: 4096,
|
|
90
|
-
});
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
After each model step, Constellation issues `caller_review` with output, usage, customer cost, supplier cost and pending tools. Your application can settle usage and decide whether to allow tools. After tools, another review can finish or request the next step. Before a later model step, `prepare_step` asks for the **whole next input**: model, instructions, messages, tools, parameters, output limit and cache policy.
|
|
38
|
+
`afterStep` happens after the model when it has no tool calls, or after all tool results. It includes output, usage, cost, and tool results. Use it to settle billing once and decide whether to complete or continue. Continuing requires a complete next `stepInput`, including model, messages, instructions, tools, parameters, output limit, and cache policy. Constellation routes each later step separately.
|
|
94
39
|
|
|
95
40
|
```ts
|
|
96
41
|
const handlers = {
|
|
97
42
|
consumerId: "stable-worker-id",
|
|
98
|
-
callerReview: async (review, state, { inputId }) => ({
|
|
99
|
-
action: await settleAndDecideOnce(review, state, inputId),
|
|
100
|
-
}),
|
|
101
43
|
tools: {
|
|
102
44
|
lookup: async (args, { inputId, signal }) => lookupOnce(args, inputId, signal),
|
|
103
45
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
46
|
+
checkpoints: {
|
|
47
|
+
afterStep: async (payload, state, { inputId, parentSpanId, signal }) => {
|
|
48
|
+
await settleOnce(inputId, payload, { parentSpanId, signal })
|
|
49
|
+
return shouldContinue(state)
|
|
50
|
+
? { action: "continue", stepInput: await buildCompleteNextStep(state) }
|
|
51
|
+
: { action: "complete" }
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await client.inputs.handle(id, inputId, handlers)
|
|
108
57
|
```
|
|
109
58
|
|
|
110
|
-
|
|
59
|
+
`inputs.handle` processes one named input, which suits webhook delivery. `inputs.poll` continuously inspects and handles inputs until terminal and must be started explicitly. Both claim and renew inputs while handlers run. Side effects should be deduplicated by input ID because delivery and responses can be retried. Low-level `inputs.claim` and `inputs.respond` are available when you manage the lifecycle yourself.
|
|
111
60
|
|
|
112
|
-
##
|
|
61
|
+
## Webhook
|
|
113
62
|
|
|
114
|
-
Configure the execution key
|
|
63
|
+
Configure an HTTPS callback URL and token on the execution key. Verify `x-constellation-webhook-token`; return 2xx after your effects are durable. A non-2xx response is retried. Model text/reasoning chunks arrive as `request.events` when `outputDelta` is `notify`; terminal notification carries a request ID and event sequence.
|
|
115
64
|
|
|
116
65
|
```ts
|
|
117
|
-
const event = await client.webhooks.verify(request,
|
|
66
|
+
const event = await client.webhooks.verify(request, webhookToken)
|
|
118
67
|
if ("inputId" in event) {
|
|
119
|
-
await client.inputs.handle(event.requestId, event.inputId, handlers)
|
|
120
|
-
} else if (event.kind === "request.
|
|
121
|
-
await
|
|
68
|
+
await client.inputs.handle(event.requestId, event.inputId, handlers)
|
|
69
|
+
} else if (event.kind === "request.events") {
|
|
70
|
+
await storeEventBatchOnce(event.requestId, event.afterSeq, event.events)
|
|
122
71
|
} else {
|
|
123
|
-
|
|
124
|
-
|
|
72
|
+
await finalizeOnce(event.requestId, event.eventSeq,
|
|
73
|
+
await client.requests.getSummary(event.requestId))
|
|
125
74
|
}
|
|
126
|
-
// Return 2xx after your input response or other effects are durable.
|
|
127
75
|
```
|
|
128
76
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
`requests.streamEvents(id, { after, signal })` is SSE with durable sequence replay. `requests.listEvents(id, after)` reads a bounded event page. SSE observes output; it does not process caller inputs. A dropped stream reconnects by cursor and does not cancel the request.
|
|
132
|
-
|
|
133
|
-
## Inspect and instrument
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
const summary = await client.requests.getSummary(id);
|
|
137
|
-
const steps = await client.requests.listRecords(id, "steps", { limit: 25, offset: 0 });
|
|
138
|
-
const timeline = await client.requests.getTimeline(id);
|
|
139
|
-
|
|
140
|
-
const span = await client.spans.start(id, "application.context", { inputId });
|
|
141
|
-
try {
|
|
142
|
-
await rebuildContext();
|
|
143
|
-
await span.end();
|
|
144
|
-
} catch (error) {
|
|
145
|
-
await span.end({ status: "error" });
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
`requests.get(id)` loads full inspection. For long runs, use `getSummary()` and `listRecords()` for steps, attempts, inputs, spans or logs. `getTimeline()` returns the same recorded and derived rows as the console. `getHttpExchange()` and `getHttpBody()` inspect a provider attempt. Caller spans may link to an `inputId`, `stepId`, `attemptId` or `parentSpanId`; `client.spans.add()` and `client.logs.add()` write batches.
|
|
151
|
-
|
|
152
|
-
User messages may contain HTTPS image or PDF URL parts. Constellation stores URL references, not media bytes. Provider adapters forward supported URLs or download within limits immediately before dispatch. Keep signed URLs valid until the provider fetches them; refresh URLs in a later agent `prepareStep` callback.
|
|
153
|
-
|
|
154
|
-
## Other namespaces
|
|
155
|
-
|
|
156
|
-
| Namespace | Methods |
|
|
157
|
-
| --- | --- |
|
|
158
|
-
| `management.registry` | `get`, `listRows`, `update`, `applyChanges`, `listHistory`, `rollback`, `preview`, `testRules` |
|
|
159
|
-
| `management.keys` | `list`, `listPage`, `create`, `revoke`, `setInputWebhook` |
|
|
160
|
-
| `management.credentials` | `list`, `listPage`, `create`, `update`, `delete` |
|
|
161
|
-
| `management.executionSettings` | `get`, `update`, `listHistory` |
|
|
162
|
-
| `telemetry.requests` | `list`, `get`, `getSummary`, `listEvents`, `listRecords`, `getTimeline`, `getHttpExchange`, `getHttpBody` |
|
|
163
|
-
| `telemetry.system` | `get`, `listHistory`, `getComponent` |
|
|
164
|
-
| `telemetry.observability` | `get`, `getSeries`, `getTiming` |
|
|
165
|
-
| `telemetry.jobs` | `listHistory` |
|
|
166
|
-
| `telemetry.notifications` | `list`, `get`, `listEvents` |
|
|
167
|
-
| `telemetry.logs` | `searchSystem`, `searchRequests`, `searchAudit` |
|
|
168
|
-
| `health` | `live`, `ready` (public probes) |
|
|
77
|
+
## Inspection and other APIs
|
|
169
78
|
|
|
170
|
-
|
|
79
|
+
`requests.get` returns the request and its records. For long executions, use `requests.getSummary`, `requests.listRecords`, `requests.getTimeline`, and the paginated request list. `requests.listEvents` reads the durable event journal; `requests.streamEvents` reconnects SSE from a cursor. Stopping an observer does not cancel execution; use `requests.cancel` explicitly.
|
|
171
80
|
|
|
172
|
-
|
|
81
|
+
Execution methods are top-level (`models`, `requests`, `inputs`, `spans`, `logs`). Configuration lives under `management`; read-only operational data lives under `telemetry`. The same API timeline is shown in the console. Caller spans can attach to a wait using `inputId` and nest beneath a span using `parentSpanId`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Durable request client. No provider credentials or
|
|
1
|
+
/** Durable request client. No provider credentials or application policy live here. */
|
|
2
2
|
export type Json = null | boolean | number | string | Json[] | {
|
|
3
3
|
[key: string]: Json;
|
|
4
4
|
};
|
|
@@ -56,25 +56,23 @@ type RequestFields = {
|
|
|
56
56
|
isolationKey?: string;
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
59
|
+
export type RequestEventModes = {
|
|
60
|
+
beforeModel?: "off" | "notify" | "wait";
|
|
61
|
+
afterModel?: "off" | "notify" | "wait";
|
|
62
|
+
afterStep?: "off" | "notify" | "wait";
|
|
63
|
+
outputDelta?: "off" | "notify";
|
|
64
|
+
terminal?: "off" | "notify";
|
|
65
|
+
};
|
|
66
|
+
/** One execution protocol; events determine where the caller participates. */
|
|
67
|
+
export type RequestInput = RequestFields & {
|
|
68
|
+
events?: RequestEventModes;
|
|
69
|
+
};
|
|
71
70
|
export type Status = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled";
|
|
72
71
|
export type RequestRecord = {
|
|
73
72
|
id: string;
|
|
74
73
|
externalId: string | null;
|
|
75
74
|
workspaceId: string;
|
|
76
75
|
userId: string | null;
|
|
77
|
-
kind: "generate" | "run" | "agent";
|
|
78
76
|
requestedModel: string;
|
|
79
77
|
virtualModel: boolean;
|
|
80
78
|
planId: string | null;
|
|
@@ -157,28 +155,16 @@ export type PreparedStep = {
|
|
|
157
155
|
maxOutputTokens: number;
|
|
158
156
|
cache: RequestInput["cache"] | null;
|
|
159
157
|
};
|
|
160
|
-
export type CallerReview = {
|
|
161
|
-
stage: "after_model" | "after_tools";
|
|
162
|
-
stepId: string;
|
|
163
|
-
toolCalls?: ToolCall[];
|
|
164
|
-
toolResults?: Message["toolResults"];
|
|
165
|
-
usage?: Json;
|
|
166
|
-
customerCost?: string | null;
|
|
167
|
-
supplierCost?: string | null;
|
|
168
|
-
customerPrice?: Json;
|
|
169
|
-
output?: Json;
|
|
170
|
-
};
|
|
171
158
|
export type PendingInput = {
|
|
172
159
|
id: string;
|
|
173
160
|
requestId: string;
|
|
174
161
|
stepId: string | null;
|
|
175
|
-
kind: "tool" | "
|
|
162
|
+
kind: "tool" | "user_input" | "before_model" | "after_model" | "after_step";
|
|
176
163
|
payload: {
|
|
177
164
|
providerCallId?: string;
|
|
178
165
|
name?: string;
|
|
179
166
|
arguments?: Json;
|
|
180
167
|
validationError?: string | null;
|
|
181
|
-
stage?: CallerReview["stage"];
|
|
182
168
|
toolCalls?: ToolCall[];
|
|
183
169
|
toolResults?: Message["toolResults"];
|
|
184
170
|
usage?: Json;
|
|
@@ -268,26 +254,20 @@ export type InputResponse = {
|
|
|
268
254
|
claimId?: string;
|
|
269
255
|
result?: Json;
|
|
270
256
|
isError?: boolean;
|
|
271
|
-
instructions?: string;
|
|
272
|
-
tools?: Tool[];
|
|
273
257
|
stepInput?: PreparedStep;
|
|
274
|
-
action?: "continue" | "
|
|
258
|
+
action?: "continue" | "wait_for_user" | "wait" | "complete" | "fail";
|
|
275
259
|
};
|
|
276
260
|
export type InputHandlers = {
|
|
261
|
+
checkpoints?: Partial<Record<"beforeModel" | "afterModel" | "afterStep", (payload: PendingInput["payload"], state: Inspection, context: {
|
|
262
|
+
inputId: string;
|
|
263
|
+
parentSpanId: string;
|
|
264
|
+
signal: AbortSignal;
|
|
265
|
+
}) => Promise<InputResponse>>>;
|
|
277
266
|
tools?: Record<string, (args: Json, context: {
|
|
278
267
|
inputId: string;
|
|
279
268
|
requestId: string;
|
|
280
269
|
signal?: AbortSignal;
|
|
281
270
|
}) => Promise<Json>>;
|
|
282
|
-
prepareStep?: (state: Inspection, context: {
|
|
283
|
-
inputId: string;
|
|
284
|
-
parentSpanId: string;
|
|
285
|
-
signal: AbortSignal;
|
|
286
|
-
}) => Promise<Pick<InputResponse, "instructions" | "tools" | "stepInput" | "action">>;
|
|
287
|
-
callerReview?: (review: CallerReview, state: Inspection, context: {
|
|
288
|
-
inputId: string;
|
|
289
|
-
signal: AbortSignal;
|
|
290
|
-
}) => Promise<Pick<InputResponse, "action" | "result">>;
|
|
291
271
|
userInput?: (state: Inspection, context: {
|
|
292
272
|
inputId: string;
|
|
293
273
|
signal: AbortSignal;
|
|
@@ -297,8 +277,6 @@ export type InputHandlers = {
|
|
|
297
277
|
toolTimeoutMs?: number;
|
|
298
278
|
onUpdate?: (state: Inspection) => void;
|
|
299
279
|
};
|
|
300
|
-
/** @deprecated Use InputHandlers. */
|
|
301
|
-
export type DriveOptions = InputHandlers;
|
|
302
280
|
export type RequestEvent = {
|
|
303
281
|
seq: number;
|
|
304
282
|
type: string;
|
|
@@ -331,11 +309,11 @@ export type ModelResolution = ModelScope & {
|
|
|
331
309
|
export type WebhookEvent = {
|
|
332
310
|
requestId: string;
|
|
333
311
|
inputId: string;
|
|
334
|
-
kind: "
|
|
312
|
+
kind: PendingInput["kind"];
|
|
335
313
|
payload: Record<string, Json>;
|
|
336
314
|
} | {
|
|
337
315
|
requestId: string;
|
|
338
|
-
kind: "request.
|
|
316
|
+
kind: "request.events";
|
|
339
317
|
afterSeq: number;
|
|
340
318
|
events: Json[];
|
|
341
319
|
} | {
|
|
@@ -446,11 +424,9 @@ export declare class Constellation {
|
|
|
446
424
|
signal?: AbortSignal;
|
|
447
425
|
}): AsyncGenerator<RequestEvent>;
|
|
448
426
|
/** Process one delivered input and exit. An input notification can be retried safely. */
|
|
449
|
-
handleInput
|
|
450
|
-
/**
|
|
451
|
-
|
|
452
|
-
targetInputId?: string;
|
|
453
|
-
}): Promise<Inspection>;
|
|
427
|
+
private handleInput;
|
|
428
|
+
/** Handle pending inputs until the request ends. */
|
|
429
|
+
private processInputs;
|
|
454
430
|
/** Client-safe discovery with an execution key. */
|
|
455
431
|
get models(): {
|
|
456
432
|
list: (scope: ModelScope, signal?: AbortSignal) => Promise<Json>;
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ export class Constellation {
|
|
|
91
91
|
: response.json();
|
|
92
92
|
}
|
|
93
93
|
submit(input, signal) {
|
|
94
|
-
return this.call("/v1/requests", "POST", input
|
|
94
|
+
return this.call("/v1/requests", "POST", input, signal);
|
|
95
95
|
}
|
|
96
96
|
inspect(id, signal) {
|
|
97
97
|
return this.call(`/v1/requests/${encodeURIComponent(id)}`, "GET", undefined, signal);
|
|
@@ -116,7 +116,7 @@ export class Constellation {
|
|
|
116
116
|
return this.call(`/v1/requests/${encodeURIComponent(id)}/records/${kind}?${params}`, "GET", undefined, signal);
|
|
117
117
|
}
|
|
118
118
|
resolve(input, signal) {
|
|
119
|
-
return this.call("/v1/requests/resolve", "POST", input
|
|
119
|
+
return this.call("/v1/requests/resolve", "POST", input, signal);
|
|
120
120
|
}
|
|
121
121
|
list(query = {}, signal) {
|
|
122
122
|
const params = new URLSearchParams();
|
|
@@ -281,10 +281,10 @@ export class Constellation {
|
|
|
281
281
|
}
|
|
282
282
|
/** Process one delivered input and exit. An input notification can be retried safely. */
|
|
283
283
|
async handleInput(id, inputId, options = {}) {
|
|
284
|
-
return this.
|
|
284
|
+
return this.processInputs(id, { ...options, targetInputId: inputId });
|
|
285
285
|
}
|
|
286
|
-
/**
|
|
287
|
-
async
|
|
286
|
+
/** Handle pending inputs until the request ends. */
|
|
287
|
+
async processInputs(id, options = {}) {
|
|
288
288
|
const consumerId = options.consumerId ?? uuid7();
|
|
289
289
|
while (true) {
|
|
290
290
|
const state = await this.inspect(id, options.signal);
|
|
@@ -301,48 +301,13 @@ export class Constellation {
|
|
|
301
301
|
let handled = false;
|
|
302
302
|
for (const pending of state.inputs.filter((i) => !i.resolvedAt && (!options.targetInputId || i.id === options.targetInputId))) {
|
|
303
303
|
if (pending.claimId && pending.claimId !== consumerId &&
|
|
304
|
-
!(
|
|
304
|
+
!(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
305
305
|
continue;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
catch (error) {
|
|
312
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
313
|
-
continue;
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
317
|
-
let closed = false;
|
|
318
|
-
const preparation = await this.startSpan(id, "caller.prepare_step", { inputId: pending.id }, activeSignal);
|
|
319
|
-
try {
|
|
320
|
-
const signal = activeSignal
|
|
321
|
-
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
322
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
323
|
-
const response = await bounded(prepareStep(state, { inputId: pending.id, parentSpanId: preparation.id, signal }), signal);
|
|
324
|
-
await preparation.end({}, activeSignal);
|
|
325
|
-
closed = true;
|
|
326
|
-
if (state.request.kind === "agent")
|
|
327
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
328
|
-
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
if (!activeSignal?.aborted && !closed)
|
|
332
|
-
await preparation.end({ status: "error" }, activeSignal).catch(() => { });
|
|
333
|
-
throw error;
|
|
334
|
-
}
|
|
335
|
-
}, options.signal);
|
|
336
|
-
handled = true;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
const callerReview = options.callerReview;
|
|
340
|
-
if (pending.kind === "caller_review" && callerReview) {
|
|
341
|
-
const stage = pending.payload.stage;
|
|
342
|
-
if (!pending.stepId || (stage !== "after_model" && stage !== "after_tools"))
|
|
343
|
-
throw new Error("Caller review is missing its step or stage");
|
|
344
|
-
const stepId = pending.stepId;
|
|
345
|
-
const review = { ...pending.payload, stepId, stage };
|
|
306
|
+
if (pending.kind === "before_model" || pending.kind === "after_model" || pending.kind === "after_step") {
|
|
307
|
+
const name = pending.kind === "before_model" ? "beforeModel" : pending.kind === "after_model" ? "afterModel" : "afterStep";
|
|
308
|
+
const handler = options.checkpoints?.[name];
|
|
309
|
+
if (!handler)
|
|
310
|
+
continue;
|
|
346
311
|
try {
|
|
347
312
|
await this.claim(id, pending.id, consumerId, options.signal);
|
|
348
313
|
}
|
|
@@ -352,21 +317,21 @@ export class Constellation {
|
|
|
352
317
|
throw error;
|
|
353
318
|
}
|
|
354
319
|
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
355
|
-
const
|
|
320
|
+
const span = await this.startSpan(id, `caller.${name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
356
321
|
let closed = false;
|
|
357
322
|
try {
|
|
358
323
|
const signal = activeSignal
|
|
359
324
|
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
360
325
|
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
361
|
-
const response = await bounded(
|
|
362
|
-
await
|
|
326
|
+
const response = await bounded(handler(pending.payload, state, { inputId: pending.id, parentSpanId: span.id, signal }), signal);
|
|
327
|
+
await span.end({}, activeSignal);
|
|
363
328
|
closed = true;
|
|
364
329
|
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
365
330
|
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
366
331
|
}
|
|
367
332
|
catch (error) {
|
|
368
333
|
if (!activeSignal?.aborted && !closed)
|
|
369
|
-
await
|
|
334
|
+
await span.end({ status: "error" }, activeSignal).catch(() => { });
|
|
370
335
|
throw error;
|
|
371
336
|
}
|
|
372
337
|
}, options.signal);
|
|
@@ -383,13 +348,12 @@ export class Constellation {
|
|
|
383
348
|
continue;
|
|
384
349
|
throw error;
|
|
385
350
|
}
|
|
386
|
-
await this.whileClaimed(id, pending.id, consumerId,
|
|
351
|
+
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
387
352
|
const signal = activeSignal
|
|
388
353
|
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
389
354
|
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
390
355
|
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
391
|
-
|
|
392
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
356
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
393
357
|
await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
394
358
|
}, options.signal);
|
|
395
359
|
handled = true;
|
|
@@ -408,7 +372,7 @@ export class Constellation {
|
|
|
408
372
|
continue;
|
|
409
373
|
throw error;
|
|
410
374
|
}
|
|
411
|
-
await this.whileClaimed(id, pending.id, consumerId,
|
|
375
|
+
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
412
376
|
const span = await this.startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
413
377
|
let result;
|
|
414
378
|
let isError = false;
|
|
@@ -432,8 +396,7 @@ export class Constellation {
|
|
|
432
396
|
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
433
397
|
}
|
|
434
398
|
await span.end({ status: isError ? "error" : "ok" }, activeSignal);
|
|
435
|
-
|
|
436
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
399
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
437
400
|
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
438
401
|
}, options.signal);
|
|
439
402
|
handled = true;
|
|
@@ -474,7 +437,7 @@ export class Constellation {
|
|
|
474
437
|
get inputs() {
|
|
475
438
|
return {
|
|
476
439
|
handle: (requestId, inputId, handlers) => this.handleInput(requestId, inputId, handlers),
|
|
477
|
-
poll: (requestId, handlers) => this.
|
|
440
|
+
poll: (requestId, handlers) => this.processInputs(requestId, handlers),
|
|
478
441
|
claim: (requestId, inputId, claimId, signal) => this.claim(requestId, inputId, claimId, signal),
|
|
479
442
|
respond: (requestId, inputId, response, signal) => this.respond(requestId, inputId, response, signal),
|
|
480
443
|
};
|
|
@@ -584,12 +547,12 @@ export class Constellation {
|
|
|
584
547
|
const event = body;
|
|
585
548
|
if (typeof event.requestId !== "string" || !event.requestId)
|
|
586
549
|
throw new Error("Invalid Constellation request ID");
|
|
587
|
-
if (typeof event.inputId === "string" && ["tool", "
|
|
550
|
+
if (typeof event.inputId === "string" && ["tool", "before_model", "after_model", "after_step", "user_input"].includes(String(event.kind))) {
|
|
588
551
|
if (request.headers.get("x-constellation-input-id") !== event.inputId || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload))
|
|
589
552
|
throw new Error("Invalid Constellation input notification");
|
|
590
553
|
return event;
|
|
591
554
|
}
|
|
592
|
-
if (event.kind === "request.
|
|
555
|
+
if (event.kind === "request.events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
|
|
593
556
|
return event;
|
|
594
557
|
if (["request.completed", "request.failed", "request.cancelled"].includes(String(event.kind)) && Number.isSafeInteger(event.eventSeq))
|
|
595
558
|
return event;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lionhummer/constellation",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TypeScript client for durable model requests and caller
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TypeScript client for durable model requests and caller events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"repository": {
|