@shardflux/sdk 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RetryRecord } from './progress.js';
2
- export declare const SDK_VERSION = "0.8.0";
2
+ export declare const SDK_VERSION = "0.10.0";
3
3
  export interface RequestOptions {
4
4
  query?: Record<string, string | number | boolean | undefined | null>;
5
5
  json?: unknown;
@@ -8,6 +8,8 @@ export interface RequestOptions {
8
8
  accept?: string;
9
9
  headers?: Record<string, string>;
10
10
  idempotencyKey?: string;
11
+ /** The request has no effect a retry could duplicate (a read-only POST such as files/search): retried like a GET. */
12
+ idempotent?: boolean;
11
13
  signal?: AbortSignal;
12
14
  /** Override the client's default request timeout (ms); 0 disables it (streams). */
13
15
  timeoutMs?: number;
@@ -22,6 +24,8 @@ export interface HttpOptions {
22
24
  maxRetries: number;
23
25
  source: 'api' | 'cell';
24
26
  sleep?: (ms: number) => Promise<void>;
27
+ /** Called after each 2xx response (never throws into the request; the version check starts from it). */
28
+ onSuccess?: (() => void) | undefined;
25
29
  }
26
30
  export declare const defaultSleep: (ms: number) => Promise<void>;
27
31
  /**
@@ -35,6 +39,8 @@ export declare const defaultSleep: (ms: number) => Promise<void>;
35
39
  */
36
40
  export declare function defaultFetch(env?: Record<string, string | undefined> | undefined, undiciVersion?: string | undefined): typeof fetch;
37
41
  export declare function buildUrl(base: string, path: string, query?: RequestOptions['query']): string;
42
+ /** `X-Tree-Revision` (file-first workspaces, contracts §29.8): a non-negative integer, else null. */
43
+ export declare function treeRevisionOf(headers: Headers): number | null;
38
44
  /** Turns a non-2xx response into a ShardfluxApiError (or a protocol error for undocumented bodies). */
39
45
  export declare function errorFrom(res: Response, source: 'api' | 'cell'): Promise<Error>;
40
46
  export declare class HttpClient {
package/dist/http.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Minimal HTTP layer shared by the application API client and the cell
3
3
  * client: JSON in/out, structured errors, per-request timeout, and bounded
4
- * retries only where a retry cannot duplicate an effect (safe methods, or
5
- * requests carrying an Idempotency-Key).
4
+ * retries only where a retry cannot duplicate an effect (safe methods,
5
+ * requests carrying an Idempotency-Key, and read-only POSTs marked
6
+ * `idempotent`). A retryable 429/502/503/504 (e.g. 503 `host_capacity`, 503
7
+ * `wake_failed`) waits `Retry-After` (at most 5 s) before the retry.
6
8
  */
7
- import { ShardfluxApiError, ShardfluxProtocolError, isErrorBody } from "./errors.js";
9
+ import { ShardfluxApiError, ShardfluxProtocolError, apiError, isErrorBody } from "./errors.js";
8
10
  import { describeFailure } from "./progress.js";
9
- export const SDK_VERSION = '0.8.0';
11
+ export const SDK_VERSION = '0.10.0';
10
12
  export const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
13
  /**
12
14
  * The fetch the SDK uses when none is given. On runtimes whose bundled undici is 8.x (Node 26) it sends
@@ -43,6 +45,14 @@ function retryAfterSeconds(res) {
43
45
  const n = Number(v);
44
46
  return Number.isFinite(n) && n >= 0 ? n : undefined;
45
47
  }
48
+ /** `X-Tree-Revision` (file-first workspaces, contracts §29.8): a non-negative integer, else null. */
49
+ export function treeRevisionOf(headers) {
50
+ const v = headers.get('x-tree-revision');
51
+ if (v === null || !/^\d{1,19}$/.test(v))
52
+ return null;
53
+ const n = Number(v);
54
+ return Number.isSafeInteger(n) ? n : null;
55
+ }
46
56
  /** Turns a non-2xx response into a ShardfluxApiError (or a protocol error for undocumented bodies). */
47
57
  export async function errorFrom(res, source) {
48
58
  const text = await res.text();
@@ -54,7 +64,7 @@ export async function errorFrom(res, source) {
54
64
  parsed = null;
55
65
  }
56
66
  if (isErrorBody(parsed))
57
- return new ShardfluxApiError(res.status, parsed, source, retryAfterSeconds(res));
67
+ return apiError(res.status, parsed, source, retryAfterSeconds(res), treeRevisionOf(res.headers) ?? undefined);
58
68
  return new ShardfluxProtocolError(`HTTP ${res.status} from ${source === 'api' ? 'the Shardflux API' : 'the cell gateway'} without an error body`, res.status);
59
69
  }
60
70
  export class HttpClient {
@@ -66,7 +76,7 @@ export class HttpClient {
66
76
  async raw(method, path, init = {}, authorization) {
67
77
  const url = buildUrl(this.opts.baseUrl, path, init.query);
68
78
  const safe = method === 'GET' || method === 'HEAD';
69
- const retriable = safe || init.idempotencyKey !== undefined;
79
+ const retriable = safe || init.idempotencyKey !== undefined || init.idempotent === true;
70
80
  const sleep = this.opts.sleep ?? defaultSleep;
71
81
  for (let attempt = 0;; attempt += 1) {
72
82
  const headers = {
@@ -102,8 +112,17 @@ export class HttpClient {
102
112
  await sleep(delayMs);
103
113
  continue;
104
114
  }
105
- if (res.ok)
115
+ if (res.ok) {
116
+ if (this.opts.onSuccess) {
117
+ try {
118
+ this.opts.onSuccess();
119
+ }
120
+ catch {
121
+ // A hook never fails the request.
122
+ }
123
+ }
106
124
  return res;
125
+ }
107
126
  const error = await errorFrom(res, this.opts.source);
108
127
  const retryableStatus = res.status === 429 || res.status === 502 || res.status === 503 || res.status === 504;
109
128
  const retryableError = error instanceof ShardfluxApiError ? error.retryable && retryableStatus : retryableStatus;
package/dist/index.d.ts CHANGED
@@ -6,18 +6,23 @@
6
6
  * const workspace = await cloud.workspaces.open({ key: `${customerId}/${projectId}`, template: 'python-node-browser' });
7
7
  * await agent.run({ input: userMessage, tools: workspaceTools(workspace) });
8
8
  *
9
+ * File-first workspaces (`mode: 'file_first'`, contracts §29) keep a versioned file tree and run each command as an
10
+ * execution: `await workspace.executions.run(['bash', '-lc', 'pytest -q'])`.
11
+ *
9
12
  * Types come from the committed OpenAPI documents: application API
10
13
  * and cell gateway.
11
14
  */
12
15
  export type { components, operations, paths } from './generated/app-api.js';
13
16
  export type { components as CellComponents, paths as CellPaths } from './generated/cell-api.js';
14
17
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from './client.js';
15
- export type { AgentSession, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ShardfluxOptions, UpdatePolicy, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
16
- export type { FinishedOperation, LifecycleOptions, WaitedLifecycleOptions } from './lifecycle.js';
18
+ export type { AgentSession, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ResumeAnswer, ResumeRequestOptions, ResumeResponse, ShardfluxOptions, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResponse, SuspendWhenIdleResult, UpdatePolicy, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
19
+ export type { FinishedOperation, LifecycleOptions, ResumeOptions, WaitedLifecycleOptions, WaitedResumeOptions } from './lifecycle.js';
17
20
  export { formatTiming } from './progress.js';
18
21
  export type { LifecycleAction, LifecyclePhase, LifecycleTiming, ProgressEvent, ProgressListener, RetryRecord, ServerTiming, TimingOutcome, TimingPhase, } from './progress.js';
22
+ export { FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH } from './feedback.js';
23
+ export type { AccountFeedbackParams, FeedbackCategory, FeedbackContext, FeedbackReceipt, SendFeedbackParams } from './feedback.js';
19
24
  export { UsageApi } from './usage.js';
20
- export type { Grants, Spend, SpendPolicy, UsageEstimate, UsageMeter, UsageSeries, UsageSeriesParams, UsageSummary } from './usage.js';
25
+ export type { Grants, Spend, SpendCap, SpendPolicy, UsageEstimate, UsageMeter, UsageSeries, UsageSeriesParams, UsageSummary } from './usage.js';
21
26
  export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatePackagesApi, TemplateUploadError, TemplateUploadsApi, TemplateVersionTestInstancesApi, TemplateVersionsApi, TemplatesApi, buildSettled, saveAsTemplateBody, } from './templates.js';
22
27
  export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile } from './template-file.js';
23
28
  export type { PackedFile, YamlParser } from './template-file.js';
@@ -33,9 +38,11 @@ export type { AttachVolumeParams, AttachmentResult, CreateVolumeParams, DeleteVo
33
38
  export { AuditApi, auditQuery, parseAuditNdjson } from './audit.js';
34
39
  export type { AuditActorType, AuditEvent, AuditFilters, AuditListParams, AuditOutcome, AuditSource } from './audit.js';
35
40
  export { Workspace } from './workspace.js';
36
- export type { WakeOptions } from './workspace.js';
41
+ export type { HintOptions, HintResult, WakeOptions } from './workspace.js';
37
42
  export { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS, cellPath, ndjson } from './cell.js';
38
- export type { BrowserContent, BrowserContentRequest, BrowserScreenshotRequest, CellClientOptions, ExecSession, ExecStartRequest, FileInfo, FileList, FileWriteResult, GitResult, GitStatus, OutputEvent, ProcessList, PtyOpenRequest, PtySession, RunOptions, RunResult, Signal, WorkspaceChange, WorkspaceChangeKind, WorkspaceChangesPage, WorkspaceChangesParams, WorkspaceChangesSummary, } from './cell.js';
43
+ export { EXECUTION_ID, ExecutionResult, newExecutionId } from './executions.js';
44
+ export type { ExecutionChange, ExecutionError, ExecutionGetOptions, ExecutionResultBody, ExecutionRunOptions, ExecutionState } from './executions.js';
45
+ export type { TreeRevisionOptions, BrowserContent, BrowserContentRequest, BrowserScreenshotRequest, CellClientOptions, ExecSession, ExecStartRequest, FileEdit, FileInfo, FileList, FilePatchEdit, FilePatchParams, FilePatchRequest, FilePatchResult, FileReadResult, FileRevision, FileSearchMatch, FileSearchOptions, FileSearchRequest, FileSearchResponse, FileSearchResult, FileWriteResult, GitResult, GitStatus, OutputEvent, ProcessList, PtyOpenRequest, PtySession, Residency, RunOptions, RunResult, ServedFrom, Signal, WakeHintResult, WorkspaceChange, WorkspaceChangeKind, WorkspaceChangesPage, WorkspaceChangesParams, WorkspaceChangesSummary, } from './cell.js';
39
46
  export { ToolTokenManager } from './tokens.js';
40
47
  export type { ToolName, ToolToken, ToolTokenOptions } from './tokens.js';
41
48
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from './tools.js';
@@ -43,6 +50,10 @@ export type { AnthropicToolDefinition, JsonSchema, OpenAIChatToolDefinition, Ope
43
50
  export { CaptureError, ToolCallCapture, captureTool } from './capture.js';
44
51
  export type { CallLike, CallRef, CaptureCall, CaptureErrorKind, CaptureEvent, CaptureFlushResult, CapturePart, CaptureSelector, CaptureSource, CaptureStats, CaptureStatus, DropReason, ToolCallCaptureOptions, WrapOptions, } from './capture.js';
45
52
  export type { AiSdkAdapter, AiSdkToolEndEvent, AnthropicAdapter, ClaudeAdapter, ClaudeCaptureHooks, ClaudeHookCallback, ClaudeHookMatcher, LangChainAdapter, LangChainToolHandler, MastraAdapter, MastraAfterToolCallContext, McpAdapter, OpenAIAgentsAdapter, } from './capture-adapters.js';
46
- export { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from './errors.js';
47
- export type { AppErrorCode, CellErrorCode, ErrorCode, ErrorReason, KnownErrorReason } from './errors.js';
53
+ export { ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from './errors.js';
54
+ export type { AppErrorCode, CellErrorCode, ErrorCode, ErrorReason, KnownErrorReason, WorkspaceMode } from './errors.js';
48
55
  export { SDK_VERSION } from './http.js';
56
+ export { API_KEY_TOOL_PERMISSIONS, AccountApiKeysApi, AccountAuditApi, AccountAuthApi, AccountBillingApi, AccountExportsApi, AccountInvitationsApi, AccountMembersApi, AccountOrganizationsApi, AccountProjectsApi, AccountTemplatesApi, AccountTotpApi, AccountUserApi, CheckoutTimeoutError, OrganizationExportsApi, SESSION_TOKEN_PATTERN, ShardfluxAccount, isSessionToken, parseEmailToken, } from './account.js';
57
+ export type { AccountClientOptions, AccountDeletion, AccountDeletionCanceled, AccountDeletionScheduled, ApiKey, ApiKeyPage, ApiKeyToolPermission, AuditExportParams, AuthSessionInfo, AuthSessionPage, AuthSessionState, AuthUser, BillingInvoicePage, BillingPortalSession, CheckoutStatus, CreateApiKeyParams, CreatedApiKey, DataExport, EmailChangeConfirmResult, EmailChangeResult, Invitation, InvitationPage, LoginResult, Member, MemberPage, MemberRole, MfaChallengeResult, Organization, OrganizationDeletion, OrganizationDeletionResult, OrganizationPage, OrganizationRole, OrganizationWorkspacePage, OrganizationWorkspacesParams, PageParams, PasswordChangeResult, PasswordResetConfirmResult, PasswordResetRequestResult, Project, ProjectPage, RecoveryCodes, RegisterResult, ResendVerificationResult, SessionTokenUpdate, ShardfluxAccountOptions, SpendPolicyUpdate, StepUpResult, TotpConfirmResult, TotpDisableResult, TotpEnrollment, VerifyEmailResult, WaitForCheckoutOptions, } from './account.js';
58
+ export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from './version-check.js';
59
+ export type { CheckClientVersionOptions, ClientEcosystem, ClientVersionEntry, ClientVersionStatus, ClientVersionStatusKind, ClientVersions, VersionCheckIdentity, VersionCheckOption, } from './version-check.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from "./client.js";
2
2
  export { formatTiming } from "./progress.js";
3
+ export { FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH } from "./feedback.js";
3
4
  export { UsageApi } from "./usage.js";
4
5
  export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatePackagesApi, TemplateUploadError, TemplateUploadsApi, TemplateVersionTestInstancesApi, TemplateVersionsApi, TemplatesApi, buildSettled, saveAsTemplateBody, } from "./templates.js";
5
6
  export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile } from "./template-file.js";
@@ -10,8 +11,11 @@ export { VolumesApi } from "./volumes.js";
10
11
  export { AuditApi, auditQuery, parseAuditNdjson } from "./audit.js";
11
12
  export { Workspace } from "./workspace.js";
12
13
  export { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS, cellPath, ndjson } from "./cell.js";
14
+ export { EXECUTION_ID, ExecutionResult, newExecutionId } from "./executions.js";
13
15
  export { ToolTokenManager } from "./tokens.js";
14
16
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from "./tools.js";
15
17
  export { CaptureError, ToolCallCapture, captureTool } from "./capture.js";
16
- export { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
18
+ export { ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from "./errors.js";
17
19
  export { SDK_VERSION } from "./http.js";
20
+ export { API_KEY_TOOL_PERMISSIONS, AccountApiKeysApi, AccountAuditApi, AccountAuthApi, AccountBillingApi, AccountExportsApi, AccountInvitationsApi, AccountMembersApi, AccountOrganizationsApi, AccountProjectsApi, AccountTemplatesApi, AccountTotpApi, AccountUserApi, CheckoutTimeoutError, OrganizationExportsApi, SESSION_TOKEN_PATTERN, ShardfluxAccount, isSessionToken, parseEmailToken, } from "./account.js";
21
+ export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from "./version-check.js";
@@ -6,10 +6,11 @@
6
6
  * and resolves when the change has FINISHED, returning the succeeded operation (typed FinishedOperation) and, on a
7
7
  * workspace handle, the refreshed view. Both paths are traced (see progress.ts).
8
8
  */
9
- import type { ClientContext, Operation, WaitOptions } from './client.js';
9
+ import type { ClientContext, Operation, WaitOptions, WorkspaceView } from './client.js';
10
10
  import type { RequestOptions } from './http.js';
11
11
  import { Trace } from './progress.js';
12
12
  import type { LifecycleAction, ProgressListener } from './progress.js';
13
+ import type { ToolName, ToolToken } from './tokens.js';
13
14
  /** An operation that has finished successfully: what a lifecycle call with `wait` resolves to. */
14
15
  export type FinishedOperation = Operation & {
15
16
  state: 'succeeded';
@@ -31,15 +32,39 @@ export interface LifecycleOptions {
31
32
  export type WaitedLifecycleOptions = LifecycleOptions & {
32
33
  wait: true | WaitOptions;
33
34
  };
35
+ /**
36
+ * `resume()` options (0.9.0). With `wait`, the server holds the resume until the workspace runs and returns a tool token
37
+ * with it (contracts §22.6): `agentLabel` and `tools` choose that token (defaults: a workspace handle's own, those given
38
+ * to open(); `workspaces.resume(id)` otherwise the key's tools and label `default`). A handle keeps it for its `cell()`
39
+ * clients of the same label and tools; `workspaces.resume(id)` only attributes it.
40
+ */
41
+ export interface ResumeOptions extends LifecycleOptions {
42
+ agentLabel?: string;
43
+ tools?: ToolName[];
44
+ }
45
+ export type WaitedResumeOptions = ResumeOptions & {
46
+ wait: true | WaitOptions;
47
+ };
34
48
  /** Internal: the trace a wait continues instead of starting its own (open(), wake() and waited lifecycle calls). */
35
49
  export declare const TRACE: unique symbol;
36
50
  /** Internal: work a workspace handle does after the operation finished, inside the same trace (refreshing its view). */
37
51
  export declare const AFTER_WAIT: unique symbol;
52
+ /**
53
+ * Internal: a workspace handle's part in a held resume (contracts §22.6): the tool token to ask for, and how it takes the
54
+ * running workspace and that token from a 200 (instead of reading the view and fetching a token afterwards).
55
+ */
56
+ export declare const HELD_RESUME: unique symbol;
57
+ export interface HeldResumeTarget {
58
+ agentLabel: string | undefined;
59
+ tools: readonly ToolName[] | undefined;
60
+ adopt(view: WorkspaceView, token: ToolToken | null): void;
61
+ }
38
62
  export type InternalWaitOptions = WaitOptions & {
39
63
  [TRACE]?: Trace;
40
64
  };
41
- export type InternalLifecycleOptions = LifecycleOptions & {
65
+ export type InternalLifecycleOptions = ResumeOptions & {
42
66
  [AFTER_WAIT]?: (trace: Trace, operation: Operation) => Promise<void>;
67
+ [HELD_RESUME]?: HeldResumeTarget;
43
68
  };
44
69
  export declare function waitOptionsOf(opts: LifecycleOptions): WaitOptions | null;
45
70
  /**
package/dist/lifecycle.js CHANGED
@@ -4,6 +4,11 @@ import { Trace, combineListeners, traced } from "./progress.js";
4
4
  export const TRACE = Symbol('shardflux.trace');
5
5
  /** Internal: work a workspace handle does after the operation finished, inside the same trace (refreshing its view). */
6
6
  export const AFTER_WAIT = Symbol('shardflux.afterWait');
7
+ /**
8
+ * Internal: a workspace handle's part in a held resume (contracts §22.6): the tool token to ask for, and how it takes the
9
+ * running workspace and that token from a 200 (instead of reading the view and fetching a token afterwards).
10
+ */
11
+ export const HELD_RESUME = Symbol('shardflux.heldResume');
7
12
  export function waitOptionsOf(opts) {
8
13
  if (opts.wait === undefined || opts.wait === false)
9
14
  return null;
package/dist/progress.js CHANGED
@@ -81,8 +81,10 @@ export function describeFailure(err, timeoutMs) {
81
81
  if (err.name === 'TimeoutError')
82
82
  return timeoutMs !== undefined ? `timeout after ${timeoutMs} ms` : 'timeout';
83
83
  const e = err;
84
- if (e.name === 'ShardfluxApiError' || e.name === 'ShardfluxProtocolError')
85
- return `HTTP ${e.status}${e.code ? ` ${e.code}` : ''}`;
84
+ // ShardfluxApiError and its subclasses (typed by reason) carry requestId; a protocol error only a status.
85
+ if (e.name === 'ShardfluxProtocolError' || (typeof e.status === 'number' && typeof e.requestId === 'string')) {
86
+ return `HTTP ${e.status}${e.code ? ` ${e.code}` : ''}${typeof e.reason === 'string' ? ` ${e.reason}` : ''}`;
87
+ }
86
88
  return `network: ${e.cause?.code ?? e.cause?.message ?? e.message}`;
87
89
  }
88
90
  return 'network error';
package/dist/templates.js CHANGED
@@ -241,7 +241,7 @@ export class TemplateBuildsApi {
241
241
  * Throws TemplateBuildTimeoutError at the deadline (default 30 min); the build continues.
242
242
  */
243
243
  async waitForBuild(organizationId, buildId, opts = {}) {
244
- const { sleep, http, authorization } = this.#ctx();
244
+ const { sleep, http } = this.#ctx();
245
245
  const timeoutMs = opts.timeoutMs ?? 1_800_000;
246
246
  const maxInterval = opts.maxPollIntervalMs ?? 10_000;
247
247
  let interval = opts.pollIntervalMs ?? 1_000;
@@ -252,7 +252,7 @@ export class TemplateBuildsApi {
252
252
  throw opts.signal.reason instanceof Error ? opts.signal.reason : new Error('aborted');
253
253
  const waitS = opts.serverWait === false ? 0 : (timeoutMs - (Date.now() - started)) / 1000;
254
254
  const t0 = Date.now();
255
- const { body: build, applied } = await pollWithWait(http, `/v1/organizations/${enc(organizationId)}/template-builds/${enc(buildId)}`, authorization, waitS, opts.signal);
255
+ const { body: build, applied } = await pollWithWait(http, `/v1/organizations/${enc(organizationId)}/template-builds/${enc(buildId)}`, this.#ctx().authorization, waitS, opts.signal);
256
256
  const key = `${build.state}/${build.registration.state}`;
257
257
  const changed = key !== lastKey;
258
258
  lastKey = key;
package/dist/tools.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CellClientOptions } from './cell.js';
2
+ import type { WorkspaceMode } from './errors.js';
2
3
  import type { ToolName } from './tokens.js';
3
4
  import type { Workspace } from './workspace.js';
4
5
  /**
@@ -18,6 +19,8 @@ export type JsonSchema = {
18
19
  maximum?: number;
19
20
  minLength?: number;
20
21
  maxLength?: number;
22
+ /** ECMAScript regular expression a string must match (0.7.0+). */
23
+ pattern?: string;
21
24
  minItems?: number;
22
25
  maxItems?: number;
23
26
  default?: unknown;
@@ -44,7 +47,12 @@ export declare class ToolArgumentError extends Error {
44
47
  /** Validates the JSON-Schema subset used by the tool definitions. Returns problems (empty = valid). */
45
48
  export declare function validateArgs(schema: JsonSchema, value: unknown, path?: string): string[];
46
49
  export interface WorkspaceToolsOptions {
47
- /** Tool permissions to expose (default: the tools of the workspace's last token, else all). */
50
+ /**
51
+ * Tool permissions to expose (default: the tools of the workspace's last token, else all). A file-first workspace
52
+ * (`workspace.mode`, contracts §29) gets only the `exec` and `files` tools: `exec` runs each command as an execution
53
+ * (a fresh VM on the workspace's files; the result adds `execution_id`, `state`, `changed` and `tree_revision`), and
54
+ * the process, terminal, git and browser tools are not offered because nothing runs between executions.
55
+ */
48
56
  tools?: ToolName[];
49
57
  /** Attribution label for the tokens these tools use (one agent session per label). */
50
58
  agentLabel?: string;
@@ -58,6 +66,25 @@ export interface WorkspaceToolsOptions {
58
66
  wake?: CellClientOptions['wake'];
59
67
  /** Bound on lifecycle waits (busy waits plus wakes) per tool call (CellClientOptions.transitionTimeoutMs). */
60
68
  transitionTimeoutMs?: number;
69
+ /**
70
+ * Send `workspace.hint()` when a tool call starts, without waiting for it (default true; 0.9.0+), so a parked
71
+ * workspace is being restored while the call is prepared. Pass false when you call `workspace.hint()` yourself
72
+ * earlier (e.g. when the model starts streaming a tool call). `read_file`, `list_files` and `search_files` never send
73
+ * it: a sleeping workspace serves them from its disk without waking (contracts §26.4), and the hint would wake a
74
+ * suspended workspace (or restore a hibernated one) that the read does not need.
75
+ */
76
+ hint?: boolean;
77
+ /**
78
+ * The mode to build the tools for (0.9.0+; default `workspace.mode`). Given, the workspace is not touched until a tool
79
+ * runs, so definitions can be built without one (e.g. to publish them before any workspace exists).
80
+ */
81
+ mode?: WorkspaceMode;
82
+ /**
83
+ * File-first workspaces: called with the execution id before the `exec` tool sends its execution (0.9.0+). An
84
+ * execution cannot be canceled, so a caller that stops waiting (an aborted signal) can still fetch its result later
85
+ * with `workspace.executions.get(id)`.
86
+ */
87
+ onExecution?: (executionId: string) => void;
61
88
  }
62
89
  /** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
63
90
  export declare function workspaceTools(workspace: Workspace, opts?: WorkspaceToolsOptions): WorkspaceTool[];
package/dist/tools.js CHANGED
@@ -11,6 +11,8 @@
11
11
  * providers' formats and executeToolCall dispatches a model's tool call.
12
12
  */
13
13
  import { CAPTURE_BARRIER } from "./cell.js";
14
+ import { ExecStartError } from "./errors.js";
15
+ import { newExecutionId } from "./executions.js";
14
16
  export class ToolArgumentError extends Error {
15
17
  tool;
16
18
  issues;
@@ -51,6 +53,8 @@ export function validateArgs(schema, value, path = '$') {
51
53
  issues.push(`${path} is too short`);
52
54
  if (schema.maxLength !== undefined && value.length > schema.maxLength)
53
55
  issues.push(`${path} is too long`);
56
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern, 'u').test(value))
57
+ issues.push(`${path} must match ${schema.pattern}`);
54
58
  }
55
59
  else if (t === 'integer' || t === 'number') {
56
60
  if (typeof value !== 'number' || !Number.isFinite(value) || (t === 'integer' && !Number.isInteger(value)))
@@ -79,6 +83,12 @@ export function validateArgs(schema, value, path = '$') {
79
83
  return issues;
80
84
  }
81
85
  const ALL = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
86
+ /** The tool permissions whose tools work on a file-first workspace (contracts §29.8): files and executions. */
87
+ const FILE_FIRST_TOOLS = ['exec', 'files'];
88
+ /** Changed paths returned to the model per execution (the rest is flagged `changed_truncated`). */
89
+ const MAX_CHANGED_LISTED = 200;
90
+ /** Tools served from a sleeping workspace's disk without waking it (contracts §26.4): the runner sends no hint. */
91
+ const DISK_READS = new Set(['read_file', 'list_files', 'search_files']);
82
92
  const obj = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
83
93
  const path = (description = 'Absolute path inside the workspace, e.g. /home/user/project/main.py') => ({ type: 'string', minLength: 1, maxLength: 4096, description });
84
94
  const signal = { type: 'string', description: 'Signal name such as SIGTERM, SIGINT or SIGKILL.', minLength: 2, maxLength: 12 };
@@ -97,29 +107,88 @@ export function workspaceTools(workspace, opts = {}) {
97
107
  });
98
108
  const max = opts.maxOutputBytes ?? 65_536;
99
109
  const prefix = opts.prefix ?? '';
100
- const allowed = new Set(opts.tools ?? workspace.grantedTools ?? ALL);
110
+ // A file-first workspace (contracts §29) has files and executions only: no processes, terminals, version control or
111
+ // browser between calls, so those tools are not offered, and exec runs each command as an execution.
112
+ const fileFirst = (opts.mode ?? workspace.mode) === 'file_first';
113
+ const allowed = new Set((opts.tools ?? workspace.grantedTools ?? ALL).filter((t) => !fileFirst || FILE_FIRST_TOOLS.includes(t)));
114
+ const execParameters = obj({
115
+ command: { type: 'string', minLength: 1, maxLength: 100_000, description: 'Shell command line, e.g. "pip install -r requirements.txt && pytest -q".' },
116
+ cwd: { type: 'string', maxLength: 4096, description: 'Working directory (absolute).' },
117
+ timeout_ms: { type: 'integer', minimum: 1000, maximum: 3_600_000, description: 'Kill the command after this long (default 600000).' },
118
+ stdin: { type: 'string', maxLength: 1_000_000, description: 'Text written to stdin.' },
119
+ }, ['command']);
101
120
  const defs = [
102
- {
103
- name: 'exec',
104
- permission: 'exec',
105
- description: 'Run a shell command in the persistent remote workspace (Linux; bash -lc) and return its exit code, stdout and stderr. Files, installed packages and background processes persist between calls.',
106
- parameters: obj({
107
- command: { type: 'string', minLength: 1, maxLength: 100_000, description: 'Shell command line, e.g. "pip install -r requirements.txt && pytest -q".' },
108
- cwd: { type: 'string', maxLength: 4096, description: 'Working directory (absolute).' },
109
- timeout_ms: { type: 'integer', minimum: 1000, maximum: 3_600_000, description: 'Kill the command after this long (default 600000).' },
110
- stdin: { type: 'string', maxLength: 1_000_000, description: 'Text written to stdin.' },
111
- }, ['command']),
112
- run: async (a, o) => {
113
- const r = await cell().exec.run(['bash', '-lc', String(a.command)], {
114
- ...(typeof a.cwd === 'string' ? { cwd: a.cwd } : opts.defaultCwd ? { cwd: opts.defaultCwd } : {}),
115
- timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
116
- ...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
117
- maxOutputBytes: max,
118
- ...(o.signal ? { signal: o.signal } : {}),
119
- });
120
- return { exit_code: r.exitCode, term_signal: r.termSignal, timed_out: r.timedOut, stdout: r.stdout, stderr: r.stderr, truncated: r.truncated, session_id: r.sessionId };
121
+ fileFirst
122
+ ? {
123
+ name: 'exec',
124
+ permission: 'exec',
125
+ description: 'Run a shell command (Linux; bash -lc) in a fresh VM on the workspace’s files and return its exit code, stdout, stderr, the files it changed and the new tree revision. Only files under /home/user persist between calls: processes, background jobs and changes elsewhere (e.g. system packages) do not, so install dependencies into /home/user (e.g. a virtualenv) and start servers within the same command.',
126
+ parameters: execParameters,
127
+ run: async (a, o) => {
128
+ const executionId = newExecutionId();
129
+ opts.onExecution?.(executionId);
130
+ const r = await cell().executions.run(['bash', '-lc', String(a.command)], {
131
+ executionId,
132
+ ...(typeof a.cwd === 'string' ? { cwd: a.cwd } : opts.defaultCwd ? { cwd: opts.defaultCwd } : {}),
133
+ timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
134
+ ...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
135
+ // The cell captures what the model can be given, and flags the rest as truncated.
136
+ outputLimitBytes: Math.max(1, Math.min(16_777_216, max)),
137
+ ...(o.signal ? { signal: o.signal } : {}),
138
+ });
139
+ const out = clip(r.stdoutText, max);
140
+ const err = clip(r.stderrText, max);
141
+ const changed = r.changed.slice(0, MAX_CHANGED_LISTED).map((c) => ({ path: c.path, change: c.change, type: c.type }));
142
+ return {
143
+ exit_code: r.exitCode,
144
+ term_signal: r.termSignal,
145
+ timed_out: r.timedOut,
146
+ stdout: out.text,
147
+ stderr: err.text,
148
+ truncated: r.stdoutTruncated || r.stderrTruncated || out.truncated || err.truncated,
149
+ execution_id: r.executionId,
150
+ state: r.state,
151
+ tree_revision: r.treeRevision,
152
+ changed,
153
+ changed_truncated: r.changedTruncated || r.changed.length > changed.length,
154
+ ...(r.error ? { error: { code: r.error.code, message: r.error.message, reason: r.errorReason } } : {}),
155
+ };
156
+ },
157
+ }
158
+ : {
159
+ name: 'exec',
160
+ permission: 'exec',
161
+ description: 'Run a shell command in the persistent remote workspace (Linux; bash -lc) and return its exit code, stdout and stderr. Files, installed packages and background processes persist between calls.',
162
+ parameters: execParameters,
163
+ run: async (a, o) => {
164
+ let r;
165
+ try {
166
+ r = await cell().exec.run(['bash', '-lc', String(a.command)], {
167
+ ...(typeof a.cwd === 'string' ? { cwd: a.cwd } : opts.defaultCwd ? { cwd: opts.defaultCwd } : {}),
168
+ timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
169
+ ...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
170
+ maxOutputBytes: max,
171
+ ...(o.signal ? { signal: o.signal } : {}),
172
+ });
173
+ }
174
+ catch (e) {
175
+ // Nothing ran (e.g. a cwd that is not a directory): the model gets the reason, as for an execution.
176
+ if (!(e instanceof ExecStartError))
177
+ throw e;
178
+ return {
179
+ exit_code: null,
180
+ term_signal: null,
181
+ timed_out: false,
182
+ stdout: '',
183
+ stderr: '',
184
+ truncated: false,
185
+ session_id: e.sessionId,
186
+ error: { code: e.code, message: e.message, reason: e.reason },
187
+ };
188
+ }
189
+ return { exit_code: r.exitCode, term_signal: r.termSignal, timed_out: r.timedOut, stdout: r.stdout, stderr: r.stderr, truncated: r.truncated, session_id: r.sessionId };
190
+ },
121
191
  },
122
- },
123
192
  {
124
193
  name: 'read_file',
125
194
  permission: 'files',
@@ -159,6 +228,77 @@ export function workspaceTools(workspace, opts = {}) {
159
228
  return { entries: r.entries.map((e) => ({ name: e.name, path: e.path, type: e.type, size: e.size, modified_at: e.modified_at })), truncated: r.truncated };
160
229
  },
161
230
  },
231
+ {
232
+ name: 'search_files',
233
+ permission: 'files',
234
+ description: 'Search file contents under a directory (or in one file) in the workspace (like grep -rn) and return the matching lines with path, line and column. The pattern is literal text unless regex is true (RE2 syntax). Binary files, symbolic links and .git/node_modules directories are skipped.',
235
+ parameters: obj({
236
+ path: path('Directory (or one file) to search, absolute, e.g. /home/user/project.'),
237
+ pattern: { type: 'string', minLength: 1, maxLength: 1000, description: 'Text to find, or an RE2 regular expression when regex is true.' },
238
+ regex: { type: 'boolean', description: 'Treat pattern as an RE2 regular expression.' },
239
+ case_insensitive: { type: 'boolean', description: 'Ignore case.' },
240
+ include: { type: 'array', maxItems: 32, items: { type: 'string', minLength: 1, maxLength: 256 }, description: 'Only files matching one of these gitignore-style globs: "*.py" matches the name at any depth, "src/**/*.ts" the path relative to path.' },
241
+ exclude: { type: 'array', maxItems: 32, items: { type: 'string', minLength: 1, maxLength: 256 }, description: 'Skip files and directories matching these globs, e.g. "build/" (default .git and node_modules; [] searches everything).' },
242
+ max_matches: { type: 'integer', minimum: 1, maximum: 5000, description: 'Stop after this many matches (default 200).' },
243
+ context_lines: { type: 'integer', minimum: 0, maximum: 5, description: 'Lines of context to return before and after each match.' },
244
+ }, ['path', 'pattern']),
245
+ run: async (a, o) => {
246
+ const r = await cell().files.search(String(a.path), String(a.pattern), {
247
+ ...(typeof a.regex === 'boolean' ? { regex: a.regex } : {}),
248
+ ...(typeof a.case_insensitive === 'boolean' ? { caseInsensitive: a.case_insensitive } : {}),
249
+ ...(Array.isArray(a.include) ? { include: a.include } : {}),
250
+ ...(Array.isArray(a.exclude) ? { exclude: a.exclude } : {}),
251
+ ...(typeof a.max_matches === 'number' ? { maxMatches: a.max_matches } : {}),
252
+ ...(typeof a.context_lines === 'number' ? { contextLines: a.context_lines } : {}),
253
+ ...(o.signal ? { signal: o.signal } : {}),
254
+ });
255
+ // Whole matches only, up to the output budget; the rest is counted.
256
+ const matches = [];
257
+ let used = 0;
258
+ for (const m of r.matches) {
259
+ used += Buffer.byteLength(JSON.stringify(m), 'utf8');
260
+ if (used > max)
261
+ break;
262
+ matches.push(m);
263
+ }
264
+ const omitted = r.matches.length - matches.length;
265
+ return { matches, truncated: r.truncated || omitted > 0, stop_reason: r.stop_reason ?? null, omitted_matches: omitted, files_scanned: r.files_scanned };
266
+ },
267
+ },
268
+ {
269
+ name: 'edit_file',
270
+ permission: 'files',
271
+ description: 'Edit a text file in the workspace by replacing exact text. Each old_text must occur exactly once in the file (include enough surrounding lines to make it unique) unless replace_all is true. The edits apply in order and atomically: all of them or none. Fails without changing anything if the file changed since expected_revision; read it again then.',
272
+ parameters: obj({
273
+ path: path(),
274
+ edits: {
275
+ type: 'array',
276
+ minItems: 1,
277
+ maxItems: 100,
278
+ description: 'Edits applied in order.',
279
+ items: obj({
280
+ old_text: { type: 'string', minLength: 1, maxLength: 1_048_576, description: 'Exact text to replace, including whitespace and indentation.' },
281
+ new_text: { type: 'string', maxLength: 1_048_576, description: 'Replacement text (empty to delete).' },
282
+ replace_all: { type: 'boolean', description: 'Replace every occurrence instead of requiring exactly one.' },
283
+ }, ['old_text', 'new_text']),
284
+ },
285
+ expected_revision: {
286
+ type: 'string',
287
+ pattern: '^[0-9a-f]{64}$',
288
+ description: 'The revision your edits are based on (the revision returned by the previous edit_file of this file). Default: the file’s revision read just before editing.',
289
+ },
290
+ }, ['path', 'edits']),
291
+ run: async (a, o) => {
292
+ const c = cell();
293
+ const file = String(a.path);
294
+ // Without a revision from the model, pin the edit to the content current now, so a concurrent change between
295
+ // this read and the patch is refused (revision_mismatch) instead of edited blindly.
296
+ const expected = typeof a.expected_revision === 'string' ? a.expected_revision : (await c.files.stat(file, { revision: true })).revision;
297
+ const edits = a.edits.map((e) => ({ oldText: e.old_text, newText: e.new_text, ...(e.replace_all !== undefined ? { replaceAll: e.replace_all } : {}) }));
298
+ const r = await c.files.patch({ path: file, edits, ...(expected !== undefined ? { expectedRevision: expected } : {}) }, o.signal ? { signal: o.signal } : {});
299
+ return { path: r.path, revision: r.revision, previous_revision: r.previous_revision, replacements: r.replacements ?? null, bytes_written: r.bytes_written };
300
+ },
301
+ },
162
302
  {
163
303
  name: 'list_processes',
164
304
  permission: 'process',
@@ -296,6 +436,17 @@ export function workspaceTools(workspace, opts = {}) {
296
436
  const issues = validateArgs(d.parameters, args);
297
437
  if (issues.length > 0)
298
438
  throw new ToolArgumentError(`${prefix}${d.name}`, issues);
439
+ // Fire and forget: a parked workspace starts restoring while this call is prepared (contracts §26.6). Not for
440
+ // the reads a sleeping workspace serves from its disk (§26.4): the hint would wake it for nothing.
441
+ if (opts.hint !== false && !DISK_READS.has(d.name)) {
442
+ workspace
443
+ .hint({
444
+ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}),
445
+ ...(opts.wake !== undefined ? { wake: opts.wake } : {}),
446
+ ...(opts.transitionTimeoutMs !== undefined ? { wakeTimeoutMs: opts.transitionTimeoutMs } : {}),
447
+ })
448
+ .catch(() => undefined);
449
+ }
299
450
  // Read-your-writes: tool calls captured before this one are in the workspace before it runs (bounded).
300
451
  const barrier = workspace[CAPTURE_BARRIER];
301
452
  const pending = typeof barrier === 'function' ? barrier.call(workspace) : undefined;