terse-sdk 0.9.20 → 0.9.22

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/index.js CHANGED
@@ -1,647 +1,878 @@
1
1
  /// <reference types="node" preserve="true" />
2
- import { defineWorkflow, sleep as durableSleep, getExecutionPhase } from "little-durable";
3
- import ms from "ms";
4
- import { ApiRoutes, IntegrationType, sdkAgentRunRequestBodySchema, sdkStateGetResponseSchema, serializedEventSchema, stripZodJsonSchemaMetadata, webhookJobChallengeRequestSchema, webhookJobTriggerRequestSchema } from "terse-types";
5
- import { z } from "zod";
6
- import { buildSdkRequestHeaders, resolveApiBaseUrl, resolveApiKey, resolveTerseBackendUrl } from "./backendRequest.js";
7
- import { claimAgentApprovalHandling, releaseAgentApprovalHandling } from "./context.js";
8
- import { __runDurableStep, runSdkStep } from "./durableExecution.js";
9
- import { DurableOnlyError, isDurableExecution, isLocalTestRun } from "./execution.js";
10
- import { computeChallengeSignature, verifyIncomingRequest } from "./hmac.js";
11
- import { resolveRunIdentity } from "./runIdentity/index.js";
12
- import { openSessionStream } from "./sessionStream.js";
13
- import { createSDKTrigger } from "./types.js";
14
- export const TERSE_JOB_WEBHOOK_TRIGGER_PATH = ApiRoutes.SDK.JOB_WEBHOOK_TRIGGER;
15
2
  export { Actor, ActorInvocationError, Persisted, Ephemeral, Emittable, Interleave, Compute } from "./actor.js";
16
3
  export { ActorProxy } from "./actorProxy.js";
17
- export { DurableOnlyError } from "./execution.js";
18
- export { __runDurableStep } from "./durableExecution.js";
19
- export { __inputRequestHook, slack, waitForInput } from "./inputRequest.js";
20
- export { isAgentApprovalHandlingClaimed } from "./context.js";
21
- export { pollUntil } from "./poll.js";
22
4
  export { SessionStreamError, openListenStream, openSessionStream } from "./sessionStream.js";
