@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 CHANGED
@@ -1,172 +1,81 @@
1
- # Constellation TypeScript SDK
1
+ # @lionhummer/constellation
2
2
 
3
- `@lionhummer/constellation` is a server-side client for durable model execution. A request keeps running after `requests.create()` returns its ID. Constellation owns admission, routing, provider calls, waits and the execution trace. Your application supplies conversation state, tools and any policy or billing decisions.
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
- ```sh
5
+ ```bash
6
6
  npm install @lionhummer/constellation
7
7
  ```
8
8
 
9
9
  ```ts
10
- import { Constellation, uuid7, type InputHandlers } from "@lionhummer/constellation";
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
- messages: [{ role: "user", text: "Explain this result." }],
39
- });
40
-
41
- for await (const event of client.requests.streamEvents(id)) {
42
- // Store event.seq to resume after a disconnect.
43
- if (["request.completed", "request.failed", "request.cancelled"].includes(event.type)) break;
44
- }
45
- const final = await client.requests.get(id);
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
- `requests.preview(input)` applies access, pricing and routing rules without creating or dispatching a request. `models.list({ workspaceId })` discovers accessible models; `models.resolve({ workspaceId, targetId })` previews one model's candidates.
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
- ### Tool-capable run
34
+ ## Events and caller work
51
35
 
52
- Use `run` when tools should execute without an application review gate after each model response. Each tool call becomes a durable caller input. The same handlers work with webhook delivery and explicit polling:
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
- ```ts
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
- prepareStep: async (state, { signal, parentSpanId }) => ({
105
- stepInput: await buildCompleteNextStep(state, { signal, parentSpanId }),
106
- }),
107
- } satisfies InputHandlers;
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
- Deduplicate billing and tool side effects by input ID. The SDK claims an input, renews an agent claim while its handler runs, adds parent spans for review/preparation/tool callbacks, and responds. If handling fails transiently, return a non-2xx webhook response so delivery retries. A terminal request rejects new caller spans and logs.
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
- ## Webhooks and events
61
+ ## Webhook
113
62
 
114
- Configure the execution key's HTTPS `inputWebhook` URL and random token when creating the key, or later with `management.keys.setInputWebhook()`. The SDK can verify the token and parse the three callback shapes:
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, process.env.CONSTELLATION_WEBHOOK_TOKEN!);
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.model_events") {
121
- await storeDeltaBatchOnce(event.requestId, event.afterSeq, event.events);
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
- const state = await client.requests.getSummary(event.requestId);
124
- await finalizeOnce(event.requestId, event.eventSeq, state);
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
- Input callbacks include `{requestId,inputId,kind,payload}` and `x-constellation-input-id`. Model-event callbacks contain `{requestId,kind:"request.model_events",afterSeq,events}`. Terminal callbacks contain `{requestId,eventSeq,kind}`. All carry `x-constellation-webhook-token`. Delivery is at least once. Model deltas may arrive late; use the final inspection as authority after terminal.
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
- Top-level `logs.add()` writes caller logs on an execution key. `telemetry.logs` searches operational logs with a telemetry key. The API's exact parameter and response schemas are served at `/v1/openapi.json` and shown inline in the console's API reference.
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
- The client defaults to a 30-second HTTP timeout; configure `timeoutMs` or pass an `AbortSignal`. `ConstellationError` carries HTTP status, server code and message. Older flat method names remain as temporary aliases for existing callers; new integrations should use the names above.
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 agent definitions live here. */
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
- /** An agent always supplies its first complete step at admission. Later steps use prepare_step. */
60
- export type RequestInput = RequestFields & ({
61
- kind: "generate";
62
- tools?: never;
63
- prepareStep?: boolean;
64
- } | {
65
- kind: "run";
66
- prepareStep?: boolean;
67
- } | {
68
- kind: "agent";
69
- prepareStep?: true;
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" | "prepare_step" | "caller_review" | "user_input";
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" | "refresh_context" | "wait_for_user" | "complete" | "fail";
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: "tool" | "prepare_step" | "caller_review" | "user_input";
312
+ kind: PendingInput["kind"];
335
313
  payload: Record<string, Json>;
336
314
  } | {
337
315
  requestId: string;
338
- kind: "request.model_events";
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(id: string, inputId: string, options?: DriveOptions): Promise<Inspection>;
450
- /** Legacy long-running helper. Use handleInput from a durable notification handler. */
451
- drive(id: string, options?: DriveOptions & {
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.kind === "agent" ? { ...input, prepareStep: true } : input, signal);
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.kind === "agent" ? { ...input, prepareStep: true } : input, signal);
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.drive(id, { ...options, targetInputId: inputId });
284
+ return this.processInputs(id, { ...options, targetInputId: inputId });
285
285
  }
286
- /** Legacy long-running helper. Use handleInput from a durable notification handler. */
287
- async drive(id, options = {}) {
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
- !(state.request.kind === "agent" && pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
304
+ !(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
305
305
  continue;
306
- const prepareStep = options.prepareStep;
307
- if (pending.kind === "prepare_step" && prepareStep) {
308
- try {
309
- await this.claim(id, pending.id, consumerId, options.signal);
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 reviewSpan = await this.startSpan(id, "caller.review", { stepId, inputId: pending.id, attributes: { stage } }, activeSignal);
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(callerReview(review, state, { inputId: pending.id, signal }), signal);
362
- await reviewSpan.end({}, activeSignal);
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 reviewSpan.end({ status: "error" }, activeSignal).catch(() => { });
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, state.request.kind === "agent", async (activeSignal) => {
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
- if (state.request.kind === "agent")
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, state.request.kind === "agent", async (activeSignal) => {
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
- if (state.request.kind === "agent")
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.drive(requestId, handlers),
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", "prepare_step", "caller_review", "user_input"].includes(String(event.kind))) {
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.model_events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
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.1.3",
4
- "description": "TypeScript client for durable model requests and caller-controlled agent execution",
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": {