@lionhummer/constellation 0.1.1 → 0.1.2
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 +128 -98
- package/dist/index.d.ts +51 -5
- package/dist/index.js +145 -49
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,125 +1,155 @@
|
|
|
1
1
|
# Constellation TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Constellation runs durable model requests and agent steps. This SDK submits requests, streams events, handles caller waits, and records spans and logs. Your application owns its conversation, tool implementations, user interaction, and any accounting or policy decisions.
|
|
4
4
|
|
|
5
|
-
Install
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
## Install and connect
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @lionhummer/constellation
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use an execution API key from a trusted server. The SDK sends it as a bearer token to the `/v1` API.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import { Constellation, uuid7 } from "@lionhummer/constellation";
|
|
15
15
|
|
|
16
|
-
const client = new Constellation({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
externalId: threadId, // Search/group identifier; no shared history.
|
|
20
|
-
workspaceId,
|
|
21
|
-
model: "obvious/fusion-1p0", // Physical model or virtual SKU.
|
|
22
|
-
kind: "run",
|
|
23
|
-
messages: [{ role: "user", text: "Echo hello using the tool." }],
|
|
24
|
-
tools: [{
|
|
25
|
-
name: "echo",
|
|
26
|
-
inputSchema: {
|
|
27
|
-
type: "object",
|
|
28
|
-
properties: { text: { type: "string" } },
|
|
29
|
-
required: ["text"],
|
|
30
|
-
additionalProperties: false,
|
|
31
|
-
},
|
|
32
|
-
}],
|
|
16
|
+
const client = new Constellation({
|
|
17
|
+
baseUrl: "https://constellation.example.com",
|
|
18
|
+
apiKey: process.env.CONSTELLATION_API_KEY,
|
|
33
19
|
});
|
|
20
|
+
```
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
The default HTTP timeout is 30 seconds per call. Pass `timeoutMs` to the constructor or an `AbortSignal` to an individual method to change it. Keep API keys out of browser code.
|
|
23
|
+
|
|
24
|
+
## Make a request
|
|
25
|
+
|
|
26
|
+
`generate` makes one model call. `run` can pause for tool results or a context refresh. `agent` gives the caller control before every model step and after each step's output.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const { id } = await client.submit({
|
|
30
|
+
idempotencyKey: uuid7(),
|
|
31
|
+
workspaceId: "workspace-123",
|
|
32
|
+
externalId: "conversation-456",
|
|
33
|
+
model: "catalog/example-model",
|
|
34
|
+
kind: "generate",
|
|
35
|
+
messages: [{ role: "user", text: "Explain the result." }],
|
|
44
36
|
});
|
|
45
|
-
|
|
37
|
+
|
|
38
|
+
for await (const event of client.events(id)) {
|
|
39
|
+
console.log(event.seq, event.type, event.data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const final = await client.inspect(id);
|
|
43
|
+
console.log(final.request.status, final.request.output);
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
Persist `idempotencyKey` and reuse it if submission is retried. `externalId` groups and finds requests; it does not carry conversation history into the next request. To resume the event stream, persist the last event's `seq` and call `events(id, { after: lastSeq })`. The stream uses SSE and replays from a durable cursor after a disconnect. Disconnecting does not cancel the request; call `cancel(id)` to do that.
|
|
47
|
+
|
|
48
|
+
## Run an agent
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
An agent request starts with a `prepare_step` wait. Supply the **complete** step input there, including conversation messages and any updated context. Constellation executes the model call, then sends a `caller_review` wait with usage, cost, output, and pending tool calls. Tool handlers run only after the caller returns `continue`. After tools finish, a second review lets the caller choose another step or complete the request.
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
import type { Message } from "@lionhummer/constellation";
|
|
54
|
+
|
|
55
|
+
const history: Message[] = [{ role: "user", text: "Find the answer." }];
|
|
56
|
+
const { id } = await client.submit({
|
|
57
|
+
idempotencyKey: uuid7(),
|
|
58
|
+
workspaceId: "workspace-123",
|
|
59
|
+
model: "catalog/example-model",
|
|
60
|
+
kind: "agent",
|
|
61
|
+
maxSteps: 4,
|
|
62
|
+
prepareStep: true,
|
|
63
|
+
messages: history,
|
|
56
64
|
});
|
|
57
|
-
|
|
58
|
-
await client.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
65
|
+
|
|
66
|
+
const final = await client.drive(id, {
|
|
67
|
+
consumerId: uuid7(), // Persist this ID if this consumer may reconnect.
|
|
68
|
+
|
|
69
|
+
prepareStep: async (state, { signal }) => {
|
|
70
|
+
signal.throwIfAborted();
|
|
71
|
+
return {
|
|
72
|
+
stepInput: {
|
|
73
|
+
model: state.request.requestedModel,
|
|
74
|
+
instructions: "Answer accurately and cite the tool result.",
|
|
75
|
+
messages: [...history],
|
|
76
|
+
tools: [{
|
|
77
|
+
name: "lookup",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: { query: { type: "string" } },
|
|
81
|
+
required: ["query"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
}],
|
|
85
|
+
parameters: {},
|
|
86
|
+
maxOutputTokens: 1024,
|
|
87
|
+
cache: null,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
callerReview: async (review, _state, { inputId, signal }) => {
|
|
93
|
+
signal.throwIfAborted();
|
|
94
|
+
if (review.stage === "after_model") {
|
|
95
|
+
// If you account for usage, settle this review once using inputId.
|
|
96
|
+
// Usage, cost, output and tool calls are available before tool dispatch.
|
|
97
|
+
const output = review.output as { message?: string } | undefined;
|
|
98
|
+
history.push({
|
|
99
|
+
role: "assistant", text: output?.message ?? "",
|
|
100
|
+
toolCalls: review.toolCalls ?? [],
|
|
101
|
+
});
|
|
102
|
+
return { action: review.toolCalls?.length ? "continue" : "complete" };
|
|
103
|
+
}
|
|
104
|
+
// after_tools contains tool results, not a second model charge.
|
|
105
|
+
history.push({ role: "tool", toolResults: review.toolResults ?? [] });
|
|
106
|
+
return { action: "continue" };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
tools: {
|
|
110
|
+
lookup: async (args, { inputId, signal }) => {
|
|
111
|
+
signal?.throwIfAborted();
|
|
112
|
+
// Deduplicate external side effects by inputId.
|
|
113
|
+
return { answer: JSON.stringify(args) };
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log(final.request.status, final.steps);
|
|
62
119
|
```
|
|
63
120
|
|
|
64
|
-
|
|
121
|
+
Build `stepInput.messages` from your current conversation each time `prepareStep` runs. Adapt the example’s text-only history handling if your provider returns richer content. Make history updates idempotent by `inputId` so a retried review does not duplicate messages. The review handler may return `continue`, `complete`, `wait_for_user`, or `fail`. A `wait_for_user` action creates a durable `user_input` wait; provide `userInput` to `drive()` or claim and respond to that input manually. Use `toolTimeoutMs` to bound callbacks if they need more than the 30-second default.
|
|
122
|
+
|
|
123
|
+
`drive()` polls request inspection to find pending inputs. It claims them before invoking callbacks, renews agent claims while callbacks run, and writes the response. Agent claims have a 60-second lease. A replacement consumer can take over after expiry, so use each `inputId` as an idempotency key for accounting or external tool effects. Reuse a stable `consumerId` when reconnecting the same consumer.
|
|
65
124
|
|
|
66
|
-
|
|
125
|
+
## Record caller work
|
|
67
126
|
|
|
68
|
-
|
|
127
|
+
Spans can measure preparation, tool work, or a caller review. Set `inputId` to nest a span under that wait in the request timeline. The SDK's `drive()` helper automatically creates spans around its preparation, review, and tool callbacks; you can add your own:
|
|
69
128
|
|
|
70
129
|
```ts
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
130
|
+
const span = await client.startSpan(id, "application.context_lookup", {
|
|
131
|
+
inputId: "pending-input-id",
|
|
132
|
+
startedAt: new Date().toISOString(),
|
|
133
|
+
});
|
|
75
134
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
and the current `effectiveInput` after application context refreshes.
|
|
135
|
+
// Perform the work and close the span before resolving the wait.
|
|
136
|
+
await span.end({ status: "ok" });
|
|
137
|
+
```
|
|
80
138
|
|
|
81
|
-
|
|
82
|
-
`records(id, "logs", { level: "error", q: "tool", offset: 0, limit: 25 })`.
|
|
83
|
-
Supported record kinds are `steps`, `attempts`, `inputs`, `spans`, and `logs`;
|
|
84
|
-
responses contain `items`, `total`, `offset`, and `limit`. Summary inspection
|
|
85
|
-
intentionally contains empty telemetry arrays; `inspect` retains its full behavior.
|
|
139
|
+
Use `addSpans()` and `addLogs()` for batches. Caller timestamps are accepted, including work that started before submission. Submit and close spans before the request becomes terminal; later writes return HTTP 409.
|
|
86
140
|
|
|
87
|
-
##
|
|
141
|
+
## Inspect and recover
|
|
88
142
|
|
|
89
|
-
| Method |
|
|
143
|
+
| Method | Purpose |
|
|
90
144
|
| --- | --- |
|
|
91
|
-
| `submit(input
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
| `addLogs(id, logs, signal?)` | Append caller logs. |
|
|
103
|
-
|
|
104
|
-
`RequestInput` requires `idempotencyKey`, `workspaceId`, `model`, `kind`, and
|
|
105
|
-
`messages`. It also accepts `externalId`, `userId`, `planId`, `instructions`,
|
|
106
|
-
`tools`, `parameters`, `maxSteps`, `timeoutMs`, `maxOutputTokens`,
|
|
107
|
-
`prepareStep` and `cache`. Set `cache.mode` to `automatic` for provider-native
|
|
108
|
-
prefix caching or `explicit` with up to four `points` for exact breakpoints.
|
|
109
|
-
Points target tools, instructions, or a message index and optional `partIndex`
|
|
110
|
-
within `contentParts`. Optional `affinityKey` and `isolationKey` are sent only
|
|
111
|
-
through fields configured on the selected provider. An isolation key excludes
|
|
112
|
-
providers without a mapping; explicit mode excludes providers without exact
|
|
113
|
-
marker support. The selected attempt records whether affinity was applied.
|
|
114
|
-
`kind: "generate"` is a single generation;
|
|
115
|
-
`kind: "run"` can pause for tools or context refreshes. The SDK exports
|
|
116
|
-
`uuid7`, typed request/inspection records, and `ConstellationError` with HTTP status,
|
|
117
|
-
error code, and message. All calls use the configured timeout (30 seconds by
|
|
118
|
-
default) and an optional abort signal.
|
|
119
|
-
|
|
120
|
-
This package covers execution requests. Model discovery (`GET /v1/models`,
|
|
121
|
-
`POST /v1/registry/resolve`), provider HTTP capture, the management APIs, and
|
|
122
|
-
operator APIs are available over HTTP; see the [HTTP API guide](../../docs/registry-api.md)
|
|
123
|
-
and the in-app Documentation page for those endpoint contracts. `events()` uses
|
|
124
|
-
SSE and reconnects from the durable cursor. Aborting `drive()` disconnects the consumer but does not
|
|
125
|
-
cancel the Constellation request.
|
|
145
|
+
| `submit(input)` / `resolve(input)` | Submit a durable request, or preview routing and rules without dispatching. |
|
|
146
|
+
| `events(id, { after, signal })` | Stream live events with durable cursor replay. |
|
|
147
|
+
| `inspect(id)` / `inspectSummary(id)` | Read the full request or a lightweight summary. |
|
|
148
|
+
| `records(id, kind, query)` | Page `steps`, `attempts`, `inputs`, `spans`, or `logs`. |
|
|
149
|
+
| `list(query)` | Page and filter requests. |
|
|
150
|
+
| `drive(id, options)` | Handle pending preparation, review, tool, and user input waits until terminal. |
|
|
151
|
+
| `claim(id, inputId, consumerId)` / `respond(id, inputId, response)` | Handle a wait manually. |
|
|
152
|
+
| `startSpan()` / `addSpans()` / `addLogs()` | Attach caller telemetry. |
|
|
153
|
+
| `cancel(id)` | Cancel a request explicitly. |
|
|
154
|
+
|
|
155
|
+
For large histories, use `inspectSummary(id)` and page the needed records, for example `records(id, "logs", { level: "error", limit: 25, offset: 0 })`. `ConstellationError` exposes the HTTP `status`, server `code`, and message. See the [execution contract](https://github.com/FlatFilers/constellation/blob/main/docs/execution.md) for request states, caching, limits, and recovery semantics.
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type RequestInput = {
|
|
|
32
32
|
userId?: string;
|
|
33
33
|
planId?: string;
|
|
34
34
|
model: string;
|
|
35
|
-
kind: "generate" | "run";
|
|
35
|
+
kind: "generate" | "run" | "agent";
|
|
36
36
|
instructions?: string;
|
|
37
37
|
messages: Message[];
|
|
38
38
|
tools?: Tool[];
|
|
@@ -60,7 +60,7 @@ export type RequestRecord = {
|
|
|
60
60
|
externalId: string | null;
|
|
61
61
|
workspaceId: string;
|
|
62
62
|
userId: string | null;
|
|
63
|
-
kind: "generate" | "run";
|
|
63
|
+
kind: "generate" | "run" | "agent";
|
|
64
64
|
requestedModel: string;
|
|
65
65
|
virtualModel: boolean;
|
|
66
66
|
planId: string | null;
|
|
@@ -88,6 +88,9 @@ export type Step = {
|
|
|
88
88
|
idx: number;
|
|
89
89
|
input: Json;
|
|
90
90
|
inputHash: string;
|
|
91
|
+
catalogRevision: number | null;
|
|
92
|
+
resolution: Json;
|
|
93
|
+
rules: Json;
|
|
91
94
|
status: string;
|
|
92
95
|
output: Json;
|
|
93
96
|
usage: Json;
|
|
@@ -127,18 +130,47 @@ export type Attempt = {
|
|
|
127
130
|
startedAt: string;
|
|
128
131
|
endedAt: string | null;
|
|
129
132
|
};
|
|
133
|
+
export type PreparedStep = {
|
|
134
|
+
model: string;
|
|
135
|
+
instructions: string;
|
|
136
|
+
messages: Message[];
|
|
137
|
+
tools: Tool[];
|
|
138
|
+
parameters: Record<string, Json>;
|
|
139
|
+
maxOutputTokens: number;
|
|
140
|
+
cache: RequestInput["cache"] | null;
|
|
141
|
+
};
|
|
142
|
+
export type CallerReview = {
|
|
143
|
+
stage: "after_model" | "after_tools";
|
|
144
|
+
stepId: string;
|
|
145
|
+
toolCalls?: ToolCall[];
|
|
146
|
+
toolResults?: Message["toolResults"];
|
|
147
|
+
usage?: Json;
|
|
148
|
+
customerCost?: string | null;
|
|
149
|
+
supplierCost?: string | null;
|
|
150
|
+
customerPrice?: Json;
|
|
151
|
+
output?: Json;
|
|
152
|
+
};
|
|
130
153
|
export type PendingInput = {
|
|
131
154
|
id: string;
|
|
132
155
|
requestId: string;
|
|
133
156
|
stepId: string | null;
|
|
134
|
-
kind: "tool" | "prepare_step" | "user_input";
|
|
157
|
+
kind: "tool" | "prepare_step" | "caller_review" | "user_input";
|
|
135
158
|
payload: {
|
|
136
159
|
providerCallId?: string;
|
|
137
160
|
name?: string;
|
|
138
161
|
arguments?: Json;
|
|
139
162
|
validationError?: string | null;
|
|
163
|
+
stage?: CallerReview["stage"];
|
|
164
|
+
toolCalls?: ToolCall[];
|
|
165
|
+
toolResults?: Message["toolResults"];
|
|
166
|
+
usage?: Json;
|
|
167
|
+
customerCost?: string | null;
|
|
168
|
+
supplierCost?: string | null;
|
|
169
|
+
customerPrice?: Json;
|
|
170
|
+
output?: Json;
|
|
140
171
|
};
|
|
141
172
|
claimId: string | null;
|
|
173
|
+
claimUntil?: string | null;
|
|
142
174
|
response: Json;
|
|
143
175
|
resolvedAt: string | null;
|
|
144
176
|
};
|
|
@@ -146,6 +178,7 @@ export type Span = {
|
|
|
146
178
|
id: string;
|
|
147
179
|
stepId?: string | null;
|
|
148
180
|
attemptId?: string | null;
|
|
181
|
+
inputId?: string | null;
|
|
149
182
|
parentSpanId?: string | null;
|
|
150
183
|
name: string;
|
|
151
184
|
startedAt: string;
|
|
@@ -186,7 +219,8 @@ export type InputResponse = {
|
|
|
186
219
|
isError?: boolean;
|
|
187
220
|
instructions?: string;
|
|
188
221
|
tools?: Tool[];
|
|
189
|
-
|
|
222
|
+
stepInput?: PreparedStep;
|
|
223
|
+
action?: "continue" | "refresh_context" | "wait_for_user" | "complete" | "fail";
|
|
190
224
|
};
|
|
191
225
|
export type RequestEvent = {
|
|
192
226
|
seq: number;
|
|
@@ -265,12 +299,16 @@ export declare class Constellation {
|
|
|
265
299
|
}>;
|
|
266
300
|
cancel(id: string, signal?: AbortSignal): Promise<void>;
|
|
267
301
|
claim(id: string, inputId: string, claimId: string, signal?: AbortSignal): Promise<void>;
|
|
302
|
+
/** Agent claims are leases. Keep one alive while a caller handler is running. */
|
|
303
|
+
private maintainClaim;
|
|
304
|
+
private whileClaimed;
|
|
268
305
|
respond(id: string, inputId: string, response: InputResponse, signal?: AbortSignal): Promise<void>;
|
|
269
306
|
addSpans(id: string, spans: Span[], signal?: AbortSignal): Promise<void>;
|
|
270
307
|
addLogs(id: string, logs: Log[], signal?: AbortSignal): Promise<void>;
|
|
271
308
|
startSpan(id: string, name: string, options?: {
|
|
272
309
|
stepId?: string;
|
|
273
310
|
attemptId?: string;
|
|
311
|
+
inputId?: string;
|
|
274
312
|
parentSpanId?: string;
|
|
275
313
|
startedAt?: string;
|
|
276
314
|
attributes?: Record<string, Json>;
|
|
@@ -297,7 +335,15 @@ export declare class Constellation {
|
|
|
297
335
|
prepareStep?: (state: Inspection, context: {
|
|
298
336
|
inputId: string;
|
|
299
337
|
signal: AbortSignal;
|
|
300
|
-
}) => Promise<Pick<InputResponse, "instructions" | "tools" | "action">>;
|
|
338
|
+
}) => Promise<Pick<InputResponse, "instructions" | "tools" | "stepInput" | "action">>;
|
|
339
|
+
callerReview?: (review: CallerReview, state: Inspection, context: {
|
|
340
|
+
inputId: string;
|
|
341
|
+
signal: AbortSignal;
|
|
342
|
+
}) => Promise<Pick<InputResponse, "action" | "result">>;
|
|
343
|
+
userInput?: (state: Inspection, context: {
|
|
344
|
+
inputId: string;
|
|
345
|
+
signal: AbortSignal;
|
|
346
|
+
}) => Promise<Json>;
|
|
301
347
|
signal?: AbortSignal;
|
|
302
348
|
consumerId?: string;
|
|
303
349
|
toolTimeoutMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,48 @@ export class Constellation {
|
|
|
113
113
|
claim(id, inputId, claimId, signal) {
|
|
114
114
|
return this.call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}/claim`, "POST", { claimId }, signal);
|
|
115
115
|
}
|
|
116
|
+
/** Agent claims are leases. Keep one alive while a caller handler is running. */
|
|
117
|
+
maintainClaim(id, inputId, claimId, externalSignal) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const signal = externalSignal
|
|
120
|
+
? AbortSignal.any([externalSignal, controller.signal])
|
|
121
|
+
: controller.signal;
|
|
122
|
+
const renewal = (async () => {
|
|
123
|
+
while (!signal.aborted) {
|
|
124
|
+
try {
|
|
125
|
+
await pause(15_000, signal);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (signal.aborted)
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
await this.claim(id, inputId, claimId, signal);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
controller.abort(error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
return {
|
|
142
|
+
signal,
|
|
143
|
+
stop: async () => {
|
|
144
|
+
controller.abort();
|
|
145
|
+
await renewal;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async whileClaimed(id, inputId, claimId, leased, operation, signal) {
|
|
150
|
+
const lease = leased ? this.maintainClaim(id, inputId, claimId, signal) : null;
|
|
151
|
+
try {
|
|
152
|
+
return await operation(lease?.signal ?? signal);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await lease?.stop();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
116
158
|
respond(id, inputId, response, signal) {
|
|
117
159
|
return this.call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}`, "POST", response, signal);
|
|
118
160
|
}
|
|
@@ -228,9 +270,11 @@ export class Constellation {
|
|
|
228
270
|
if (terminal(state.request.status))
|
|
229
271
|
return state;
|
|
230
272
|
for (const pending of state.inputs.filter((i) => !i.resolvedAt)) {
|
|
231
|
-
if (pending.claimId && pending.claimId !== consumerId
|
|
273
|
+
if (pending.claimId && pending.claimId !== consumerId &&
|
|
274
|
+
!(state.request.kind === "agent" && pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
232
275
|
continue;
|
|
233
|
-
|
|
276
|
+
const prepareStep = options.prepareStep;
|
|
277
|
+
if (pending.kind === "prepare_step" && prepareStep) {
|
|
234
278
|
try {
|
|
235
279
|
await this.claim(id, pending.id, consumerId, options.signal);
|
|
236
280
|
}
|
|
@@ -239,27 +283,83 @@ export class Constellation {
|
|
|
239
283
|
continue;
|
|
240
284
|
throw error;
|
|
241
285
|
}
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
287
|
+
let closed = false;
|
|
288
|
+
const preparation = await this.startSpan(id, "caller.prepare_step", { inputId: pending.id }, activeSignal);
|
|
289
|
+
try {
|
|
290
|
+
const signal = activeSignal
|
|
291
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
292
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
293
|
+
const response = await bounded(prepareStep(state, { inputId: pending.id, signal }), signal);
|
|
294
|
+
await preparation.end({}, activeSignal);
|
|
295
|
+
closed = true;
|
|
296
|
+
if (state.request.kind === "agent")
|
|
297
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
298
|
+
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (!activeSignal?.aborted && !closed)
|
|
302
|
+
await preparation.end({ status: "error" }, activeSignal).catch(() => { });
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}, options.signal);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const callerReview = options.callerReview;
|
|
309
|
+
if (pending.kind === "caller_review" && callerReview) {
|
|
310
|
+
const stage = pending.payload.stage;
|
|
311
|
+
if (!pending.stepId || (stage !== "after_model" && stage !== "after_tools"))
|
|
312
|
+
throw new Error("Caller review is missing its step or stage");
|
|
313
|
+
const stepId = pending.stepId;
|
|
314
|
+
const review = { ...pending.payload, stepId, stage };
|
|
244
315
|
try {
|
|
245
|
-
|
|
246
|
-
? AbortSignal.any([
|
|
247
|
-
options.signal,
|
|
248
|
-
AbortSignal.timeout(options.toolTimeoutMs ?? 30000),
|
|
249
|
-
])
|
|
250
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
251
|
-
const response = await bounded(options.prepareStep(state, { inputId: pending.id, signal }), signal);
|
|
252
|
-
await preparation.end({}, options.signal);
|
|
253
|
-
closed = true;
|
|
254
|
-
await this.respond(id, pending.id, { ...response, claimId: consumerId }, options.signal);
|
|
316
|
+
await this.claim(id, pending.id, consumerId, options.signal);
|
|
255
317
|
}
|
|
256
318
|
catch (error) {
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
319
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
320
|
+
continue;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
324
|
+
const reviewSpan = await this.startSpan(id, "caller.review", { stepId, inputId: pending.id, attributes: { stage } }, activeSignal);
|
|
325
|
+
let closed = false;
|
|
326
|
+
try {
|
|
327
|
+
const signal = activeSignal
|
|
328
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
329
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
330
|
+
const response = await bounded(callerReview(review, state, { inputId: pending.id, signal }), signal);
|
|
331
|
+
await reviewSpan.end({}, activeSignal);
|
|
332
|
+
closed = true;
|
|
333
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
334
|
+
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (!activeSignal?.aborted && !closed)
|
|
338
|
+
await reviewSpan.end({ status: "error" }, activeSignal).catch(() => { });
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}, options.signal);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const userInput = options.userInput;
|
|
345
|
+
if (pending.kind === "user_input" && userInput) {
|
|
346
|
+
try {
|
|
347
|
+
await this.claim(id, pending.id, consumerId, options.signal);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
351
|
+
continue;
|
|
261
352
|
throw error;
|
|
262
353
|
}
|
|
354
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
355
|
+
const signal = activeSignal
|
|
356
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
357
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
358
|
+
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
359
|
+
if (state.request.kind === "agent")
|
|
360
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
361
|
+
await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
362
|
+
}, options.signal);
|
|
263
363
|
continue;
|
|
264
364
|
}
|
|
265
365
|
if (pending.kind !== "tool")
|
|
@@ -275,38 +375,34 @@ export class Constellation {
|
|
|
275
375
|
continue;
|
|
276
376
|
throw error;
|
|
277
377
|
}
|
|
278
|
-
|
|
279
|
-
stepId: pending.stepId ?? undefined,
|
|
280
|
-
|
|
378
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
379
|
+
const span = await this.startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
380
|
+
let result;
|
|
381
|
+
let isError = false;
|
|
382
|
+
try {
|
|
383
|
+
if (pending.payload.validationError)
|
|
384
|
+
throw new Error(pending.payload.validationError);
|
|
385
|
+
if (!tool)
|
|
386
|
+
throw new Error("Tool unavailable");
|
|
387
|
+
const toolSignal = activeSignal
|
|
388
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
389
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
390
|
+
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
391
|
+
inputId: pending.id,
|
|
392
|
+
requestId: id,
|
|
393
|
+
signal: toolSignal,
|
|
394
|
+
}), toolSignal);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
activeSignal?.throwIfAborted();
|
|
398
|
+
isError = true;
|
|
399
|
+
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
400
|
+
}
|
|
401
|
+
await span.end({ status: isError ? "error" : "ok" }, activeSignal);
|
|
402
|
+
if (state.request.kind === "agent")
|
|
403
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
404
|
+
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
281
405
|
}, options.signal);
|
|
282
|
-
let result;
|
|
283
|
-
let isError = false;
|
|
284
|
-
try {
|
|
285
|
-
if (pending.payload.validationError)
|
|
286
|
-
throw new Error(pending.payload.validationError);
|
|
287
|
-
if (!tool)
|
|
288
|
-
throw new Error("Tool unavailable");
|
|
289
|
-
const toolSignal = options.signal
|
|
290
|
-
? AbortSignal.any([
|
|
291
|
-
options.signal,
|
|
292
|
-
AbortSignal.timeout(options.toolTimeoutMs ?? 30000),
|
|
293
|
-
])
|
|
294
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
295
|
-
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
296
|
-
inputId: pending.id,
|
|
297
|
-
requestId: id,
|
|
298
|
-
signal: toolSignal,
|
|
299
|
-
}), toolSignal);
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
options.signal?.throwIfAborted();
|
|
303
|
-
isError = true;
|
|
304
|
-
result = {
|
|
305
|
-
error: error instanceof Error ? error.message : "Tool failed",
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
await span.end({ status: isError ? "error" : "ok" }, options.signal);
|
|
309
|
-
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, options.signal);
|
|
310
406
|
}
|
|
311
407
|
await pause(500, options.signal);
|
|
312
408
|
}
|
package/package.json
CHANGED