23
- // Re-export SDK-specific types
24
- export { createSDKTrigger, registerEventTransform } from "./types.js";
25
- // Re-export shared types for consumer convenience
26
- export { ApolloOutputConfig, AttioEventType, AttioInputConfig, AttioOutputConfig, ConfigType, DatadogConfig, GitHubConfig, GitHubEventType, GmailConfig, GmailDraftOutputConfig, GmailEventType, GmailOutputConfig, GoogleSearchConsoleOutputConfig, HeyReachEventType, HeyReachInputConfig, HiggsfieldOutputConfig, ImageEditConfig, LaunchDarklyConfig, LinearEventType, LinearInputConfig, LinearOutputConfig, MemoryConfig, MetaAdsOutputConfig, NotionConfig, PosthogConfig, ResendOutputConfig, SlackConfig, SlackEventType, SlackOutputConfig, SnowflakeOutputConfig, TimeTriggerConfig, WebConfig, WebMonitorConfig, WebhookInputConfig, WorkOSEventType, WorkOSInputConfig, WorkOSOutputConfig, debugTrigger, formatTriggerForAgent } from "terse-types";
27
- export { IntegrationType } from "terse-types";
28
- export { TIMEZONES } from "terse-types";
29
- export { RunHistoryStatus } from "terse-types";
30
- export function createJob(params) {
31
- const currentJobs = fetchRegisteredJobs();
32
- if (currentJobs.has(params.name)) {
33
- throw new Error(`Job "${params.name}" is registered twice on this Terse instance.`);
34
- }
35
- const webhookCount = params.triggers.filter(t => t.integrationType === IntegrationType.WEBHOOK).length;
36
- if (webhookCount > 1) {
37
- throw new Error(`Job "${params.name}" has ${webhookCount} webhook triggers. Only one webhook trigger per job is allowed.`);
38
- }
39
- registerJob(params);
40
- if (params.durable) {
41
- __registerDurableWorkflow(__defineTerseWorkflow(params));
42
- }
43
- return params;
44
- }
45
- // Process-wide directory of Terse instances. Each `new Terse()` registers itself here on
46
- // construction so the CLI (via tsImport) can discover jobs across module-graph boundaries.
47
- // Keyed by a process-global Symbol so every copy of this module shares one array.
48
- // type JobsLike = { jobs: Map<string, CreateJobParameters> }
49
- const TERSE_INSTANCES_KEY = Symbol.for("jobs.instances");
50
- const TERSE_DURABLE_WORKFLOWS_KEY = Symbol.for("jobs.durable-workflows");
51
- function registerJob(job) {
52
- const g = globalThis;
53
- g[TERSE_INSTANCES_KEY] ??= new Map();
54
- if (g[TERSE_INSTANCES_KEY].has(job.name)) {
55
- throw new Error(`Job "${job.name}" is registered twice on this Terse instance.`);
56
- }
57
- g[TERSE_INSTANCES_KEY].set(job.name, job);
58
- }
59
- export function fetchRegisteredJobs() {
60
- const g = globalThis;
61
- return g[TERSE_INSTANCES_KEY] ?? new Map();
62
- }
63
- export function __defineTerseWorkflow(job) {
64
- return defineWorkflow({
65
- name: job.name,
66
- input: serializedEventSchema,
67
- run: async (input) => {
68
- const event = createSDKTrigger(input);
69
- const state = __buildJobStateAccessor(job.states ?? []);
70
- await job.onTrigger(event, state);
71
- }
72
- });
73
- }
74
- export function __registerDurableWorkflow(workflow) {
75
- const global = globalThis;
76
- global[TERSE_DURABLE_WORKFLOWS_KEY] ??= new Map();
77
- global[TERSE_DURABLE_WORKFLOWS_KEY].set(workflow.name, workflow);
78
- }
79
- export function __fetchRegisteredDurableWorkflow(name) {
80
- return globalThis[TERSE_DURABLE_WORKFLOWS_KEY]?.get(name);
81
- }
82
- // Clear the process-global instance registry. The CLI calls this before
83
- // re-importing an entry file so a second import (e.g. after a retry) starts
84
- // from a clean slate instead of seeing stale instances from the first pass.
85
- export function __resetRegisteredTerseInstances() {
86
- const g = globalThis;
87
- g[TERSE_INSTANCES_KEY] = new Map();
88
- const durableGlobal = globalThis;
89
- durableGlobal[TERSE_DURABLE_WORKFLOWS_KEY] = new Map();
90
- }
91
- async function stateGet(key) {
92
- return runSdkStep({
93
- name: "terse.state.get",
94
- input: { key },
95
- run: async (input) => {
96
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.STATE_GET}`, {
97
- method: "POST",
98
- headers: await buildSdkRequestHeaders(),
99
- body: JSON.stringify(input)
100
- });
101
- if (!res.ok) {
102
- const data = (await res.json().catch(() => ({})));
103
- throw new Error(`Failed to read state "${input.key}": ${data.error ?? res.statusText}`);
104
- }
105
- return sdkStateGetResponseSchema.parse(await res.json()).content;
106
- }
107
- });
108
- }
109
- async function statePut(key, content) {
110
- return runSdkStep({
111
- name: "terse.state.put",
112
- input: { key, content },
113
- run: async (input) => {
114
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.STATE_PUT}`, {
115
- method: "POST",
116
- headers: await buildSdkRequestHeaders(),
117
- body: JSON.stringify(input)
118
- });
119
- if (!res.ok) {
120
- const data = (await res.json().catch(() => ({})));
121
- throw new Error(`Failed to write state "${input.key}": ${data.error ?? res.statusText}`);
122
- }
123
- }
124
- });
125
- }
126
- export function __buildJobStateAccessor(states) {
127
- const schemas = new Map(states.map(s => [s.key, s.value]));
128
- const schemaFor = (key) => {
129
- const schema = schemas.get(key);
130
- if (!schema)
131
- throw new Error(`Unknown state key "${key}". Declare it in the job's \`states\`.`);
132
- return schema;
133
- };
134
- const accessor = {
135
- async get(key) {
136
- const schema = schemaFor(key);
137
- const raw = await stateGet(key);
138
- if (raw !== null)
139
- return schema.parse(JSON.parse(raw));
140
- return schema.safeParse(undefined).data;
141
- },
142
- async set(key, value) {
143
- const schema = schemaFor(key);
144
- const parsed = schema.safeParse(value);
145
- if (!parsed.success) {
146
- throw new Error(`Invalid value for state "${key}": ${parsed.error.issues.map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`);
147
- }
148
- await statePut(key, JSON.stringify(parsed.data));
149
- return parsed.data;
150
- }
151
- };
152
- // TS can't correlate a dynamic string key to its schema's type (microsoft/TypeScript#30581), so we assert the per-key shape here.
153
- return accessor;
154
- }
155
- export class Terse {
156
- constructor() { }
157
- /**
158
- * Handle an incoming trigger request from the Terse backend.
159
- * Wire this into your own HTTP route (Express, Hono, Next.js, etc.).
160
- *
161
- * Two-phase protocol: (1) POST with `{ type: "challenge", challenge: "<token>" }` verifies the
162
- * HMAC signature and echoes the challenge with a signed response. (2) POST with
163
- * `{ jobName, runId, event }` opens an SDK session stream, **awaits** `onTrigger` (so this
164
- * method does not return until your job handler finishes), then closes the session stream.
165
- *
166
- * Both phases require `TERSE_SIGNING_SECRET` to verify request signatures.
167
- *
168
- * @example
169
- * ```ts
170
- * // Express — use TERSE_JOB_WEBHOOK_TRIGGER_PATH so the path matches the backend.
171
- * app.post(TERSE_JOB_WEBHOOK_TRIGGER_PATH, async (req, res) => {
172
- * const result = await terse.handleTrigger(req.body, req.headers)
173
- * res.json(result)
174
- * })
175
- * ```
176
- */
177
- async handleTrigger(body, headers) {
178
- const signingSecret = process.env.TERSE_SIGNING_SECRET;
179
- if (!signingSecret) {
180
- throw new Error("TERSE_SIGNING_SECRET is not set. " +
181
- "Add it to your .env file or export it before starting your server. " +
182
- "You can find your signing secret in the Terse dashboard under your job's settings.");
183
- }
184
- const apiKey = resolveApiKey();
185
- await verifyIncomingRequest(signingSecret, headers, JSON.stringify(body));
186
- const challenge = webhookJobChallengeRequestSchema.safeParse(body);
187
- if (challenge.success) {
188
- const signature = await computeChallengeSignature(signingSecret, challenge.data.challenge);
189
- return { challenge: challenge.data.challenge, signature };
190
- }
191
- const full = webhookJobTriggerRequestSchema.safeParse(body);
192
- if (!full.success) {
193
- const detail = full.error.issues.map((issue) => issue.message).join("; ");
194
- throw new Error(`Invalid trigger payload: ${detail}`);
195
- }
196
- const jobs = fetchRegisteredJobs();
197
- const { jobName, runId, event } = full.data;
198
- const job = jobs.get(jobName);
199
- if (!job) {
200
- const available = [...jobs.keys()];
201
- throw new Error(`Job "${jobName}" is not registered on this Terse instance. ` +
202
- (available.length
203
- ? `Registered jobs: ${available.join(", ")}. Make sure the job name in your Terse dashboard matches your code.`
204
- : `No jobs are registered. Make sure your job file is imported before the server starts.`));
205
- }
206
- const apiBaseUrl = resolveTerseBackendUrl();
207
- const session = await openSessionStream(apiBaseUrl, apiKey);
208
- try {
209
- const inputEvent = createSDKTrigger(event);
210
- const state = __buildJobStateAccessor(job.states ?? []);
211
- if (job.filter) {
212
- const shouldRun = await job.filter(inputEvent, state);
213
- if (!shouldRun) {
214
- return { status: "ok", filtered: true };
215
- }
216
- }
217
- await job.onTrigger(inputEvent, state);
218
- return { status: "ok" };
219
- }
220
- catch (error) {
221
- session.close();
222
- throw error;
223
- }
224
- finally {
225
- session.close();
226
- }
227
- }
228
- }
229
- export class TerseAgent {
230
- prompt;
231
- skills;
232
- toolApprovals;
233
- /**
234
- * Optional callback invoked when the agent requires tool approval.
235
- * Return `true` to approve, `false` to reject.
236
- * If not set, approval events are yielded as ToolApprovalRequestedResult
237
- * but no decision is submitted (the stream will hang waiting).
238
- */
239
- onApprovalRequired;
240
- constructor(params) {
241
- this.prompt = params.prompt;
242
- this.skills = params.skills;
243
- this.toolApprovals = params.toolApprovals;
244
- const createTools = globalThis.__terse_createTools;
245
- this.tools = createTools ? createTools(this) : {};
246
- }
247
- static create(params) {
248
- const skills = (params.skills ?? []);
249
- const toolApprovals = (params.toolApprovals ?? []);
250
- return new TerseAgent({ prompt: params.prompt, skills, toolApprovals });
251
- }
252
- async *run(userMessage, outputSchema) {
253
- const requestBody = this.buildRequestBody(userMessage, outputSchema);
254
- // Claim approval handling for this run if the caller wired up its own
255
- // callback, so the CLI's session-stream handler won't also prompt and
256
- // submit a decision for the same stepId.
257
- const ownsApprovals = !!this.onApprovalRequired;
258
- if (ownsApprovals)
259
- claimAgentApprovalHandling();
260
- try {
261
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.AGENT_RUN}`, {
262
- method: "POST",
263
- headers: await buildSdkRequestHeaders(),
264
- body: JSON.stringify(requestBody)
265
- });
266
- const contentType = res.headers.get("content-type") ?? "";
267
- if (contentType.includes("text/event-stream")) {
268
- yield* this.consumeSseStream(res);
269
- return;
270
- }
271
- const data = (await res.json());
272
- if (!res.ok || !data.success) {
273
- const details = data.details?.length ? ` (${data.details.join("; ")})` : "";
274
- throw new Error(`${data.error ?? "Agent run failed"}${details}`);
275
- }
276
- yield new TextResult("Agent run request accepted.");
277
- }
278
- finally {
279
- if (ownsApprovals)
280
- releaseAgentApprovalHandling();
281
- }
282
- }
283
- async submitApprovalDecision(params) {
284
- return runSdkStep({
285
- name: "terse.agent.submit-approval",
286
- input: params,
287
- run: async (input) => {
288
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.APPROVAL_DECISION}`, {
289
- method: "POST",
290
- headers: await buildSdkRequestHeaders(),
291
- body: JSON.stringify(input)
292
- });
293
- if (!res.ok) {
294
- const data = (await res.json().catch(() => ({})));
295
- throw new Error(`Approval decision failed: ${data.error ?? res.statusText}`);
296
- }
297
- }
298
- });
299
- }
300
- async runAndWait(userMessage, outputSchema) {
301
- const requestBody = this.buildRequestBody(userMessage, outputSchema);
302
- const raw = await TerseAgent.fetchFinalOutput(requestBody);
303
- if (!outputSchema)
304
- return raw;
305
- return outputSchema.parse(JSON.parse(raw));
306
- }
307
- buildRequestBody(userMessage, outputSchema) {
308
- return sdkAgentRunRequestBodySchema.parse({
309
- prompt: this.prompt,
310
- toolApprovals: this.toolApprovals,
311
- message: userMessage,
312
- skills: this.skills,
313
- ...(outputSchema ? { outputSchema: stripZodJsonSchemaMetadata(z.toJSONSchema(outputSchema)) } : {})
314
- });
315
- }
316
- static async fetchFinalOutput(requestBody) {
317
- return runSdkStep({
318
- name: "terse.agent.final-output",
319
- input: requestBody,
320
- run: async (input) => {
321
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.AGENT_RUN}`, {
322
- method: "POST",
323
- headers: await TerseAgent.buildHeaders(),
324
- body: JSON.stringify(input)
325
- });
326
- const contentType = res.headers.get("content-type") ?? "";
327
- if (!contentType.includes("text/event-stream")) {
328
- const data = await res.json();
329
- const details = data.details?.length ? ` (${data.details.join("; ")})` : "";
330
- if (!res.ok || !data.success)
331
- throw new Error(`${data.error ?? "Agent run failed"}${details}`);
332
- throw new Error("Run completed without final output");
333
- }
334
- if (!res.body)
335
- throw new Error("Stream did not provide a response body.");
336
- for await (const eventData of iterateSseDataLines(res.body)) {
337
- const parsed = safeParseStreamEvent(eventData);
338
- if (!parsed)
339
- continue;
340
- if (parsed.type === "error")
341
- throw new Error(parsed.message);
342
- if (parsed.type === "done")
343
- break;
344
- const result = mapStreamEventToResult(parsed);
345
- if (result instanceof FinalOutputResult)
346
- return result.finalOutput;
347
- }
348
- throw new Error("Run completed without final output");
349
- }
350
- });
351
- }
352
- static async executeTool(toolName, params = {}) {
353
- return runSdkStep({
354
- name: "terse.agent.execute-tool",
355
- input: { toolName, params },
356
- run: async (input) => {
357
- const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.TOOL_EXECUTE}`, {
358
- method: "POST",
359
- headers: await buildSdkRequestHeaders(),
360
- body: JSON.stringify(input)
361
- });
362
- const data = (await res.json());
363
- if (!data.success) {
364
- throw new Error(data.error ?? "Tool execution failed");
365
- }
366
- return data.result;
367
- }
368
- });
369
- }
370
- static async buildHeaders() {
371
- const headers = {
372
- Authorization: `Bearer ${resolveApiKey()}`,
373
- "Content-Type": "application/json",
374
- Accept: "text/event-stream"
375
- };
376
- const { sessionId, runId } = await resolveRunIdentity();
377
- if (sessionId)
378
- headers["X-Terse-Session-Id"] = sessionId;
379
- const runIdHeader = runId ?? process.env.TERSE_RUN_ID;
380
- if (runIdHeader)
381
- headers["X-Terse-Run-Id"] = runIdHeader;
382
- return headers;
383
- }
384
- async *consumeSseStream(res) {
385
- if (!res.body) {
386
- throw new Error("Stream did not provide a response body.");
387
- }
388
- let runId = "";
389
- const failedToolCalls = [];
390
- for await (const eventData of iterateSseDataLines(res.body)) {
391
- const parsed = safeParseStreamEvent(eventData);
392
- if (!parsed)
393
- continue;
394
- if (parsed.type === "run_started") {
395
- runId = parsed.runId;
396
- continue;
397
- }
398
- if (parsed.type === "done") {
399
- if (failedToolCalls.length > 0) {
400
- throw new Error(`Run completed with failed tool calls: ${failedToolCalls.join("; ")}`);
401
- }
402
- return;
403
- }
404
- if (parsed.type === "error") {
405
- throw new Error(parsed.message);
406
- }
407
- if (parsed.type === "tool_call_completed") {
408
- const parsedTool = parseToolCallCompleted(parsed.toolCallCompleted);
409
- if (parsedTool && parsedTool.status && parsedTool.status !== "completed") {
410
- failedToolCalls.push(`${parsedTool.tool}: ${parsedTool.status}`);
411
- }
412
- }
413
- if (parsed.type === "tool_approval_requested" && this.onApprovalRequired) {
414
- const info = parsed.toolApprovalRequested;
415
- yield mapStreamEventToResult(parsed);
416
- const approved = await this.onApprovalRequired(info);
417
- await this.submitApprovalDecision({ runId, stepId: info.stepId, approved });
418
- continue;
419
- }
420
- yield mapStreamEventToResult(parsed);
421
- }
422
- }
423
- }
424
- export async function generateText(params) {
425
- const agent = TerseAgent.create({
426
- prompt: " ",
427
- skills: params.skills ? [...params.skills] : [],
428
- toolApprovals: params.toolApprovals
429
- });
430
- if ("outputSchema" in params) {
431
- return await agent.runAndWait(params.prompt, params.outputSchema);
432
- }
433
- return await agent.runAndWait(params.prompt);
434
- }
435
- export function jobStep(opts) {
436
- // Synchronous guard so an un-awaited jobStep() still throws at the call site
437
- // instead of floating as an unhandled rejection.
438
- if (getExecutionPhase() !== "workflow") {
439
- throw new DurableOnlyError("jobStep() is only available in durable jobs. Add `durable: true` to this job.");
440
- }
441
- return runJobStep(opts);
442
- }
443
- async function runJobStep(opts) {
444
- const input = opts.inputSchema ? opts.inputSchema.parse(opts.input) : null;
445
- return __runDurableStep({
446
- name: "terse.job-step",
447
- input: input,
448
- run: async (storedInput) => {
449
- const result = await opts.run(opts.inputSchema ? storedInput : undefined);
450
- return opts.outputSchema ? opts.outputSchema.parse(result) : result;
451
- }
452
- });
453
- }
454
- /**
455
- * Runs the wrapped call as a journaled durable step: `step(client.method(args))`.
456
- * Terse rewrites the call to an inline durable closure before loading the job,
457
- * so `step()` must wrap the call directly, and its arguments and resolved value
458
- * must be serializable. Only available in durable jobs.
459
- */
460
- // Source preparation rewrites every valid step() call site away, so this body only
461
- // runs where the transform could not apply. Fail loudly either way.
462
- export function step(promise) {
463
- throw new DurableOnlyError("step() is only available in durable jobs. Add `durable: true` and wrap the call directly, e.g. step(client.method(args)).");
464
- }
465
- /**
466
- * Replay-safe logging for durable jobs: `await log("processed", count)`.
467
- * In workflow code the call is a journaled step, so the line prints exactly
468
- * once instead of once per replay like a bare `console.log` outside a step
469
- * would. Arguments cross the step boundary, so they must be serializable.
470
- * Inside step bodies and non-durable jobs it simply forwards to `console.log`.
471
- */
472
- export async function log(...args) {
473
- const print = (values) => {
474
- const prefix = process.env.NO_COLOR ? "[job]" : "[job]";
475
- console.log(prefix, ...values);
476
- };
477
- if (getExecutionPhase() !== "workflow") {
478
- print(args);
479
- return;
480
- }
481
- await __runDurableStep({
482
- name: "terse.log",
483
- input: args,
484
- run: values => print(values)
485
- });
486
- }
487
- export function sleep(duration) {
488
- if (!isDurableExecution()) {
489
- throw new DurableOnlyError("sleep() is only available in durable jobs. Add `durable: true` to this job.");
490
- }
491
- // Locally there is no suspend machinery, so skip the wait and note what production
492
- // would have done instead.
493
- if (isLocalTestRun()) {
494
- console.log(`[terse] Skipping sleep locally — in production this run would suspend and resume after ${describeDuration(duration)}.`);
495
- return Promise.resolve();
496
- }
497
- return durableSleep(toDurableDuration(duration));
498
- }
499
- function toDurableDuration(duration) {
500
- if (typeof duration === "string")
501
- return duration;
502
- const milliseconds = duration instanceof Date ? Math.max(0, duration.getTime() - Date.now()) : duration;
503
- return `${milliseconds}ms`;
504
- }
505
- function describeDuration(duration) {
506
- if (typeof duration === "string")
507
- return duration;
508
- if (duration instanceof Date)
509
- return ms(Math.max(0, duration.getTime() - Date.now()), { long: true });
510
- return ms(duration, { long: true });
511
- }
512
- export var EventType;
513
- (function (EventType) {
514
- EventType["TEXT"] = "text";
515
- EventType["FINAL_OUTPUT"] = "final_output";
516
- EventType["TOOL_CALL_PARAMS"] = "tool_call_params";
517
- EventType["TOOL_CALL_STARTED"] = "tool_call_started";
518
- EventType["TOOL_CALL_COMPLETED"] = "tool_call_completed";
519
- EventType["TOOL_APPROVAL_REQUESTED"] = "tool_approval_requested";
520
- EventType["ACTION"] = "action";
521
- })(EventType || (EventType = {}));
522
- export class TextResult {
523
- type;
524
- text;
525
- constructor(text) {
526
- this.type = EventType.TEXT;
527
- this.text = text;
528
- }
529
- }
530
- export class FinalOutputResult {
531
- type;
532
- finalOutput;
533
- constructor(finalOutput) {
534
- this.type = EventType.FINAL_OUTPUT;
535
- this.finalOutput = finalOutput;
536
- }
537
- }
538
- export class ToolCallParamsResult {
539
- type;
540
- toolCallParams;
541
- constructor(toolCallParams) {
542
- this.type = EventType.TOOL_CALL_PARAMS;
543
- this.toolCallParams = toolCallParams;
544
- }
545
- }
546
- export class ToolCallStartedResult {
547
- type;
548
- toolCallStarted;
549
- constructor(toolCallStarted) {
550
- this.type = EventType.TOOL_CALL_STARTED;
551
- this.toolCallStarted = toolCallStarted;
552
- }
553
- }
554
- export class ToolCallCompletedResult {
555
- type;
556
- toolCallCompleted;
557
- constructor(toolCallCompleted) {
558
- this.type = EventType.TOOL_CALL_COMPLETED;
559
- this.toolCallCompleted = toolCallCompleted;
560
- }
561
- }
562
- export class ToolApprovalRequestedResult {
563
- type;
564
- toolApprovalRequested;
565
- constructor(toolApprovalRequested) {
566
- this.type = EventType.TOOL_APPROVAL_REQUESTED;
567
- this.toolApprovalRequested = toolApprovalRequested;
568
- }
569
- }
570
- export class ActionResult {
571
- type;
572
- action;
573
- constructor(action) {
574
- this.type = EventType.ACTION;
575
- this.action = action;
576
- }
577
- }
578
- async function* iterateSseDataLines(stream) {
579
- const reader = stream.getReader();
580
- const decoder = new TextDecoder();
581
- let buffer = "";
582
- try {
583
- while (true) {
584
- const { done, value } = await reader.read();
585
- if (done)
586
- break;
587
- buffer += decoder.decode(value, { stream: true });
588
- let separatorIndex = buffer.indexOf("\n\n");
589
- while (separatorIndex !== -1) {
590
- const rawEvent = buffer.slice(0, separatorIndex);
591
- buffer = buffer.slice(separatorIndex + 2);
592
- const dataLines = rawEvent
593
- .split("\n")
594
- .filter(line => line.startsWith("data:"))
595
- .map(line => line.slice(5).trim())
596
- .filter(Boolean);
597
- if (dataLines.length > 0) {
598
- yield dataLines.join("\n");
599
- }
600
- separatorIndex = buffer.indexOf("\n\n");
601
- }
602
- }
603
- }
604
- finally {
605
- reader.releaseLock();
606
- }
607
- }
608
- function safeParseStreamEvent(raw) {
609
- try {
610
- return JSON.parse(raw);
611
- }
612
- catch {
613
- return null;
614
- }
615
- }
616
- function mapStreamEventToResult(event) {
617
- switch (event.type) {
618
- case "text":
619
- return new TextResult(event.text);
620
- case "final_output":
621
- return new FinalOutputResult(event.finalOutput);
622
- case "tool_call_params":
623
- return new ToolCallParamsResult(event.toolCallParams);
624
- case "tool_call_started":
625
- return new ToolCallStartedResult(event.toolCallStarted);
626
- case "tool_call_completed":
627
- return new ToolCallCompletedResult(event.toolCallCompleted);
628
- case "tool_approval_requested":
629
- return new ToolApprovalRequestedResult(event.toolApprovalRequested);
630
- case "action":
631
- return new ActionResult(event.action);
632
- case "done":
633
- case "error":
634
- return new TextResult("");
635
- default:
636
- return new TextResult("");
637
- }
638
- }
639
- function parseToolCallCompleted(raw) {
640
- try {
641
- return JSON.parse(raw);
642
- }
643
- catch {
644
- return null;
645
- }
646
- }
5
+ // LEGACY-JOBS: The jobs SDK (createJob, agents, generateText, durable steps, triggers, integrations) is commented out while Terse is actors-only.
6
+ // To restore it: replace the exports above with this block uncommented, uncomment every file marked LEGACY-JOBS, and re-add little-durable, ms, and @types/ms to package.json.
7
+ // /// <reference types="node" preserve="true" />
8
+ // import { defineWorkflow, sleep as durableSleep, getExecutionPhase } from "little-durable"
9
+ // import type { StepStartedEvent, WorkflowDefinition } from "little-durable"
10
+ // import ms from "ms"
11
+ // import type {
12
+ // RunHistoryAction,
13
+ // SdkAgentRunRequestBody,
14
+ // SdkAgentRunResponseBody,
15
+ // SdkAgentStreamEvent,
16
+ // SdkApprovalDecisionRequestBody,
17
+ // SdkStateGetRequest,
18
+ // SdkStatePutRequest,
19
+ // WebhookJobChallengeResponse,
20
+ // WebhookJobTriggerResponse
21
+ // } from "terse-types"
22
+ // import {
23
+ // ApiRoutes,
24
+ // IntegrationType,
25
+ // sdkAgentRunRequestBodySchema,
26
+ // sdkStateGetResponseSchema,
27
+ // serializedEventSchema,
28
+ // stripZodJsonSchemaMetadata,
29
+ // webhookJobChallengeRequestSchema,
30
+ // webhookJobTriggerRequestSchema
31
+ // } from "terse-types"
32
+ // // The type-erased base event type, enriched with the SDK methods (formatForAgentRunner/debugLog).
33
+ // // Concrete trigger types are generated into the user's workspace by `terse generate`.
34
+ // import type { Trigger as _RawTrigger } from "terse-types"
35
+ // import { z } from "zod"
36
+ //
37
+ // import { buildSdkRequestHeaders, resolveApiBaseUrl, resolveApiKey, resolveTerseBackendUrl } from "./backendRequest.js"
38
+ // import { claimAgentApprovalHandling, releaseAgentApprovalHandling } from "./context.js"
39
+ // import { __runDurableStep, runSdkStep } from "./durableExecution.js"
40
+ // import { DurableOnlyError, isDurableExecution, isLocalTestRun } from "./execution.js"
41
+ // import { computeChallengeSignature, verifyIncomingRequest } from "./hmac.js"
42
+ // import { resolveRunIdentity } from "./runIdentity/index.js"
43
+ // import { openSessionStream } from "./sessionStream.js"
44
+ // import {
45
+ // type InferEvents,
46
+ // type InferToolApprovals,
47
+ // type SDKTrigger,
48
+ // type StateAccessor,
49
+ // type StateDefinition,
50
+ // type TriggerLike,
51
+ // type TypedSkill,
52
+ // type TypedTrigger,
53
+ // createSDKTrigger
54
+ // } from "./types.js"
55
+ //
56
+ // declare const process: { env: Record<string, string | undefined> }
57
+ //
58
+ // export const TERSE_JOB_WEBHOOK_TRIGGER_PATH = ApiRoutes.SDK.JOB_WEBHOOK_TRIGGER
59
+ //
60
+ // export { Actor, ActorInvocationError, Persisted, Ephemeral, Emittable, Interleave, Compute } from "./actor.js"
61
+ // export type { ActorBroadcastOptions, ActorClass, ActorConnection, ActorSocket, ActorSocketMessage, ActorSocketState, ComputeOptions, ComputeRegion } from "./actor.js"
62
+ //
63
+ // export { ActorProxy } from "./actorProxy.js"
64
+ // export type { ActorProxyOptions, ProxyActor, SocketAuthorization, SocketGrant } from "./actorProxy.js"
65
+ //
66
+ // export { DurableOnlyError } from "./execution.js"
67
+ // export { __runDurableStep } from "./durableExecution.js"
68
+ //
69
+ // export { __inputRequestHook, slack, waitForInput } from "./inputRequest.js"
70
+ // export type { InputDelivery, InputMedia, InputOption, InputRespondent, InputResponse, InputTarget, SlackInputTarget, WaitForInputParams } from "./inputRequest.js"
71
+ //
72
+ // export { isAgentApprovalHandlingClaimed } from "./context.js"
73
+ // export type { TerseJobContext } from "./context.js"
74
+ //
75
+ // export { pollUntil } from "./poll.js"
76
+ // export type { PollUntilOptions } from "./poll.js"
77
+ //
78
+ // export { SessionStreamError, openListenStream, openSessionStream } from "./sessionStream.js"
79
+ // export type { ListenStreamHandle, OpenListenStreamOptions, OpenSessionStreamOptions, SessionStartedEvent, SessionStreamEvent, SessionStreamHandle } from "./sessionStream.js"
80
+ //
81
+ // // Re-export SDK-specific types
82
+ // export { createSDKTrigger, registerEventTransform } from "./types.js"
83
+ // export type {
84
+ // InferEvent,
85
+ // InferEvents,
86
+ // InferStructuredOutput,
87
+ // InferToolApproval,
88
+ // InferToolApprovals,
89
+ // SDKTrigger,
90
+ // StateAccessor,
91
+ // StateDefinition,
92
+ // ToolboxEntry,
93
+ // TriggerLike,
94
+ // TypedSkill,
95
+ // TypedTrigger
96
+ // } from "./types.js"
97
+ //
98
+ // // Re-export shared types for consumer convenience
99
+ // export {
100
+ // ApolloOutputConfig,
101
+ // AttioEventType,
102
+ // AttioInputConfig,
103
+ // AttioOutputConfig,
104
+ // ConfigInstance,
105
+ // ConfigType,
106
+ // DatadogConfig,
107
+ // GitHubConfig,
108
+ // GitHubEventType,
109
+ // GmailConfig,
110
+ // GmailDraftOutputConfig,
111
+ // GmailEventType,
112
+ // GmailOutputConfig,
113
+ // GoogleSearchConsoleOutputConfig,
114
+ // HeyReachEventType,
115
+ // HeyReachInputConfig,
116
+ // HiggsfieldOutputConfig,
117
+ // ImageEditConfig,
118
+ // LaunchDarklyConfig,
119
+ // LinearEventType,
120
+ // LinearInputConfig,
121
+ // LinearOutputConfig,
122
+ // MemoryConfig,
123
+ // MetaAdsOutputConfig,
124
+ // NotionConfig,
125
+ // PosthogConfig,
126
+ // ResendOutputConfig,
127
+ // SlackConfig,
128
+ // SlackEventType,
129
+ // SlackOutputConfig,
130
+ // SnowflakeOutputConfig,
131
+ // TimeTriggerConfig,
132
+ // WebConfig,
133
+ // WebMonitorConfig,
134
+ // WebhookInputConfig,
135
+ // WorkOSEventType,
136
+ // WorkOSInputConfig,
137
+ // WorkOSOutputConfig,
138
+ // debugTrigger,
139
+ // formatTriggerForAgent
140
+ // } from "terse-types"
141
+ //
142
+ // export type Trigger = SDKTrigger<_RawTrigger>
143
+ //
144
+ // export { FrequencyUnit, IntegrationType } from "terse-types"
145
+ // export { TIMEZONES } from "terse-types"
146
+ // export type { Timezone } from "terse-types"
147
+ // export type { SdkAgentRunOptionsPayload, SdkAgentRunRequestBody, SdkAgentRunResponseBody, SdkAgentStreamEvent, ToolInputByName, ToolOutputByName } from "terse-types"
148
+ // export type { SlackAttachments, SlackBlocks, SlackFiles } from "terse-types"
149
+ //
150
+ // export { RunHistoryAction, RunHistoryDecision, RunHistoryRecord, RunHistoryStatus, RunHistoryTrigger } from "terse-types"
151
+ //
152
+ // // SDK types
153
+ //
154
+ // type Action = RunHistoryAction
155
+ //
156
+ // export type CreateJobParameters<TTriggers extends readonly TypedTrigger<TriggerLike>[] = TypedTrigger<TriggerLike>[], TStates extends readonly StateDefinition[] = readonly StateDefinition[]> = {
157
+ // name: string
158
+ // triggers: [...TTriggers]
159
+ // states?: [...TStates]
160
+ // filter?: (event: InferEvents<TTriggers>, state: StateAccessor<TStates>) => boolean | Promise<boolean>
161
+ // onTrigger: (event: InferEvents<TTriggers>, state: StateAccessor<TStates>) => Promise<void>
162
+ // remoteServerUrl?: string
163
+ // durable?: boolean
164
+ // }
165
+ //
166
+ // export function createJob<TTriggers extends readonly TypedTrigger<TriggerLike>[], const TStates extends readonly StateDefinition[] = readonly []>(params: CreateJobParameters<TTriggers, TStates>) {
167
+ // const currentJobs = fetchRegisteredJobs()
168
+ // if (currentJobs.has(params.name)) {
169
+ // throw new Error(`Job "${params.name}" is registered twice on this Terse instance.`)
170
+ // }
171
+ // const webhookCount = params.triggers.filter(t => t.integrationType === IntegrationType.WEBHOOK).length
172
+ // if (webhookCount > 1) {
173
+ // throw new Error(`Job "${params.name}" has ${webhookCount} webhook triggers. Only one webhook trigger per job is allowed.`)
174
+ // }
175
+ // registerJob<TTriggers>(params)
176
+ // if (params.durable) {
177
+ // __registerDurableWorkflow(__defineTerseWorkflow(params as unknown as CreateJobParameters))
178
+ // }
179
+ // return params
180
+ // }
181
+ //
182
+ // // Process-wide directory of Terse instances. Each `new Terse()` registers itself here on
183
+ // // construction so the CLI (via tsImport) can discover jobs across module-graph boundaries.
184
+ // // Keyed by a process-global Symbol so every copy of this module shares one array.
185
+ // // type JobsLike = { jobs: Map<string, CreateJobParameters> }
186
+ // const TERSE_INSTANCES_KEY = Symbol.for("jobs.instances")
187
+ // const TERSE_DURABLE_WORKFLOWS_KEY = Symbol.for("jobs.durable-workflows")
188
+ // type GlobalWithInstances = typeof globalThis & { [TERSE_INSTANCES_KEY]?: Map<string, CreateJobParameters> }
189
+ // type TerseWorkflowDefinition = WorkflowDefinition<typeof serializedEventSchema>
190
+ // type GlobalWithDurableWorkflows = typeof globalThis & { [TERSE_DURABLE_WORKFLOWS_KEY]?: Map<string, TerseWorkflowDefinition> }
191
+ //
192
+ // function registerJob<TTriggers extends readonly TypedTrigger<TriggerLike>[]>(job: CreateJobParameters<TTriggers>): void {
193
+ // const g = globalThis as GlobalWithInstances
194
+ // g[TERSE_INSTANCES_KEY] ??= new Map<string, CreateJobParameters>()
195
+ //
196
+ // if (g[TERSE_INSTANCES_KEY].has(job.name)) {
197
+ // throw new Error(`Job "${job.name}" is registered twice on this Terse instance.`)
198
+ // }
199
+ //
200
+ // g[TERSE_INSTANCES_KEY].set(job.name, job as unknown as CreateJobParameters)
201
+ // }
202
+ //
203
+ // export function fetchRegisteredJobs(): Map<string, CreateJobParameters> {
204
+ // const g = globalThis as GlobalWithInstances
205
+ // return g[TERSE_INSTANCES_KEY] ?? new Map<string, CreateJobParameters>()
206
+ // }
207
+ //
208
+ // export function __defineTerseWorkflow(job: CreateJobParameters): TerseWorkflowDefinition {
209
+ // return defineWorkflow({
210
+ // name: job.name,
211
+ // input: serializedEventSchema,
212
+ // run: async input => {
213
+ // const event = createSDKTrigger(input)
214
+ // const state = __buildJobStateAccessor(job.states ?? [])
215
+ // await job.onTrigger(event, state)
216
+ // }
217
+ // })
218
+ // }
219
+ //
220
+ // export function __registerDurableWorkflow(workflow: TerseWorkflowDefinition): void {
221
+ // const global = globalThis as GlobalWithDurableWorkflows
222
+ // global[TERSE_DURABLE_WORKFLOWS_KEY] ??= new Map<string, TerseWorkflowDefinition>()
223
+ // global[TERSE_DURABLE_WORKFLOWS_KEY].set(workflow.name, workflow)
224
+ // }
225
+ //
226
+ // export function __fetchRegisteredDurableWorkflow(name: string): TerseWorkflowDefinition | undefined {
227
+ // return (globalThis as GlobalWithDurableWorkflows)[TERSE_DURABLE_WORKFLOWS_KEY]?.get(name)
228
+ // }
229
+ //
230
+ // // Clear the process-global instance registry. The CLI calls this before
231
+ // // re-importing an entry file so a second import (e.g. after a retry) starts
232
+ // // from a clean slate instead of seeing stale instances from the first pass.
233
+ // export function __resetRegisteredTerseInstances(): void {
234
+ // const g = globalThis as GlobalWithInstances
235
+ // g[TERSE_INSTANCES_KEY] = new Map<string, CreateJobParameters>()
236
+ // const durableGlobal = globalThis as GlobalWithDurableWorkflows
237
+ // durableGlobal[TERSE_DURABLE_WORKFLOWS_KEY] = new Map<string, TerseWorkflowDefinition>()
238
+ // }
239
+ //
240
+ // async function stateGet(key: string): Promise<string | null> {
241
+ // return runSdkStep({
242
+ // name: "terse.state.get",
243
+ // input: { key },
244
+ // run: async input => {
245
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.STATE_GET}`, {
246
+ // method: "POST",
247
+ // headers: await buildSdkRequestHeaders(),
248
+ // body: JSON.stringify(input satisfies SdkStateGetRequest)
249
+ // })
250
+ // if (!res.ok) {
251
+ // const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
252
+ // throw new Error(`Failed to read state "${input.key}": ${data.error ?? res.statusText}`)
253
+ // }
254
+ // return sdkStateGetResponseSchema.parse(await res.json()).content
255
+ // }
256
+ // })
257
+ // }
258
+ //
259
+ // async function statePut(key: string, content: string): Promise<void> {
260
+ // return runSdkStep({
261
+ // name: "terse.state.put",
262
+ // input: { key, content },
263
+ // run: async input => {
264
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.STATE_PUT}`, {
265
+ // method: "POST",
266
+ // headers: await buildSdkRequestHeaders(),
267
+ // body: JSON.stringify(input satisfies SdkStatePutRequest)
268
+ // })
269
+ // if (!res.ok) {
270
+ // const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
271
+ // throw new Error(`Failed to write state "${input.key}": ${data.error ?? res.statusText}`)
272
+ // }
273
+ // }
274
+ // })
275
+ // }
276
+ //
277
+ // export function __buildJobStateAccessor<TStates extends readonly StateDefinition[]>(states: TStates): StateAccessor<TStates> {
278
+ // const schemas = new Map(states.map(s => [s.key, s.value] as const))
279
+ // const schemaFor = (key: string): z.ZodType => {
280
+ // const schema = schemas.get(key)
281
+ // if (!schema) throw new Error(`Unknown state key "${key}". Declare it in the job's \`states\`.`)
282
+ // return schema
283
+ // }
284
+ // const accessor: StateAccessorImpl = {
285
+ // async get(key) {
286
+ // const schema = schemaFor(key)
287
+ // const raw = await stateGet(key)
288
+ // if (raw !== null) return schema.parse(JSON.parse(raw))
289
+ // return schema.safeParse(undefined).data
290
+ // },
291
+ // async set(key, value) {
292
+ // const schema = schemaFor(key)
293
+ // const parsed = schema.safeParse(value)
294
+ // if (!parsed.success) {
295
+ // throw new Error(`Invalid value for state "${key}": ${parsed.error.issues.map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`)
296
+ // }
297
+ // await statePut(key, JSON.stringify(parsed.data))
298
+ // return parsed.data
299
+ // }
300
+ // }
301
+ // // TS can't correlate a dynamic string key to its schema's type (microsoft/TypeScript#30581), so we assert the per-key shape here.
302
+ // return accessor as StateAccessor<TStates>
303
+ // }
304
+ //
305
+ // export class Terse {
306
+ // constructor() {}
307
+ // /**
308
+ // * Handle an incoming trigger request from the Terse backend.
309
+ // * Wire this into your own HTTP route (Express, Hono, Next.js, etc.).
310
+ // *
311
+ // * Two-phase protocol: (1) POST with `{ type: "challenge", challenge: "<token>" }` verifies the
312
+ // * HMAC signature and echoes the challenge with a signed response. (2) POST with
313
+ // * `{ jobName, runId, event }` opens an SDK session stream, **awaits** `onTrigger` (so this
314
+ // * method does not return until your job handler finishes), then closes the session stream.
315
+ // *
316
+ // * Both phases require `TERSE_SIGNING_SECRET` to verify request signatures.
317
+ // *
318
+ // * @example
319
+ // * ```ts
320
+ // * // Express — use TERSE_JOB_WEBHOOK_TRIGGER_PATH so the path matches the backend.
321
+ // * app.post(TERSE_JOB_WEBHOOK_TRIGGER_PATH, async (req, res) => {
322
+ // * const result = await terse.handleTrigger(req.body, req.headers)
323
+ // * res.json(result)
324
+ // * })
325
+ // * ```
326
+ // */
327
+ // async handleTrigger(body: unknown, headers: Record<string, string | string[] | undefined>): Promise<WebhookJobChallengeResponse | WebhookJobTriggerResponse> {
328
+ // const signingSecret = process.env.TERSE_SIGNING_SECRET
329
+ // if (!signingSecret) {
330
+ // throw new Error(
331
+ // "TERSE_SIGNING_SECRET is not set. " +
332
+ // "Add it to your .env file or export it before starting your server. " +
333
+ // "You can find your signing secret in the Terse dashboard under your job's settings."
334
+ // )
335
+ // }
336
+ //
337
+ // const apiKey = resolveApiKey()
338
+ //
339
+ // await verifyIncomingRequest(signingSecret, headers, JSON.stringify(body))
340
+ //
341
+ // const challenge = webhookJobChallengeRequestSchema.safeParse(body)
342
+ // if (challenge.success) {
343
+ // const signature = await computeChallengeSignature(signingSecret, challenge.data.challenge)
344
+ // return { challenge: challenge.data.challenge, signature }
345
+ // }
346
+ //
347
+ // const full = webhookJobTriggerRequestSchema.safeParse(body)
348
+ // if (!full.success) {
349
+ // const detail = full.error.issues.map((issue: { message: string }) => issue.message).join("; ")
350
+ // throw new Error(`Invalid trigger payload: ${detail}`)
351
+ // }
352
+ //
353
+ // const jobs = fetchRegisteredJobs()
354
+ //
355
+ // const { jobName, runId, event } = full.data
356
+ // const job = jobs.get(jobName)
357
+ // if (!job) {
358
+ // const available = [...jobs.keys()]
359
+ // throw new Error(
360
+ // `Job "${jobName}" is not registered on this Terse instance. ` +
361
+ // (available.length
362
+ // ? `Registered jobs: ${available.join(", ")}. Make sure the job name in your Terse dashboard matches your code.`
363
+ // : `No jobs are registered. Make sure your job file is imported before the server starts.`)
364
+ // )
365
+ // }
366
+ //
367
+ // const apiBaseUrl = resolveTerseBackendUrl()
368
+ // const session = await openSessionStream(apiBaseUrl, apiKey)
369
+ //
370
+ // try {
371
+ // const inputEvent = createSDKTrigger(event)
372
+ // const state = __buildJobStateAccessor(job.states ?? [])
373
+ //
374
+ // if (job.filter) {
375
+ // const shouldRun = await job.filter(inputEvent, state)
376
+ // if (!shouldRun) {
377
+ // return { status: "ok" as const, filtered: true }
378
+ // }
379
+ // }
380
+ // await job.onTrigger(inputEvent, state)
381
+ //
382
+ // return { status: "ok" as const }
383
+ // } catch (error) {
384
+ // session.close()
385
+ // throw error
386
+ // } finally {
387
+ // session.close()
388
+ // }
389
+ // }
390
+ // }
391
+ //
392
+ // export type ApprovalRequestInfo = {
393
+ // stepId: string
394
+ // toolName: string
395
+ // arguments: string
396
+ // }
397
+ //
398
+ // export class TerseAgent<TSkills extends readonly TypedSkill<string>[] = readonly TypedSkill<string>[]> {
399
+ // readonly prompt: string
400
+ // readonly skills: TSkills
401
+ // readonly toolApprovals: InferToolApprovals<TSkills>[]
402
+ //
403
+ // /**
404
+ // * Optional callback invoked when the agent requires tool approval.
405
+ // * Return `true` to approve, `false` to reject.
406
+ // * If not set, approval events are yielded as ToolApprovalRequestedResult
407
+ // * but no decision is submitted (the stream will hang waiting).
408
+ // */
409
+ // onApprovalRequired?: (info: ApprovalRequestInfo) => Promise<boolean>
410
+ //
411
+ // private constructor(params: { prompt: string; skills: TSkills; toolApprovals: InferToolApprovals<TSkills>[] }) {
412
+ // this.prompt = params.prompt
413
+ // this.skills = params.skills
414
+ // this.toolApprovals = params.toolApprovals
415
+ //
416
+ // const createTools = (globalThis as any).__terse_createTools as ((agent: TerseAgent) => unknown) | undefined
417
+ // ;(this as any).tools = createTools ? createTools(this) : {}
418
+ // }
419
+ //
420
+ // static create<TSkills extends readonly TypedSkill<string>[]>(params: { prompt: string; skills: [...TSkills]; toolApprovals?: InferToolApprovals<TSkills>[] }): TerseAgent<TSkills>
421
+ // static create(params: { prompt: string }): TerseAgent<readonly []>
422
+ // static create<TSkills extends readonly TypedSkill<string>[] = readonly []>(params: { prompt: string; skills?: [...TSkills]; toolApprovals?: InferToolApprovals<TSkills>[] }): TerseAgent<TSkills> {
423
+ // const skills = (params.skills ?? []) as TSkills
424
+ // const toolApprovals = (params.toolApprovals ?? []) as InferToolApprovals<TSkills>[]
425
+ // return new TerseAgent<TSkills>({ prompt: params.prompt, skills, toolApprovals })
426
+ // }
427
+ //
428
+ // async *run(userMessage: string, outputSchema?: z.ZodType): AsyncGenerator<TerseAgentResult> {
429
+ // const requestBody = this.buildRequestBody(userMessage, outputSchema)
430
+ //
431
+ // // Claim approval handling for this run if the caller wired up its own
432
+ // // callback, so the CLI's session-stream handler won't also prompt and
433
+ // // submit a decision for the same stepId.
434
+ // const ownsApprovals = !!this.onApprovalRequired
435
+ // if (ownsApprovals) claimAgentApprovalHandling()
436
+ //
437
+ // try {
438
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.AGENT_RUN}`, {
439
+ // method: "POST",
440
+ // headers: await buildSdkRequestHeaders(),
441
+ // body: JSON.stringify(requestBody)
442
+ // })
443
+ //
444
+ // const contentType = res.headers.get("content-type") ?? ""
445
+ // if (contentType.includes("text/event-stream")) {
446
+ // yield* this.consumeSseStream(res)
447
+ // return
448
+ // }
449
+ //
450
+ // const data = (await res.json()) as SdkAgentRunResponseBody
451
+ // if (!res.ok || !data.success) {
452
+ // const details = data.details?.length ? ` (${data.details.join("; ")})` : ""
453
+ // throw new Error(`${data.error ?? "Agent run failed"}${details}`)
454
+ // }
455
+ //
456
+ // yield new TextResult("Agent run request accepted.")
457
+ // } finally {
458
+ // if (ownsApprovals) releaseAgentApprovalHandling()
459
+ // }
460
+ // }
461
+ //
462
+ // async submitApprovalDecision(params: { runId: string; stepId: string; approved: boolean }): Promise<void> {
463
+ // return runSdkStep({
464
+ // name: "terse.agent.submit-approval",
465
+ // input: params,
466
+ // run: async input => {
467
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.APPROVAL_DECISION}`, {
468
+ // method: "POST",
469
+ // headers: await buildSdkRequestHeaders(),
470
+ // body: JSON.stringify(input satisfies SdkApprovalDecisionRequestBody)
471
+ // })
472
+ //
473
+ // if (!res.ok) {
474
+ // const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
475
+ // throw new Error(`Approval decision failed: ${data.error ?? res.statusText}`)
476
+ // }
477
+ // }
478
+ // })
479
+ // }
480
+ //
481
+ // async runAndWait<OutputSchema extends z.ZodType>(userMessage: string, outputSchema: OutputSchema): Promise<z.infer<OutputSchema>>
482
+ // async runAndWait(userMessage: string): Promise<string>
483
+ // async runAndWait<OutputSchema extends z.ZodType>(userMessage: string, outputSchema?: OutputSchema): Promise<string | z.infer<OutputSchema>> {
484
+ // const requestBody = this.buildRequestBody(userMessage, outputSchema)
485
+ // const raw = await TerseAgent.fetchFinalOutput(requestBody)
486
+ // if (!outputSchema) return raw
487
+ // return outputSchema.parse(JSON.parse(raw))
488
+ // }
489
+ //
490
+ // private buildRequestBody(userMessage: string, outputSchema?: z.ZodType): SdkAgentRunRequestBody {
491
+ // return sdkAgentRunRequestBodySchema.parse({
492
+ // prompt: this.prompt,
493
+ // toolApprovals: this.toolApprovals,
494
+ // message: userMessage,
495
+ // skills: this.skills,
496
+ // ...(outputSchema ? { outputSchema: stripZodJsonSchemaMetadata(z.toJSONSchema(outputSchema)) } : {})
497
+ // })
498
+ // }
499
+ //
500
+ // private static async fetchFinalOutput(requestBody: SdkAgentRunRequestBody): Promise<string> {
501
+ // return runSdkStep({
502
+ // name: "terse.agent.final-output",
503
+ // input: requestBody as StepStartedEvent["input"],
504
+ // run: async input => {
505
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.AGENT_RUN}`, {
506
+ // method: "POST",
507
+ // headers: await TerseAgent.buildHeaders(),
508
+ // body: JSON.stringify(input)
509
+ // })
510
+ // const contentType = res.headers.get("content-type") ?? ""
511
+ // if (!contentType.includes("text/event-stream")) {
512
+ // const data: SdkAgentRunResponseBody = await res.json()
513
+ // const details = data.details?.length ? ` (${data.details.join("; ")})` : ""
514
+ // if (!res.ok || !data.success) throw new Error(`${data.error ?? "Agent run failed"}${details}`)
515
+ // throw new Error("Run completed without final output")
516
+ // }
517
+ // if (!res.body) throw new Error("Stream did not provide a response body.")
518
+ // for await (const eventData of iterateSseDataLines(res.body)) {
519
+ // const parsed = safeParseStreamEvent(eventData)
520
+ // if (!parsed) continue
521
+ // if (parsed.type === "error") throw new Error(parsed.message)
522
+ // if (parsed.type === "done") break
523
+ // const result = mapStreamEventToResult(parsed)
524
+ // if (result instanceof FinalOutputResult) return result.finalOutput
525
+ // }
526
+ // throw new Error("Run completed without final output")
527
+ // }
528
+ // })
529
+ // }
530
+ //
531
+ // static async executeTool<TOutput = unknown>(toolName: string, params: Record<string, unknown> = {}): Promise<TOutput> {
532
+ // return runSdkStep({
533
+ // name: "terse.agent.execute-tool",
534
+ // input: { toolName, params } as StepStartedEvent["input"],
535
+ // run: async input => {
536
+ // const res = await fetch(`${resolveApiBaseUrl()}${ApiRoutes.SDK.TOOL_EXECUTE}`, {
537
+ // method: "POST",
538
+ // headers: await buildSdkRequestHeaders(),
539
+ // body: JSON.stringify(input)
540
+ // })
541
+ // const data = (await res.json()) as { success: boolean; result?: unknown; error?: string }
542
+ // if (!data.success) {
543
+ // throw new Error(data.error ?? "Tool execution failed")
544
+ // }
545
+ // return data.result as TOutput
546
+ // }
547
+ // })
548
+ // }
549
+ //
550
+ // private static async buildHeaders(): Promise<Record<string, string>> {
551
+ // const headers: Record<string, string> = {
552
+ // Authorization: `Bearer ${resolveApiKey()}`,
553
+ // "Content-Type": "application/json",
554
+ // Accept: "text/event-stream"
555
+ // }
556
+ // const { sessionId, runId } = await resolveRunIdentity()
557
+ // if (sessionId) headers["X-Terse-Session-Id"] = sessionId
558
+ // const runIdHeader = runId ?? process.env.TERSE_RUN_ID
559
+ // if (runIdHeader) headers["X-Terse-Run-Id"] = runIdHeader
560
+ // return headers
561
+ // }
562
+ //
563
+ // private async *consumeSseStream(res: Response): AsyncGenerator<TerseAgentResult> {
564
+ // if (!res.body) {
565
+ // throw new Error("Stream did not provide a response body.")
566
+ // }
567
+ // let runId = ""
568
+ // const failedToolCalls: string[] = []
569
+ // for await (const eventData of iterateSseDataLines(res.body)) {
570
+ // const parsed = safeParseStreamEvent(eventData)
571
+ // if (!parsed) continue
572
+ // if (parsed.type === "run_started") {
573
+ // runId = parsed.runId
574
+ // continue
575
+ // }
576
+ // if (parsed.type === "done") {
577
+ // if (failedToolCalls.length > 0) {
578
+ // throw new Error(`Run completed with failed tool calls: ${failedToolCalls.join("; ")}`)
579
+ // }
580
+ // return
581
+ // }
582
+ // if (parsed.type === "error") {
583
+ // throw new Error(parsed.message)
584
+ // }
585
+ // if (parsed.type === "tool_call_completed") {
586
+ // const parsedTool = parseToolCallCompleted(parsed.toolCallCompleted)
587
+ // if (parsedTool && parsedTool.status && parsedTool.status !== "completed") {
588
+ // failedToolCalls.push(`${parsedTool.tool}: ${parsedTool.status}`)
589
+ // }
590
+ // }
591
+ // if (parsed.type === "tool_approval_requested" && this.onApprovalRequired) {
592
+ // const info = parsed.toolApprovalRequested
593
+ // yield mapStreamEventToResult(parsed)
594
+ // const approved = await this.onApprovalRequired(info)
595
+ // await this.submitApprovalDecision({ runId, stepId: info.stepId, approved })
596
+ // continue
597
+ // }
598
+ // yield mapStreamEventToResult(parsed)
599
+ // }
600
+ // }
601
+ // }
602
+ //
603
+ // type GenerateTextParams<TSkills extends readonly TypedSkill<string>[] = readonly TypedSkill<string>[]> = {
604
+ // prompt: string
605
+ // skills?: TSkills
606
+ // toolApprovals?: InferToolApprovals<TSkills>[]
607
+ // }
608
+ //
609
+ // type GenerateTextStructuredOutput<OutputSchema extends z.ZodType> = GenerateTextParams & {
610
+ // outputSchema: OutputSchema
611
+ // }
612
+ //
613
+ // export async function generateText<OutputSchema extends z.ZodType>(params: GenerateTextStructuredOutput<OutputSchema>): Promise<z.infer<OutputSchema>>
614
+ // export async function generateText(params: GenerateTextParams): Promise<string>
615
+ // export async function generateText<OutputSchema extends z.ZodType>(params: GenerateTextParams | GenerateTextStructuredOutput<OutputSchema>): Promise<string | z.infer<OutputSchema>> {
616
+ // const agent = TerseAgent.create({
617
+ // prompt: " ",
618
+ // skills: params.skills ? [...params.skills] : [],
619
+ // toolApprovals: params.toolApprovals
620
+ // })
621
+ // if ("outputSchema" in params) {
622
+ // return await agent.runAndWait(params.prompt, params.outputSchema)
623
+ // }
624
+ // return await agent.runAndWait(params.prompt)
625
+ // }
626
+ //
627
+ // export function jobStep<I extends z.ZodType, O>(opts: { input: z.infer<I>; inputSchema: I; outputSchema?: z.ZodType<O>; run: (input: z.infer<I>) => Promise<O> }): Promise<O>
628
+ // export function jobStep<O>(opts: { outputSchema?: z.ZodType<O>; run: () => Promise<O> }): Promise<O>
629
+ // export function jobStep(opts: { input?: unknown; inputSchema?: z.ZodType; outputSchema?: z.ZodType; run: (input?: unknown) => Promise<unknown> }): Promise<unknown> {
630
+ // // Synchronous guard so an un-awaited jobStep() still throws at the call site
631
+ // // instead of floating as an unhandled rejection.
632
+ // if (getExecutionPhase() !== "workflow") {
633
+ // throw new DurableOnlyError("jobStep() is only available in durable jobs. Add `durable: true` to this job.")
634
+ // }
635
+ // return runJobStep(opts)
636
+ // }
637
+ //
638
+ // async function runJobStep(opts: { input?: unknown; inputSchema?: z.ZodType; outputSchema?: z.ZodType; run: (input?: unknown) => Promise<unknown> }): Promise<unknown> {
639
+ // const input = opts.inputSchema ? opts.inputSchema.parse(opts.input) : null
640
+ // return __runDurableStep({
641
+ // name: "terse.job-step",
642
+ // input: input as StepStartedEvent["input"],
643
+ // run: async storedInput => {
644
+ // const result = await opts.run(opts.inputSchema ? storedInput : undefined)
645
+ // return opts.outputSchema ? opts.outputSchema.parse(result) : result
646
+ // }
647
+ // })
648
+ // }
649
+ //
650
+ // /**
651
+ // * Runs the wrapped call as a journaled durable step: `step(client.method(args))`.
652
+ // * Terse rewrites the call to an inline durable closure before loading the job,
653
+ // * so `step()` must wrap the call directly, and its arguments and resolved value
654
+ // * must be serializable. Only available in durable jobs.
655
+ // */
656
+ // // Source preparation rewrites every valid step() call site away, so this body only
657
+ // // runs where the transform could not apply. Fail loudly either way.
658
+ // export function step<T>(promise: Promise<T>): Promise<T> {
659
+ // throw new DurableOnlyError("step() is only available in durable jobs. Add `durable: true` and wrap the call directly, e.g. step(client.method(args)).")
660
+ // }
661
+ //
662
+ // /**
663
+ // * Replay-safe logging for durable jobs: `await log("processed", count)`.
664
+ // * In workflow code the call is a journaled step, so the line prints exactly
665
+ // * once instead of once per replay like a bare `console.log` outside a step
666
+ // * would. Arguments cross the step boundary, so they must be serializable.
667
+ // * Inside step bodies and non-durable jobs it simply forwards to `console.log`.
668
+ // */
669
+ // export async function log(...args: unknown[]): Promise<void> {
670
+ // const print = (values: readonly unknown[]) => {
671
+ // const prefix = process.env.NO_COLOR ? "[job]" : "[job]"
672
+ // console.log(prefix, ...values)
673
+ // }
674
+ //
675
+ // if (getExecutionPhase() !== "workflow") {
676
+ // print(args)
677
+ // return
678
+ // }
679
+ //
680
+ // await __runDurableStep({
681
+ // name: "terse.log",
682
+ // input: args as StepStartedEvent["input"],
683
+ // run: values => print(values as readonly unknown[])
684
+ // })
685
+ // }
686
+ //
687
+ // export function sleep(duration: string | number | Date): Promise<void> {
688
+ // if (!isDurableExecution()) {
689
+ // throw new DurableOnlyError("sleep() is only available in durable jobs. Add `durable: true` to this job.")
690
+ // }
691
+ // // Locally there is no suspend machinery, so skip the wait and note what production
692
+ // // would have done instead.
693
+ // if (isLocalTestRun()) {
694
+ // console.log(`[terse] Skipping sleep locally — in production this run would suspend and resume after ${describeDuration(duration)}.`)
695
+ // return Promise.resolve()
696
+ // }
697
+ // return durableSleep(toDurableDuration(duration))
698
+ // }
699
+ //
700
+ // function toDurableDuration(duration: string | number | Date): Parameters<typeof durableSleep>[0] {
701
+ // if (typeof duration === "string") return duration as Parameters<typeof durableSleep>[0]
702
+ // const milliseconds = duration instanceof Date ? Math.max(0, duration.getTime() - Date.now()) : duration
703
+ // return `${milliseconds}ms` as Parameters<typeof durableSleep>[0]
704
+ // }
705
+ //
706
+ // function describeDuration(duration: string | number | Date): string {
707
+ // if (typeof duration === "string") return duration
708
+ // if (duration instanceof Date) return ms(Math.max(0, duration.getTime() - Date.now()), { long: true })
709
+ // return ms(duration, { long: true })
710
+ // }
711
+ //
712
+ // export enum EventType {
713
+ // TEXT = "text",
714
+ // FINAL_OUTPUT = "final_output",
715
+ // TOOL_CALL_PARAMS = "tool_call_params",
716
+ // TOOL_CALL_STARTED = "tool_call_started",
717
+ // TOOL_CALL_COMPLETED = "tool_call_completed",
718
+ // TOOL_APPROVAL_REQUESTED = "tool_approval_requested",
719
+ // ACTION = "action"
720
+ // }
721
+ //
722
+ // export interface TerseAgentResult {
723
+ // type: EventType
724
+ // }
725
+ //
726
+ // export class TextResult implements TerseAgentResult {
727
+ // type: EventType.TEXT
728
+ // text: string
729
+ //
730
+ // constructor(text: string) {
731
+ // this.type = EventType.TEXT
732
+ // this.text = text
733
+ // }
734
+ // }
735
+ //
736
+ // export class FinalOutputResult implements TerseAgentResult {
737
+ // type: EventType.FINAL_OUTPUT
738
+ // finalOutput: string
739
+ //
740
+ // constructor(finalOutput: string) {
741
+ // this.type = EventType.FINAL_OUTPUT
742
+ // this.finalOutput = finalOutput
743
+ // }
744
+ // }
745
+ //
746
+ // export class ToolCallParamsResult implements TerseAgentResult {
747
+ // type: EventType.TOOL_CALL_PARAMS
748
+ // toolCallParams: string
749
+ //
750
+ // constructor(toolCallParams: string) {
751
+ // this.type = EventType.TOOL_CALL_PARAMS
752
+ // this.toolCallParams = toolCallParams
753
+ // }
754
+ // }
755
+ //
756
+ // export class ToolCallStartedResult implements TerseAgentResult {
757
+ // type: EventType.TOOL_CALL_STARTED
758
+ // toolCallStarted: string
759
+ //
760
+ // constructor(toolCallStarted: string) {
761
+ // this.type = EventType.TOOL_CALL_STARTED
762
+ // this.toolCallStarted = toolCallStarted
763
+ // }
764
+ // }
765
+ //
766
+ // export class ToolCallCompletedResult implements TerseAgentResult {
767
+ // type: EventType.TOOL_CALL_COMPLETED
768
+ // toolCallCompleted: string
769
+ //
770
+ // constructor(toolCallCompleted: string) {
771
+ // this.type = EventType.TOOL_CALL_COMPLETED
772
+ // this.toolCallCompleted = toolCallCompleted
773
+ // }
774
+ // }
775
+ //
776
+ // export class ToolApprovalRequestedResult implements TerseAgentResult {
777
+ // type: EventType.TOOL_APPROVAL_REQUESTED
778
+ // toolApprovalRequested: {
779
+ // stepId: string
780
+ // toolName: string
781
+ // arguments: string
782
+ // }
783
+ //
784
+ // constructor(toolApprovalRequested: { stepId: string; toolName: string; arguments: string }) {
785
+ // this.type = EventType.TOOL_APPROVAL_REQUESTED
786
+ // this.toolApprovalRequested = toolApprovalRequested
787
+ // }
788
+ // }
789
+ //
790
+ // export class ActionResult implements TerseAgentResult {
791
+ // type: EventType.ACTION
792
+ // action: Action
793
+ //
794
+ // constructor(action: Action) {
795
+ // this.type = EventType.ACTION
796
+ // this.action = action
797
+ // }
798
+ // }
799
+ //
800
+ // async function* iterateSseDataLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
801
+ // const reader = stream.getReader()
802
+ // const decoder = new TextDecoder()
803
+ // let buffer = ""
804
+ //
805
+ // try {
806
+ // while (true) {
807
+ // const { done, value } = await reader.read()
808
+ // if (done) break
809
+ // buffer += decoder.decode(value, { stream: true })
810
+ //
811
+ // let separatorIndex = buffer.indexOf("\n\n")
812
+ // while (separatorIndex !== -1) {
813
+ // const rawEvent = buffer.slice(0, separatorIndex)
814
+ // buffer = buffer.slice(separatorIndex + 2)
815
+ //
816
+ // const dataLines = rawEvent
817
+ // .split("\n")
818
+ // .filter(line => line.startsWith("data:"))
819
+ // .map(line => line.slice(5).trim())
820
+ // .filter(Boolean)
821
+ //
822
+ // if (dataLines.length > 0) {
823
+ // yield dataLines.join("\n")
824
+ // }
825
+ //
826
+ // separatorIndex = buffer.indexOf("\n\n")
827
+ // }
828
+ // }
829
+ // } finally {
830
+ // reader.releaseLock()
831
+ // }
832
+ // }
833
+ //
834
+ // function safeParseStreamEvent(raw: string): SdkAgentStreamEvent | null {
835
+ // try {
836
+ // return JSON.parse(raw) as SdkAgentStreamEvent
837
+ // } catch {
838
+ // return null
839
+ // }
840
+ // }
841
+ //
842
+ // function mapStreamEventToResult(event: SdkAgentStreamEvent): TerseAgentResult {
843
+ // switch (event.type) {
844
+ // case "text":
845
+ // return new TextResult(event.text)
846
+ // case "final_output":
847
+ // return new FinalOutputResult(event.finalOutput)
848
+ // case "tool_call_params":
849
+ // return new ToolCallParamsResult(event.toolCallParams)
850
+ // case "tool_call_started":
851
+ // return new ToolCallStartedResult(event.toolCallStarted)
852
+ // case "tool_call_completed":
853
+ // return new ToolCallCompletedResult(event.toolCallCompleted)
854
+ // case "tool_approval_requested":
855
+ // return new ToolApprovalRequestedResult(event.toolApprovalRequested)
856
+ // case "action":
857
+ // return new ActionResult(event.action)
858
+ // case "done":
859
+ // case "error":
860
+ // return new TextResult("")
861
+ // default:
862
+ // return new TextResult("")
863
+ // }
864
+ // }
865
+ //
866
+ // function parseToolCallCompleted(raw: string): { tool?: string; status?: string } | null {
867
+ // try {
868
+ // return JSON.parse(raw) as { tool?: string; status?: string }
869
+ // } catch {
870
+ // return null
871
+ // }
872
+ // }
873
+ //
874
+ // type StateAccessorImpl = {
875
+ // get(key: string): Promise<unknown>
876
+ // set(key: string, value: unknown): Promise<unknown>
877
+ // }
647
878
  //# sourceMappingURL=index.js.map