@wice24/tars-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @wice24/tars-sdk
2
+
3
+ TypeScript client for [TARS](https://github.com/meridian-capital/tars) — a
4
+ self-hosted managed-agents platform: server-side agent loops in sandboxes,
5
+ an append-only session event log with SSE streaming + replay, client-executed
6
+ tools, per-end-user MCP credentials, skills, files, and usage metering.
7
+
8
+ ```bash
9
+ npm install @wice24/tars-sdk
10
+ ```
11
+
12
+ ## Quickstart
13
+
14
+ ```ts
15
+ import { TarsClient } from '@wice24/tars-sdk';
16
+
17
+ const tars = new TarsClient({
18
+ apiKey: process.env.TARS_API_KEY!, // tars_sk_…
19
+ baseUrl: 'https://your-tars-host',
20
+ });
21
+
22
+ const agent = await tars.agents.create({
23
+ name: 'assistant',
24
+ system: 'You are a helpful assistant.',
25
+ model: 'anthropic/claude-sonnet-5',
26
+ });
27
+
28
+ const session = await tars.sessions.create({ agentId: agent.id });
29
+ for await (const event of session.send('Summarize the attached file.')) {
30
+ if (event.type === 'text_delta') process.stdout.write(event.text);
31
+ }
32
+ ```
33
+
34
+ ## Streaming surfaces
35
+
36
+ - `session.send(text)` — yields the turn's events; client-executed tools you
37
+ registered (`sessions.create({ tools })` / `session.onToolCall`) are run and
38
+ their results returned to the turn automatically.
39
+ - `session.sendRaw(text)` — same events, **no auto-dispatch**: hosts that own
40
+ their tool dispatcher consume this.
41
+ - `session.stream({ after })` — full event log: replay after a seq cursor,
42
+ then live-tail. Survives page refreshes and server restarts.
43
+ - `session.resume({ after })` — current-turn reconnect: replay after the
44
+ cursor, then close immediately when idle or right after `turn_end`.
45
+ Never re-dispatches client tools.
46
+ - `session.interrupt()` — abort the in-flight turn; partial text is kept.
47
+
48
+ Every event carries a monotonic `seq` — persist the last one you rendered and
49
+ reconnect with it.
50
+
51
+ ## Beyond sessions
52
+
53
+ `tars.agents` (versioned configs, skills pins, MCP servers, sandbox
54
+ environment), `tars.skills` (zip bundles with a `SKILL.md`, immutable
55
+ versions), `tars.files` (upload / mount / outputs), `tars.mcpCredentials`
56
+ (per-end-user or tenant-wide, OAuth refresh with rotation), `tars.usage`,
57
+ `tars.apiKeys`, and `tars.completions` for cheap one-shot generations.
58
+
59
+ ## Testing
60
+
61
+ Inject `fetchImpl` to stub transport:
62
+
63
+ ```ts
64
+ const tars = new TarsClient({ apiKey: 'tars_sk_test', fetchImpl: myStub });
65
+ ```
66
+
67
+ ## Requirements
68
+
69
+ Node 18+ (global `fetch`). ESM only.
@@ -0,0 +1,218 @@
1
+ import type { Agent, AgentVersion, ClientTool, CreateAgentInput, FileMeta, McpCredentialInput, McpCredentialStatus, SessionEvent, SessionInfo, SessionListItem, UpdateAgentInput, UploadFileInput, UsageSummary, McpServer, Skill, SkillVersion, ApiKeyInfo } from './types.js';
2
+ type ToolHandler = (input: any) => unknown | Promise<unknown>;
3
+ export * from './types.js';
4
+ export declare class TarsApiError extends Error {
5
+ readonly status: number;
6
+ readonly body: unknown;
7
+ constructor(status: number, body: unknown, message?: string);
8
+ }
9
+ export interface TarsClientOptions {
10
+ apiKey: string;
11
+ baseUrl?: string;
12
+ /** Injectable fetch (tests, custom agents/proxies). Defaults to global fetch. */
13
+ fetchImpl?: typeof globalThis.fetch;
14
+ }
15
+ export declare class TarsClient {
16
+ private readonly baseUrl;
17
+ private readonly apiKey;
18
+ private readonly fetchImpl;
19
+ constructor(options: TarsClientOptions);
20
+ private request;
21
+ readonly agents: {
22
+ create: (input: CreateAgentInput) => Promise<Agent>;
23
+ list: (options?: {
24
+ includeArchived?: boolean;
25
+ }) => Promise<Agent[]>;
26
+ get: (id: string) => Promise<Agent>;
27
+ update: (id: string, patch: UpdateAgentInput) => Promise<Agent>;
28
+ archive: (id: string) => Promise<{
29
+ ok: boolean;
30
+ }>;
31
+ unarchive: (id: string) => Promise<{
32
+ ok: boolean;
33
+ }>;
34
+ versions: (id: string) => Promise<AgentVersion[]>;
35
+ };
36
+ readonly sessions: {
37
+ create: (input: {
38
+ agentId: string;
39
+ model?: string;
40
+ endUserId?: string;
41
+ title?: string;
42
+ /** String-KV scoping/labels (owner_id/app_id conventions). */
43
+ metadata?: Record<string, string>;
44
+ /** Extra MCP servers for THIS session (per-user connectors); the
45
+ * agent's declared servers win on name collision. */
46
+ mcpServers?: McpServer[];
47
+ /** Appended to the agent's system prompt for this session only. */
48
+ systemSuffix?: string;
49
+ tools?: ClientTool[];
50
+ }) => Promise<TarsSession>;
51
+ list: (opts?: {
52
+ limit?: number;
53
+ cursor?: string;
54
+ /** Filter by metadata keys, e.g. { owner_id: 'u1', app_id: 'coral' }. */
55
+ metadata?: Record<string, string>;
56
+ includeArchived?: boolean;
57
+ }) => Promise<{
58
+ sessions: SessionListItem[];
59
+ nextCursor: string | null;
60
+ }>;
61
+ rename: (id: string, title: string) => Promise<{
62
+ ok: boolean;
63
+ }>;
64
+ /** Replace the session's extra MCP server set (takes effect next turn;
65
+ * 409s while a turn is in flight). */
66
+ setMcpServers: (id: string, servers: McpServer[]) => Promise<{
67
+ ok: boolean;
68
+ }>;
69
+ /** Abort the in-flight turn, if any. Idempotent. */
70
+ interrupt: (id: string) => Promise<{
71
+ ok: boolean;
72
+ interrupted: boolean;
73
+ }>;
74
+ usage: (id: string) => Promise<{
75
+ session_id: string;
76
+ model: string;
77
+ input_tokens: number;
78
+ output_tokens: number;
79
+ cache_read_tokens: number;
80
+ cache_write_tokens: number;
81
+ cost_usd: number | null;
82
+ turns: number;
83
+ }>;
84
+ get: (id: string) => Promise<SessionInfo>;
85
+ open: (id: string, opts?: {
86
+ tools?: ClientTool[];
87
+ }) => Promise<TarsSession>;
88
+ destroy: (id: string) => Promise<{
89
+ ok: boolean;
90
+ }>;
91
+ };
92
+ readonly mcpCredentials: {
93
+ set: (input: {
94
+ server: string;
95
+ auth: McpCredentialInput;
96
+ endUserId?: string;
97
+ }) => Promise<void>;
98
+ list: (endUserId?: string) => Promise<McpCredentialStatus[]>;
99
+ delete: (server: string, endUserId?: string) => Promise<{
100
+ ok: boolean;
101
+ }>;
102
+ };
103
+ readonly skills: {
104
+ create: (input: {
105
+ slug: string;
106
+ displayTitle: string;
107
+ }) => Promise<Skill>;
108
+ list: () => Promise<Skill[]>;
109
+ get: (id: string) => Promise<Skill>;
110
+ versions: (id: string) => Promise<SkillVersion[]>;
111
+ /** Upload a new immutable version from zip bytes. */
112
+ pushVersion: (id: string, zipBytes: Uint8Array) => Promise<{
113
+ skill_id: string;
114
+ slug: string;
115
+ version: number;
116
+ files: number;
117
+ }>;
118
+ };
119
+ readonly apiKeys: {
120
+ create: (input?: {
121
+ name?: string;
122
+ }) => Promise<{
123
+ id: string;
124
+ key: string;
125
+ key_prefix: string;
126
+ }>;
127
+ list: () => Promise<ApiKeyInfo[]>;
128
+ revoke: (id: string) => Promise<{
129
+ ok: boolean;
130
+ }>;
131
+ };
132
+ readonly completions: {
133
+ create: (input: {
134
+ prompt: string;
135
+ model?: string;
136
+ system?: string;
137
+ maxTokens?: number;
138
+ }) => Promise<{
139
+ text: string;
140
+ usage: {
141
+ input_tokens: number;
142
+ output_tokens: number;
143
+ };
144
+ }>;
145
+ };
146
+ readonly usage: {
147
+ get: (filter?: {
148
+ from?: string | Date;
149
+ to?: string | Date;
150
+ sessionId?: string;
151
+ endUserId?: string;
152
+ }) => Promise<UsageSummary>;
153
+ };
154
+ readonly files: {
155
+ upload: (input: UploadFileInput) => Promise<FileMeta>;
156
+ uploadText: (name: string, content: string, mime?: string) => Promise<FileMeta>;
157
+ list: (filter?: {
158
+ sessionId?: string;
159
+ name?: string;
160
+ }) => Promise<FileMeta[]>;
161
+ get: (id: string) => Promise<FileMeta>;
162
+ download: (id: string) => Promise<{
163
+ body: ReadableStream<Uint8Array>;
164
+ contentType: string;
165
+ }>;
166
+ downloadBytes: (id: string) => Promise<Uint8Array>;
167
+ };
168
+ mountFile(sessionId: string, input: {
169
+ fileId: string;
170
+ mountPath?: string;
171
+ }): Promise<{
172
+ mount_path: string;
173
+ size_bytes: number;
174
+ }>;
175
+ /** Send one user message and stream the turn's raw events. No client-tool
176
+ * auto-dispatch happens here — callers that registered tool handlers want
177
+ * TarsSession.send(); adapters that dispatch themselves want this (or
178
+ * TarsSession.sendRaw). */
179
+ streamMessage(sessionId: string, text: string): Promise<AsyncGenerator<SessionEvent>>;
180
+ /** Follow the session event log: replay after `after`, then live-tail.
181
+ * mode 'turn' is the reconnect shape: closes immediately when the session
182
+ * is idle, else closes right after the in-flight turn's turn_end. */
183
+ streamEvents(sessionId: string, after: number, opts?: {
184
+ mode?: 'turn';
185
+ }): Promise<AsyncGenerator<SessionEvent>>;
186
+ /** @internal used by TarsSession to return a client tool result */
187
+ submitToolResult(sessionId: string, callId: string, output: unknown, isError: boolean): Promise<void>;
188
+ }
189
+ export declare class TarsSession {
190
+ private readonly client;
191
+ readonly info: SessionInfo;
192
+ private readonly toolHandlers;
193
+ constructor(client: TarsClient, info: SessionInfo, toolHandlers?: Map<string, ToolHandler>);
194
+ get id(): string;
195
+ onToolCall(name: string, execute: ToolHandler): this;
196
+ send(text: string): AsyncGenerator<SessionEvent>;
197
+ private fulfill;
198
+ sendRaw(text: string): AsyncGenerator<SessionEvent>;
199
+ stream(opts?: {
200
+ after?: number;
201
+ }): AsyncGenerator<SessionEvent>;
202
+ resume(opts?: {
203
+ after?: number;
204
+ }): AsyncGenerator<SessionEvent>;
205
+ mountFile(fileId: string, mountPath?: string): Promise<{
206
+ mount_path: string;
207
+ size_bytes: number;
208
+ }>;
209
+ attachFile(input: UploadFileInput & {
210
+ mountPath?: string;
211
+ }): Promise<{
212
+ file: FileMeta;
213
+ mount_path: string;
214
+ }>;
215
+ destroy(): Promise<{
216
+ ok: boolean;
217
+ }>;
218
+ }
package/dist/index.js ADDED
@@ -0,0 +1,402 @@
1
+ export * from './types.js';
2
+ export class TarsApiError extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body, message) {
6
+ super(message ?? `TARS API error ${status}: ${JSON.stringify(body)}`);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.name = 'TarsApiError';
10
+ }
11
+ }
12
+ export class TarsClient {
13
+ baseUrl;
14
+ apiKey;
15
+ fetchImpl;
16
+ constructor(options) {
17
+ if (!options.apiKey)
18
+ throw new Error('TarsClient: apiKey is required');
19
+ this.apiKey = options.apiKey;
20
+ this.baseUrl = (options.baseUrl ?? 'http://localhost:3001').replace(/\/$/, '');
21
+ // Bind: an unbound global fetch throws "Illegal invocation" in browsers.
22
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
23
+ }
24
+ // ── low-level HTTP ───────────────────────────────────────────
25
+ async request(method, path, body) {
26
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
27
+ method,
28
+ headers: {
29
+ authorization: `Bearer ${this.apiKey}`,
30
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
31
+ },
32
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
33
+ });
34
+ const payload = await res.json().catch(() => undefined);
35
+ if (!res.ok)
36
+ throw new TarsApiError(res.status, payload);
37
+ return payload;
38
+ }
39
+ // ── agents ───────────────────────────────────────────────────
40
+ agents = {
41
+ create: (input) => this.request('POST', '/v1/agents', input),
42
+ list: async (options) => {
43
+ const qs = options?.includeArchived ? '?include_archived=true' : '';
44
+ const { agents } = await this.request('GET', `/v1/agents${qs}`);
45
+ return agents;
46
+ },
47
+ get: (id) => this.request('GET', `/v1/agents/${id}`),
48
+ update: (id, patch) => this.request('PATCH', `/v1/agents/${id}`, patch),
49
+ archive: (id) => this.request('POST', `/v1/agents/${id}/archive`),
50
+ unarchive: (id) => this.request('POST', `/v1/agents/${id}/unarchive`),
51
+ versions: async (id) => {
52
+ const { versions } = await this.request('GET', `/v1/agents/${id}/versions`);
53
+ return versions;
54
+ },
55
+ };
56
+ // ── sessions ─────────────────────────────────────────────────
57
+ sessions = {
58
+ create: async (input) => {
59
+ const info = await this.request('POST', '/v1/sessions', {
60
+ agent_id: input.agentId,
61
+ ...(input.model ? { model: input.model } : {}),
62
+ ...(input.endUserId ? { end_user_id: input.endUserId } : {}),
63
+ ...(input.title ? { title: input.title } : {}),
64
+ ...(input.metadata ? { metadata: input.metadata } : {}),
65
+ ...(input.mcpServers?.length ? { mcp_servers: input.mcpServers } : {}),
66
+ ...(input.systemSuffix ? { system_suffix: input.systemSuffix } : {}),
67
+ ...(input.tools?.length
68
+ ? {
69
+ custom_tools: input.tools.map((t) => ({
70
+ name: t.name,
71
+ description: t.description,
72
+ input_schema: t.inputSchema,
73
+ ...(t.timeoutSec ? { timeout_sec: t.timeoutSec } : {}),
74
+ })),
75
+ }
76
+ : {}),
77
+ });
78
+ const handlers = new Map((input.tools ?? []).map((t) => [t.name, t.execute]));
79
+ return new TarsSession(this, info, handlers);
80
+ },
81
+ list: async (opts) => {
82
+ const params = new URLSearchParams();
83
+ if (opts?.limit)
84
+ params.set('limit', String(opts.limit));
85
+ if (opts?.cursor)
86
+ params.set('cursor', opts.cursor);
87
+ if (opts?.includeArchived)
88
+ params.set('include_archived', 'true');
89
+ for (const [k, v] of Object.entries(opts?.metadata ?? {}))
90
+ params.set(`metadata.${k}`, v);
91
+ const qs = params.size ? `?${params}` : '';
92
+ const res = await this.request('GET', `/v1/sessions${qs}`);
93
+ return { sessions: res.sessions, nextCursor: res.next_cursor };
94
+ },
95
+ rename: (id, title) => this.request('PATCH', `/v1/sessions/${id}`, { title }),
96
+ /** Replace the session's extra MCP server set (takes effect next turn;
97
+ * 409s while a turn is in flight). */
98
+ setMcpServers: (id, servers) => this.request('PATCH', `/v1/sessions/${id}`, { mcp_servers: servers }),
99
+ /** Abort the in-flight turn, if any. Idempotent. */
100
+ interrupt: (id) => this.request('POST', `/v1/sessions/${id}/interrupt`),
101
+ usage: (id) => this.request('GET', `/v1/sessions/${id}/usage`),
102
+ get: (id) => this.request('GET', `/v1/sessions/${id}`),
103
+ // Re-open an existing session (e.g. after a page refresh or on another
104
+ // device) — sessions are durable server-side resources. Re-supply `tools`
105
+ // to keep fulfilling client tool calls (handlers are code, not stored).
106
+ open: async (id, opts) => {
107
+ const info = await this.request('GET', `/v1/sessions/${id}`);
108
+ const handlers = new Map((opts?.tools ?? []).map((t) => [t.name, t.execute]));
109
+ return new TarsSession(this, info, handlers);
110
+ },
111
+ destroy: (id) => this.request('DELETE', `/v1/sessions/${id}`),
112
+ };
113
+ // ── MCP credentials ──────────────────────────────────────────
114
+ // The tenant supplies a credential (obtained via their own OAuth) for an
115
+ // external MCP server; TARS stores it in Key Vault and injects it when the
116
+ // agent calls that server. Scope to an end-user via endUserId, or omit for a
117
+ // tenant-level credential.
118
+ mcpCredentials = {
119
+ set: (input) => this.request('PUT', '/v1/mcp-credentials', {
120
+ server: input.server,
121
+ auth: input.auth,
122
+ ...(input.endUserId ? { end_user_id: input.endUserId } : {}),
123
+ }),
124
+ list: async (endUserId) => {
125
+ const qs = endUserId ? `?end_user_id=${encodeURIComponent(endUserId)}` : '';
126
+ const { credentials } = await this.request('GET', `/v1/mcp-credentials${qs}`);
127
+ return credentials;
128
+ },
129
+ delete: (server, endUserId) => {
130
+ const params = new URLSearchParams({ server });
131
+ if (endUserId)
132
+ params.set('end_user_id', endUserId);
133
+ return this.request('DELETE', `/v1/mcp-credentials?${params}`);
134
+ },
135
+ };
136
+ // ── usage ────────────────────────────────────────────────────
137
+ // Versioned skill bundles (zip with SKILL.md at root), pinned per agent.
138
+ skills = {
139
+ create: (input) => this.request('POST', '/v1/skills', { slug: input.slug, display_title: input.displayTitle }),
140
+ list: async () => (await this.request('GET', '/v1/skills')).skills,
141
+ get: (id) => this.request('GET', `/v1/skills/${id}`),
142
+ versions: async (id) => (await this.request('GET', `/v1/skills/${id}/versions`)).versions,
143
+ /** Upload a new immutable version from zip bytes. */
144
+ pushVersion: async (id, zipBytes) => {
145
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/skills/${id}/versions`, {
146
+ method: 'POST',
147
+ headers: { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/zip' },
148
+ body: zipBytes,
149
+ });
150
+ if (!res.ok)
151
+ throw new TarsApiError(res.status, await res.json().catch(() => ({})));
152
+ return res.json();
153
+ },
154
+ };
155
+ apiKeys = {
156
+ create: (input) => this.request('POST', '/v1/api-keys', { ...(input?.name ? { name: input.name } : {}) }),
157
+ list: async () => (await this.request('GET', '/v1/api-keys')).keys,
158
+ revoke: (id) => this.request('DELETE', `/v1/api-keys/${id}`),
159
+ };
160
+ // Cheap non-session completion (titles and other one-shots) — routed
161
+ // through the same provider seam as agent turns.
162
+ completions = {
163
+ create: (input) => this.request('POST', '/v1/completions', {
164
+ prompt: input.prompt,
165
+ ...(input.model ? { model: input.model } : {}),
166
+ ...(input.system ? { system: input.system } : {}),
167
+ ...(input.maxTokens ? { max_tokens: input.maxTokens } : {}),
168
+ }),
169
+ };
170
+ usage = {
171
+ // Aggregated token/cost usage for this tenant, with per-model, per-agent,
172
+ // and per-end-user breakdowns. Dates are ISO 8601; sessionId narrows to one
173
+ // session; endUserId narrows to (and is the key to bill) one tenant user.
174
+ get: async (filter) => {
175
+ const params = new URLSearchParams();
176
+ if (filter?.from)
177
+ params.set('from', new Date(filter.from).toISOString());
178
+ if (filter?.to)
179
+ params.set('to', new Date(filter.to).toISOString());
180
+ if (filter?.sessionId)
181
+ params.set('session_id', filter.sessionId);
182
+ if (filter?.endUserId)
183
+ params.set('end_user_id', filter.endUserId);
184
+ const qs = params.size ? `?${params}` : '';
185
+ return this.request('GET', `/v1/usage${qs}`);
186
+ },
187
+ };
188
+ // ── files ────────────────────────────────────────────────────
189
+ files = {
190
+ // Upload bytes to the File API (registry + blob storage). Not tied to a
191
+ // session until mounted (or produced by an agent as an output).
192
+ upload: async (input) => {
193
+ const bytes = typeof input.content === 'string'
194
+ ? new TextEncoder().encode(input.content)
195
+ : input.content;
196
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/files?name=${encodeURIComponent(input.name)}`, {
197
+ method: 'POST',
198
+ headers: {
199
+ authorization: `Bearer ${this.apiKey}`,
200
+ 'content-type': input.mime ?? 'application/octet-stream',
201
+ },
202
+ body: bytes,
203
+ });
204
+ const payload = await res.json().catch(() => undefined);
205
+ if (!res.ok)
206
+ throw new TarsApiError(res.status, payload);
207
+ return payload;
208
+ },
209
+ uploadText: (name, content, mime = 'text/plain') => this.files.upload({ name, content, mime }),
210
+ // List/search files — filter by session (includes agent outputs and files
211
+ // mounted into it) and/or exact name.
212
+ list: async (filter) => {
213
+ const params = new URLSearchParams();
214
+ if (filter?.sessionId)
215
+ params.set('session_id', filter.sessionId);
216
+ if (filter?.name)
217
+ params.set('name', filter.name);
218
+ const qs = params.size ? `?${params}` : '';
219
+ const { files } = await this.request('GET', `/v1/files${qs}`);
220
+ return files;
221
+ },
222
+ get: (id) => this.request('GET', `/v1/files/${id}`),
223
+ download: async (id) => {
224
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/files/${id}/content`, {
225
+ headers: { authorization: `Bearer ${this.apiKey}` },
226
+ });
227
+ if (!res.ok || !res.body) {
228
+ throw new TarsApiError(res.status, await res.json().catch(() => undefined));
229
+ }
230
+ return {
231
+ body: res.body,
232
+ contentType: res.headers.get('content-type') ?? 'application/octet-stream',
233
+ };
234
+ },
235
+ downloadBytes: async (id) => {
236
+ const { body } = await this.files.download(id);
237
+ const chunks = [];
238
+ for await (const chunk of body)
239
+ chunks.push(chunk);
240
+ const total = chunks.reduce((n, c) => n + c.length, 0);
241
+ const out = new Uint8Array(total);
242
+ let offset = 0;
243
+ for (const c of chunks) {
244
+ out.set(c, offset);
245
+ offset += c.length;
246
+ }
247
+ return out;
248
+ },
249
+ };
250
+ // Copies an uploaded file into a session's sandbox. Default mount path is
251
+ // /mnt/session/uploads/<name>; idempotent for the same file+path.
252
+ async mountFile(sessionId, input) {
253
+ return this.request('POST', `/v1/sessions/${sessionId}/files`, {
254
+ file_id: input.fileId,
255
+ ...(input.mountPath ? { mount_path: input.mountPath } : {}),
256
+ });
257
+ }
258
+ /** Send one user message and stream the turn's raw events. No client-tool
259
+ * auto-dispatch happens here — callers that registered tool handlers want
260
+ * TarsSession.send(); adapters that dispatch themselves want this (or
261
+ * TarsSession.sendRaw). */
262
+ async streamMessage(sessionId, text) {
263
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/sessions/${sessionId}/messages`, {
264
+ method: 'POST',
265
+ headers: {
266
+ authorization: `Bearer ${this.apiKey}`,
267
+ 'content-type': 'application/json',
268
+ },
269
+ body: JSON.stringify({ text }),
270
+ });
271
+ if (!res.ok || !res.body) {
272
+ const payload = await res.json().catch(() => undefined);
273
+ throw new TarsApiError(res.status, payload);
274
+ }
275
+ return parseSse(res.body);
276
+ }
277
+ /** Follow the session event log: replay after `after`, then live-tail.
278
+ * mode 'turn' is the reconnect shape: closes immediately when the session
279
+ * is idle, else closes right after the in-flight turn's turn_end. */
280
+ async streamEvents(sessionId, after, opts) {
281
+ const qs = `after=${after}` + (opts?.mode ? `&mode=${opts.mode}` : '');
282
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/sessions/${sessionId}/stream?${qs}`, {
283
+ headers: { authorization: `Bearer ${this.apiKey}` },
284
+ });
285
+ if (!res.ok || !res.body) {
286
+ const payload = await res.json().catch(() => undefined);
287
+ throw new TarsApiError(res.status, payload);
288
+ }
289
+ return parseSse(res.body);
290
+ }
291
+ /** @internal used by TarsSession to return a client tool result */
292
+ async submitToolResult(sessionId, callId, output, isError) {
293
+ await this.request('POST', `/v1/sessions/${sessionId}/tool-results`, {
294
+ tool_call_id: callId,
295
+ output,
296
+ is_error: isError,
297
+ });
298
+ }
299
+ }
300
+ // One live agent session: send messages, iterate events.
301
+ export class TarsSession {
302
+ client;
303
+ info;
304
+ toolHandlers;
305
+ constructor(client, info, toolHandlers = new Map()) {
306
+ this.client = client;
307
+ this.info = info;
308
+ this.toolHandlers = toolHandlers;
309
+ }
310
+ get id() {
311
+ return this.info.id;
312
+ }
313
+ // Register (or replace) a client tool handler after construction — handy when
314
+ // reopening a session. Mirrors passing `tools` to sessions.create/open.
315
+ onToolCall(name, execute) {
316
+ this.toolHandlers.set(name, execute);
317
+ return this;
318
+ }
319
+ // Sends one user message; yields this turn's events as they stream. When the
320
+ // agent calls a client-executed tool, the SDK runs the registered handler and
321
+ // returns its result to the turn transparently — the caller still sees the
322
+ // tool_call / tool_result events.
323
+ async *send(text) {
324
+ const inflight = [];
325
+ try {
326
+ for await (const event of await this.client.streamMessage(this.info.id, text)) {
327
+ if (event.type === 'tool_call' && event.client_executed && event.id) {
328
+ const handler = this.toolHandlers.get(event.tool);
329
+ if (handler)
330
+ inflight.push(this.fulfill(handler, event.id, event.input));
331
+ // No handler → the backend times out this call as an error result.
332
+ }
333
+ yield event;
334
+ }
335
+ }
336
+ finally {
337
+ // Surface handler/POST failures and avoid dangling work after the stream.
338
+ await Promise.allSettled(inflight);
339
+ }
340
+ }
341
+ async fulfill(handler, callId, input) {
342
+ try {
343
+ const output = await handler(input);
344
+ await this.client.submitToolResult(this.info.id, callId, output, false);
345
+ }
346
+ catch (err) {
347
+ await this.client.submitToolResult(this.info.id, callId, { error: String(err) }, true);
348
+ }
349
+ }
350
+ // send() without the client-tool auto-dispatch: yields the turn's raw
351
+ // events and leaves tool_call handling entirely to the caller. This is the
352
+ // adapter-facing surface — a host that owns its own dispatcher (heartbeats,
353
+ // per-turn credentials) must not have the SDK racing it.
354
+ async *sendRaw(text) {
355
+ yield* await this.client.streamMessage(this.info.id, text);
356
+ }
357
+ // Follow the session's full event stream: replays history after `after`
358
+ // (default: from the beginning), then keeps yielding live events. Survives
359
+ // page refreshes and server restarts — reconnect with the last seq you saw.
360
+ async *stream(opts) {
361
+ yield* await this.client.streamEvents(this.info.id, opts?.after ?? -1);
362
+ }
363
+ // Reconnect to the CURRENT turn only: replay everything after `after`, then
364
+ // (idle → the stream closes immediately; turn in flight → live events until
365
+ // its turn_end). Never re-dispatches client tools — results already posted
366
+ // stay posted; unresolved calls keep their park on the server.
367
+ async *resume(opts) {
368
+ yield* await this.client.streamEvents(this.info.id, opts?.after ?? -1, { mode: 'turn' });
369
+ }
370
+ mountFile(fileId, mountPath) {
371
+ return this.client.mountFile(this.info.id, { fileId, mountPath });
372
+ }
373
+ // Upload + mount in one step — "attach a file to the chat".
374
+ async attachFile(input) {
375
+ const file = await this.client.files.upload(input);
376
+ const { mount_path } = await this.mountFile(file.id, input.mountPath);
377
+ return { file, mount_path };
378
+ }
379
+ destroy() {
380
+ return this.client.sessions.destroy(this.info.id);
381
+ }
382
+ }
383
+ // Minimal SSE parser: yields the JSON payload of each `data:` frame.
384
+ async function* parseSse(body) {
385
+ const decoder = new TextDecoder();
386
+ let buffer = '';
387
+ for await (const chunk of body) {
388
+ buffer += decoder.decode(chunk, { stream: true });
389
+ let sep;
390
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
391
+ const frame = buffer.slice(0, sep);
392
+ buffer = buffer.slice(sep + 2);
393
+ const data = frame
394
+ .split('\n')
395
+ .filter((line) => line.startsWith('data: '))
396
+ .map((line) => line.slice(6))
397
+ .join('');
398
+ if (data)
399
+ yield JSON.parse(data);
400
+ }
401
+ }
402
+ }
@@ -0,0 +1,247 @@
1
+ export type SessionStatus = 'provisioning' | 'running' | 'stopped' | 'failed';
2
+ /** Turn lifecycle as clients see it — distinct from SessionStatus, which is
3
+ * sandbox lifecycle. */
4
+ export type TurnState = 'idle' | 'running' | 'awaiting_tool';
5
+ export type ToolSource = 'builtin' | 'client' | 'mcp';
6
+ export type AgentErrorCode = 'mcp_authentication_failed_error' | 'mcp_connection_failed_error' | 'model_request_failed_error' | 'model_overloaded_error' | 'model_rate_limited_error' | 'billing_error' | 'sandbox_failed_error' | 'unknown_error';
7
+ export type SessionEvent = {
8
+ seq: number;
9
+ ts: string;
10
+ } & ({
11
+ type: 'status';
12
+ status: SessionStatus;
13
+ } | {
14
+ type: 'user_message';
15
+ text: string;
16
+ attachments?: Array<{
17
+ file_id: string;
18
+ name: string;
19
+ mime: string;
20
+ }>;
21
+ } | {
22
+ type: 'turn_start';
23
+ turn: number;
24
+ } | {
25
+ type: 'text_start';
26
+ id: string;
27
+ } | {
28
+ type: 'text_delta';
29
+ id: string;
30
+ text: string;
31
+ } | {
32
+ type: 'text_end';
33
+ id: string;
34
+ text: string;
35
+ } | {
36
+ type: 'tool_call';
37
+ tool: string;
38
+ input: unknown;
39
+ id: string;
40
+ source: ToolSource;
41
+ server?: string;
42
+ client_executed?: boolean;
43
+ } | {
44
+ type: 'tool_result';
45
+ tool: string;
46
+ output: unknown;
47
+ isError?: boolean;
48
+ id: string;
49
+ source: ToolSource;
50
+ server?: string;
51
+ } | {
52
+ type: 'output_file';
53
+ fileId: string;
54
+ name: string;
55
+ sandboxPath: string;
56
+ sizeBytes: number;
57
+ } | {
58
+ type: 'connect_required';
59
+ server: string;
60
+ } | {
61
+ type: 'error';
62
+ code: AgentErrorCode;
63
+ message: string;
64
+ server?: string;
65
+ retry_status: 'terminal' | 'exhausted';
66
+ } | {
67
+ type: 'turn_end';
68
+ reason: 'completed' | 'interrupted' | 'error';
69
+ usage?: {
70
+ inputTokens?: number;
71
+ outputTokens?: number;
72
+ cacheReadTokens?: number;
73
+ cacheWriteTokens?: number;
74
+ };
75
+ });
76
+ export type BuiltInTool = 'bash' | 'read_file' | 'write_file';
77
+ export interface McpServer {
78
+ name: string;
79
+ url: string;
80
+ transport: 'http' | 'sse';
81
+ auth: 'none' | 'required';
82
+ }
83
+ export interface ClientTool<I = any> {
84
+ name: string;
85
+ description: string;
86
+ inputSchema: Record<string, unknown>;
87
+ execute: (input: I) => unknown | Promise<unknown>;
88
+ timeoutSec?: number;
89
+ }
90
+ export interface SkillPin {
91
+ skill_id: string;
92
+ version: number;
93
+ }
94
+ export interface AgentEnvironment {
95
+ snapshot?: string;
96
+ packages?: {
97
+ pip?: string[];
98
+ npm?: string[];
99
+ apt?: string[];
100
+ };
101
+ }
102
+ export interface Agent {
103
+ id: string;
104
+ status: 'active' | 'archived';
105
+ version: number;
106
+ name: string;
107
+ description: string | null;
108
+ model: string;
109
+ system: string;
110
+ tools: BuiltInTool[] | null;
111
+ metadata: Record<string, unknown> | null;
112
+ mcp_servers: McpServer[] | null;
113
+ skills: SkillPin[] | null;
114
+ environment: AgentEnvironment | null;
115
+ created_at: string;
116
+ updated_at: string;
117
+ }
118
+ export interface Skill {
119
+ id: string;
120
+ slug: string;
121
+ display_title: string;
122
+ latest_version: number;
123
+ created_at: string;
124
+ }
125
+ export interface SkillVersion {
126
+ version: number;
127
+ name: string | null;
128
+ description: string | null;
129
+ size_bytes: number;
130
+ created_at: string;
131
+ }
132
+ export interface ApiKeyInfo {
133
+ id: string;
134
+ key_prefix: string;
135
+ name: string | null;
136
+ created_at: string;
137
+ revoked_at: string | null;
138
+ }
139
+ export type McpCredentialInput = {
140
+ type: 'bearer';
141
+ token: string;
142
+ } | {
143
+ type: 'oauth';
144
+ access_token: string;
145
+ refresh_token: string;
146
+ token_url: string;
147
+ client_id: string;
148
+ client_secret?: string;
149
+ scope?: string;
150
+ expires_at?: string;
151
+ };
152
+ export interface McpCredentialStatus {
153
+ server: string;
154
+ end_user_id: string | null;
155
+ auth_type: 'bearer' | 'oauth';
156
+ expires_at: string | null;
157
+ updated_at: string;
158
+ }
159
+ export interface AgentVersion {
160
+ version: number;
161
+ name: string;
162
+ description: string | null;
163
+ model: string;
164
+ system: string;
165
+ tools: BuiltInTool[] | null;
166
+ metadata: Record<string, unknown> | null;
167
+ created_at: string;
168
+ }
169
+ export interface CreateAgentInput {
170
+ skills?: SkillPin[];
171
+ environment?: AgentEnvironment;
172
+ name: string;
173
+ system: string;
174
+ model?: string;
175
+ description?: string | null;
176
+ tools?: BuiltInTool[] | null;
177
+ metadata?: Record<string, unknown> | null;
178
+ mcp_servers?: McpServer[] | null;
179
+ }
180
+ export type UpdateAgentInput = Partial<CreateAgentInput>;
181
+ export interface SessionInfo {
182
+ id: string;
183
+ status: SessionStatus;
184
+ turn_state?: TurnState;
185
+ running?: boolean;
186
+ agentId?: string;
187
+ agentVersion?: number;
188
+ endUserId?: string | null;
189
+ model: string;
190
+ sandboxId: string | null;
191
+ title?: string | null;
192
+ metadata?: Record<string, string> | null;
193
+ archived?: boolean;
194
+ last_seq?: number;
195
+ events?: SessionEvent[];
196
+ }
197
+ export interface FileMeta {
198
+ id: string;
199
+ name: string;
200
+ mime: string;
201
+ size_bytes: number;
202
+ source: 'upload' | 'output';
203
+ session_id: string | null;
204
+ sandbox_path: string | null;
205
+ created_at: string;
206
+ updated_at: string;
207
+ }
208
+ export interface UploadFileInput {
209
+ name: string;
210
+ content: string | Uint8Array;
211
+ mime?: string;
212
+ }
213
+ export interface UsageBreakdownRow {
214
+ input_tokens: number;
215
+ output_tokens: number;
216
+ cost_usd: number | null;
217
+ turns: number;
218
+ }
219
+ export interface UsageSummary {
220
+ totals: {
221
+ input_tokens: number;
222
+ output_tokens: number;
223
+ cost_usd: number;
224
+ turns: number;
225
+ };
226
+ by_model: (UsageBreakdownRow & {
227
+ model: string;
228
+ })[];
229
+ by_agent: (UsageBreakdownRow & {
230
+ agent_id: string | null;
231
+ })[];
232
+ by_user: (UsageBreakdownRow & {
233
+ end_user_id: string | null;
234
+ })[];
235
+ }
236
+ export interface SessionListItem {
237
+ id: string;
238
+ status: string;
239
+ turn_state: TurnState;
240
+ model: string;
241
+ agent_id: string | null;
242
+ end_user_id: string | null;
243
+ title: string | null;
244
+ metadata: Record<string, string> | null;
245
+ created_at: string;
246
+ updated_at: string;
247
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Wire types for the TARS public API. The SDK is a standalone published
2
+ // package — these are intentionally duplicated from the backend, because the
3
+ // HTTP API (not shared code) is the contract.
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@wice24/tars-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Client SDK for TARS \u2014 the self-hosted managed-agents platform (sessions, streaming turns, client tools, MCP credentials, skills, files, usage)",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:coverage": "vitest run --coverage"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^6.0.3",
24
+ "@types/node": "^26.1.0",
25
+ "@vitest/coverage-v8": "^3.2.4",
26
+ "vitest": "^3.2.4"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/meridian-capital/tars.git",
32
+ "directory": "sdk"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "keywords": [
38
+ "agents",
39
+ "ai",
40
+ "mcp",
41
+ "anthropic",
42
+ "sdk"
43
+ ]
44
+ }