@runtypelabs/sdk 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/endpoints.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AgentsEndpoint = exports.ClientTokensEndpoint = exports.EvalEndpoint = exports.ToolsEndpoint = exports.ContextTemplatesEndpoint = exports.FlowStepsEndpoint = exports.AnalyticsEndpoint = exports.UsersEndpoint = exports.ChatEndpoint = exports.DispatchEndpoint = exports.ModelConfigsEndpoint = exports.ApiKeysEndpoint = exports.RecordsEndpoint = exports.PromptsEndpoint = exports.FlowsEndpoint = void 0;
7
+ const generated_tool_gate_1 = require("./generated-tool-gate");
7
8
  /**
8
9
  * Flows endpoint handlers
9
10
  */
@@ -419,6 +420,34 @@ class DispatchEndpoint {
419
420
  }
420
421
  return this.client.post('/dispatch/resume', data);
421
422
  }
423
+ /**
424
+ * Evaluate a model-proposed runtime tool against a configurable allowlist policy.
425
+ * Useful for local `propose_runtime_tool` handlers before redispatch.
426
+ */
427
+ gateGeneratedRuntimeToolProposal(proposal, options) {
428
+ return (0, generated_tool_gate_1.evaluateGeneratedRuntimeToolProposal)(proposal, options);
429
+ }
430
+ /**
431
+ * Build standardized local-tool output for a generated tool proposal.
432
+ * Returns `{ approved, reason, violations, tool? }`.
433
+ */
434
+ buildGeneratedRuntimeToolGateOutput(proposal, options) {
435
+ return (0, generated_tool_gate_1.buildGeneratedRuntimeToolGateOutput)(proposal, options);
436
+ }
437
+ /**
438
+ * Attach approved runtime tools to a prompt step in a redispatch request.
439
+ * Returns a new request object and does not mutate the original.
440
+ */
441
+ attachApprovedRuntimeTools(request, runtimeTools, options) {
442
+ return (0, generated_tool_gate_1.attachRuntimeToolsToDispatchRequest)(request, runtimeTools, options);
443
+ }
444
+ /**
445
+ * Validate a generated runtime tool proposal and attach it to the redispatch
446
+ * request if approved, in one call.
447
+ */
448
+ applyGeneratedRuntimeToolProposal(request, proposal, options) {
449
+ return (0, generated_tool_gate_1.applyGeneratedRuntimeToolProposalToDispatchRequest)(request, proposal, options);
450
+ }
422
451
  }
423
452
  exports.DispatchEndpoint = DispatchEndpoint;
424
453
  /**
@@ -885,6 +914,9 @@ function dispatchAgentEvent(event, callbacks) {
885
914
  case 'agent_error':
886
915
  callbacks.onError?.(typedData);
887
916
  break;
917
+ case 'agent_paused':
918
+ callbacks.onAgentPaused?.(typedData);
919
+ break;
888
920
  case 'agent_ping':
889
921
  callbacks.onPing?.(typedData);
890
922
  break;
@@ -927,6 +959,57 @@ async function processAgentStream(body, callbacks) {
927
959
  reader.releaseLock();
928
960
  }
929
961
  }
962
+ const GENERATED_RUNTIME_TOOL_PROPOSAL_SCHEMA = {
963
+ type: 'object',
964
+ properties: {
965
+ name: {
966
+ type: 'string',
967
+ description: 'Tool name. Use letters/numbers/underscore only.',
968
+ },
969
+ description: {
970
+ type: 'string',
971
+ description: 'Clear description of what the generated tool does.',
972
+ },
973
+ toolType: {
974
+ type: 'string',
975
+ enum: ['custom'],
976
+ description: 'Must be "custom" for generated code execution tools.',
977
+ },
978
+ parametersSchema: {
979
+ type: 'object',
980
+ description: 'JSON schema for tool call arguments.',
981
+ },
982
+ config: {
983
+ type: 'object',
984
+ description: 'Runtime tool config including code, sandboxProvider, language, and timeout.',
985
+ },
986
+ reason: {
987
+ type: 'string',
988
+ description: 'Why this tool is needed.',
989
+ },
990
+ },
991
+ required: ['name', 'description', 'toolType', 'parametersSchema', 'config'],
992
+ };
993
+ function appendRuntimeToolsToAgentRequest(request, runtimeTools) {
994
+ const existing = request.tools?.runtimeTools || [];
995
+ const existingNames = new Set(existing.map((tool) => tool.name));
996
+ const converted = runtimeTools
997
+ .filter((tool) => !existingNames.has(tool.name))
998
+ .map((tool) => ({
999
+ name: tool.name,
1000
+ description: tool.description,
1001
+ toolType: tool.toolType,
1002
+ parametersSchema: tool.parametersSchema,
1003
+ ...(tool.config ? { config: tool.config } : {}),
1004
+ }));
1005
+ return {
1006
+ ...request,
1007
+ tools: {
1008
+ ...request.tools,
1009
+ runtimeTools: [...existing, ...converted],
1010
+ },
1011
+ };
1012
+ }
930
1013
  /**
931
1014
  * Agents endpoint handlers
932
1015
  */
@@ -964,6 +1047,54 @@ class AgentsEndpoint {
964
1047
  async delete(id) {
965
1048
  return this.client.delete(`/agents/${id}`);
966
1049
  }
1050
+ /**
1051
+ * Evaluate a model-proposed runtime tool against a configurable allowlist policy.
1052
+ * Useful for local `propose_runtime_tool` handlers before follow-up execution.
1053
+ */
1054
+ gateGeneratedRuntimeToolProposal(proposal, options) {
1055
+ return (0, generated_tool_gate_1.evaluateGeneratedRuntimeToolProposal)(proposal, options);
1056
+ }
1057
+ /**
1058
+ * Build standardized local-tool output for a generated tool proposal.
1059
+ * Returns `{ approved, reason, violations, tool? }`.
1060
+ */
1061
+ buildGeneratedRuntimeToolGateOutput(proposal, options) {
1062
+ return (0, generated_tool_gate_1.buildGeneratedRuntimeToolGateOutput)(proposal, options);
1063
+ }
1064
+ /**
1065
+ * Create a local tool definition that validates model-proposed runtime tools.
1066
+ * Plug this into `executeWithLocalTools()` under a name like `propose_runtime_tool`.
1067
+ */
1068
+ createGeneratedRuntimeToolGateLocalTool(options) {
1069
+ const { description, ...gateOptions } = options || {};
1070
+ return {
1071
+ description: description ||
1072
+ 'Validate a generated runtime custom tool and return { approved, reason, violations, tool? }',
1073
+ parametersSchema: GENERATED_RUNTIME_TOOL_PROPOSAL_SCHEMA,
1074
+ execute: async (args) => (0, generated_tool_gate_1.buildGeneratedRuntimeToolGateOutput)(args, gateOptions),
1075
+ };
1076
+ }
1077
+ /**
1078
+ * Attach approved runtime tools to an agent execute request.
1079
+ * Returns a new request object and does not mutate the original.
1080
+ */
1081
+ attachApprovedRuntimeTools(request, runtimeTools) {
1082
+ return appendRuntimeToolsToAgentRequest(request, runtimeTools);
1083
+ }
1084
+ /**
1085
+ * Validate a generated runtime tool proposal and append it to an agent execute
1086
+ * request if approved, in one call.
1087
+ */
1088
+ applyGeneratedRuntimeToolProposal(request, proposal, options) {
1089
+ const decision = (0, generated_tool_gate_1.evaluateGeneratedRuntimeToolProposal)(proposal, options);
1090
+ if (!decision.approved || !decision.tool) {
1091
+ return { decision, request };
1092
+ }
1093
+ return {
1094
+ decision,
1095
+ request: appendRuntimeToolsToAgentRequest(request, [decision.tool]),
1096
+ };
1097
+ }
967
1098
  /**
968
1099
  * Execute an agent (non-streaming)
969
1100
  */
@@ -1048,6 +1179,386 @@ class AgentsEndpoint {
1048
1179
  });
1049
1180
  return completeEvent;
1050
1181
  }
1182
+ /**
1183
+ * Execute an agent with local tool support (pause/resume loop)
1184
+ *
1185
+ * When the agent hits a tool with `toolType: 'local'`, the server emits
1186
+ * `agent_paused`. This method automatically executes the local tool and
1187
+ * resumes execution, repeating until the agent completes.
1188
+ *
1189
+ * @example
1190
+ * ```typescript
1191
+ * const result = await client.agents.executeWithLocalTools('agt_123', {
1192
+ * messages: [{ role: 'user', content: 'Create a file called hello.txt' }],
1193
+ * }, {
1194
+ * write_file: async ({ path, content }) => {
1195
+ * fs.writeFileSync(path, content)
1196
+ * return 'ok'
1197
+ * },
1198
+ * })
1199
+ * ```
1200
+ */
1201
+ async executeWithLocalTools(id, data, localTools, callbacks) {
1202
+ // Build runtime tool definitions from local tool schemas and inject into request
1203
+ const runtimeTools = Object.entries(localTools).map(([name, def]) => ({
1204
+ name,
1205
+ description: def.description,
1206
+ toolType: 'local',
1207
+ parametersSchema: def.parametersSchema,
1208
+ }));
1209
+ const requestData = {
1210
+ ...data,
1211
+ tools: {
1212
+ ...data.tools,
1213
+ runtimeTools: [
1214
+ ...(data.tools?.runtimeTools || []),
1215
+ ...runtimeTools,
1216
+ ],
1217
+ },
1218
+ };
1219
+ const response = await this.executeStream(id, requestData);
1220
+ if (!response.ok) {
1221
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
1222
+ throw new Error(error.error || `HTTP ${response.status}`);
1223
+ }
1224
+ let currentBody = response.body;
1225
+ while (true) {
1226
+ let pausedEvent = null;
1227
+ let completeEvent = null;
1228
+ await processAgentStream(currentBody, {
1229
+ ...callbacks,
1230
+ onAgentPaused: (event) => {
1231
+ pausedEvent = event;
1232
+ callbacks?.onAgentPaused?.(event);
1233
+ },
1234
+ onAgentComplete: (event) => {
1235
+ completeEvent = event;
1236
+ callbacks?.onAgentComplete?.(event);
1237
+ },
1238
+ });
1239
+ if (completeEvent)
1240
+ return completeEvent;
1241
+ if (pausedEvent) {
1242
+ const { toolName, parameters, executionId } = pausedEvent;
1243
+ const toolDef = localTools[toolName];
1244
+ if (!toolDef) {
1245
+ throw new Error(`Local tool "${toolName}" required but not provided`);
1246
+ }
1247
+ // Recursively unwrap stringified parameters — the server pipeline may
1248
+ // double-serialize: object → JSON string → JSON string
1249
+ let parsedParams = {};
1250
+ let current = parameters;
1251
+ for (let i = 0; i < 3; i++) {
1252
+ if (typeof current === 'string') {
1253
+ try {
1254
+ current = JSON.parse(current);
1255
+ }
1256
+ catch {
1257
+ console.warn(`[local-tools] Failed to parse parameters (attempt ${i + 1}):`, typeof current, String(current).slice(0, 200));
1258
+ break;
1259
+ }
1260
+ }
1261
+ else {
1262
+ break;
1263
+ }
1264
+ }
1265
+ if (current && typeof current === 'object' && !Array.isArray(current)) {
1266
+ parsedParams = current;
1267
+ }
1268
+ else {
1269
+ console.warn('[local-tools] Parameters could not be resolved to an object:', typeof current, String(current).slice(0, 200));
1270
+ }
1271
+ let toolResult;
1272
+ try {
1273
+ toolResult = await toolDef.execute(parsedParams);
1274
+ }
1275
+ catch (err) {
1276
+ // Return the error as a tool result so the agent can recover
1277
+ toolResult = `Error: ${err instanceof Error ? err.message : String(err)}`;
1278
+ }
1279
+ // Resume via agent resume endpoint
1280
+ const resumeResponse = await this.client.requestStream(`/agents/${id}/resume`, {
1281
+ method: 'POST',
1282
+ body: JSON.stringify({
1283
+ executionId,
1284
+ toolOutputs: { [toolName]: toolResult },
1285
+ streamResponse: true,
1286
+ debugMode: data.debugMode,
1287
+ }),
1288
+ });
1289
+ if (!resumeResponse.ok) {
1290
+ const error = await resumeResponse.json().catch(() => ({ error: 'Unknown error' }));
1291
+ throw new Error(error.error || `HTTP ${resumeResponse.status}`);
1292
+ }
1293
+ currentBody = resumeResponse.body;
1294
+ continue;
1295
+ }
1296
+ // Stream ended without complete or paused
1297
+ return null;
1298
+ }
1299
+ }
1300
+ // ─── Long-Task Agent Execution ───────────────────────────────────────
1301
+ /**
1302
+ * Run a long-task agent across multiple sessions with automatic state management.
1303
+ *
1304
+ * Each session is a single agent execution. The SDK drives the loop client-side,
1305
+ * calling the agent's execute endpoint repeatedly and accumulating context.
1306
+ * Progress is optionally synced to a Runtype record for dashboard visibility.
1307
+ *
1308
+ * @example
1309
+ * ```typescript
1310
+ * const result = await client.agents.runTask('agt_123', {
1311
+ * message: 'Build a REST API with CRUD endpoints',
1312
+ * maxSessions: 20,
1313
+ * maxCost: 5.00,
1314
+ * trackProgress: true,
1315
+ * onSession: (state) => {
1316
+ * console.log(`Session ${state.sessionCount}: ${state.lastStopReason} ($${state.totalCost.toFixed(4)})`)
1317
+ * },
1318
+ * })
1319
+ *
1320
+ * console.log(`Finished: ${result.status} after ${result.sessionCount} sessions`)
1321
+ * ```
1322
+ */
1323
+ async runTask(id, options) {
1324
+ const maxSessions = options.maxSessions ?? 50;
1325
+ const maxCost = options.maxCost;
1326
+ const useStream = options.stream ?? true;
1327
+ // Resolve agent metadata
1328
+ const agent = await this.get(id);
1329
+ const taskName = typeof options.trackProgress === 'string'
1330
+ ? options.trackProgress
1331
+ : options.trackProgress
1332
+ ? `${agent.name} task`
1333
+ : '';
1334
+ // Initialize state
1335
+ const state = {
1336
+ agentId: id,
1337
+ agentName: agent.name,
1338
+ taskName: taskName || `${agent.name} task`,
1339
+ status: 'running',
1340
+ sessionCount: 0,
1341
+ totalCost: 0,
1342
+ lastOutput: '',
1343
+ lastStopReason: 'complete',
1344
+ sessions: [],
1345
+ startedAt: new Date().toISOString(),
1346
+ updatedAt: new Date().toISOString(),
1347
+ };
1348
+ // Track the record ID if we're syncing
1349
+ let recordId;
1350
+ // Extract local tool names for prompt injection
1351
+ const localToolNames = options.localTools ? Object.keys(options.localTools) : undefined;
1352
+ // Session loop
1353
+ for (let session = 0; session < maxSessions; session++) {
1354
+ // Build messages for this session
1355
+ const messages = this.buildSessionMessages(options.message, state, session, maxSessions, localToolNames);
1356
+ // Execute one session
1357
+ let sessionResult;
1358
+ const sessionData = { messages, debugMode: options.debugMode, model: options.model };
1359
+ if (useStream && options.localTools) {
1360
+ // Local tools require the pause/resume streaming loop
1361
+ const completeEvent = await this.executeWithLocalTools(id, sessionData, options.localTools, options.streamCallbacks);
1362
+ if (!completeEvent) {
1363
+ throw new Error('Agent stream ended without a complete event');
1364
+ }
1365
+ sessionResult = {
1366
+ success: completeEvent.success,
1367
+ result: completeEvent.finalOutput || '',
1368
+ iterations: completeEvent.iterations,
1369
+ totalCost: completeEvent.totalCost || 0,
1370
+ stopReason: completeEvent.stopReason,
1371
+ error: completeEvent.error,
1372
+ };
1373
+ }
1374
+ else if (useStream && options.streamCallbacks) {
1375
+ const completeEvent = await this.executeWithCallbacks(id, sessionData, options.streamCallbacks);
1376
+ if (!completeEvent) {
1377
+ throw new Error('Agent stream ended without a complete event');
1378
+ }
1379
+ sessionResult = {
1380
+ success: completeEvent.success,
1381
+ result: completeEvent.finalOutput || '',
1382
+ iterations: completeEvent.iterations,
1383
+ totalCost: completeEvent.totalCost || 0,
1384
+ stopReason: completeEvent.stopReason,
1385
+ error: completeEvent.error,
1386
+ };
1387
+ }
1388
+ else {
1389
+ sessionResult = await this.execute(id, sessionData);
1390
+ }
1391
+ // Update state
1392
+ const sessionCost = sessionResult.totalCost;
1393
+ state.sessionCount = session + 1;
1394
+ state.totalCost += sessionCost;
1395
+ state.lastOutput = sessionResult.result;
1396
+ state.lastStopReason = sessionResult.stopReason;
1397
+ state.updatedAt = new Date().toISOString();
1398
+ state.sessions.push({
1399
+ index: session + 1,
1400
+ cost: sessionCost,
1401
+ iterations: sessionResult.iterations,
1402
+ stopReason: sessionResult.stopReason,
1403
+ outputPreview: sessionResult.result.slice(0, 300),
1404
+ completedAt: new Date().toISOString(),
1405
+ });
1406
+ // Keep session log trimmed to last 50 entries
1407
+ if (state.sessions.length > 50) {
1408
+ state.sessions = state.sessions.slice(-50);
1409
+ }
1410
+ // Check terminal conditions
1411
+ if (sessionResult.stopReason === 'complete') {
1412
+ state.status = 'complete';
1413
+ }
1414
+ else if (sessionResult.stopReason === 'error') {
1415
+ state.status = 'complete';
1416
+ }
1417
+ else if (sessionResult.stopReason === 'max_cost') {
1418
+ state.status = 'budget_exceeded';
1419
+ }
1420
+ else if (this.detectTaskCompletion(sessionResult.result)) {
1421
+ // Client-side stop-phrase detection for non-loop agents returning 'end_turn'
1422
+ state.status = 'complete';
1423
+ }
1424
+ else if (maxCost && state.totalCost >= maxCost) {
1425
+ state.status = 'budget_exceeded';
1426
+ }
1427
+ else if (session + 1 >= maxSessions) {
1428
+ state.status = 'max_sessions';
1429
+ }
1430
+ // Sync to record if enabled
1431
+ if (options.trackProgress) {
1432
+ recordId = await this.syncProgressRecord(state, recordId);
1433
+ }
1434
+ // Notify caller
1435
+ if (options.onSession) {
1436
+ const shouldStop = await options.onSession(state);
1437
+ if (shouldStop === false) {
1438
+ state.status = 'paused';
1439
+ }
1440
+ }
1441
+ // Stop if terminal
1442
+ if (state.status !== 'running') {
1443
+ break;
1444
+ }
1445
+ }
1446
+ return {
1447
+ status: state.status,
1448
+ sessionCount: state.sessionCount,
1449
+ totalCost: state.totalCost,
1450
+ lastOutput: state.lastOutput,
1451
+ sessions: state.sessions,
1452
+ recordId,
1453
+ };
1454
+ }
1455
+ /**
1456
+ * Client-side fallback for detecting task completion in agent output.
1457
+ * Mirrors the API's detectAutoComplete() for non-loop agents that return 'end_turn'.
1458
+ */
1459
+ detectTaskCompletion(output) {
1460
+ const upper = output.toUpperCase();
1461
+ return AgentsEndpoint.STOP_PHRASES.some((phrase) => upper.includes(phrase.toUpperCase()));
1462
+ }
1463
+ /**
1464
+ * Build messages for a session, injecting progress context for continuation sessions.
1465
+ */
1466
+ buildSessionMessages(originalMessage, state, sessionIndex, maxSessions, localToolNames) {
1467
+ // Build local tools guidance block when tools are available
1468
+ const toolsBlock = localToolNames?.length
1469
+ ? [
1470
+ '',
1471
+ '--- Local Tools ---',
1472
+ `You have access to local filesystem tools (${localToolNames.join(', ')}) that execute directly on the user's machine.`,
1473
+ 'Use these tools to create working, runnable files — not just code in your response.',
1474
+ 'Prefer creating self-contained HTML files that the user can open in a web browser.',
1475
+ 'For example, write a single .html file with inline CSS and JavaScript that demonstrates the result.',
1476
+ 'Always use write_file to save your output so the user can run it immediately.',
1477
+ ].join('\n')
1478
+ : '';
1479
+ // First session: user message + completion signal instruction
1480
+ if (sessionIndex === 0) {
1481
+ const content = [
1482
+ originalMessage,
1483
+ toolsBlock,
1484
+ '',
1485
+ `This is a multi-session task (session 1/${maxSessions}). When you have fully completed the task, end your response with TASK_COMPLETE on its own line.`,
1486
+ ].join('\n');
1487
+ return [{ role: 'user', content }];
1488
+ }
1489
+ // Continuation sessions: inject progress context
1490
+ const recentSessions = state.sessions.slice(-5);
1491
+ const progressSummary = recentSessions
1492
+ .map((s) => ` Session ${s.index}: ${s.stopReason} ($${s.cost.toFixed(4)}) — ${s.outputPreview.slice(0, 100)}`)
1493
+ .join('\n');
1494
+ const content = [
1495
+ originalMessage,
1496
+ toolsBlock,
1497
+ '',
1498
+ `--- Progress (session ${sessionIndex + 1}/${maxSessions}, $${state.totalCost.toFixed(4)} spent) ---`,
1499
+ `Previous sessions:`,
1500
+ progressSummary,
1501
+ '',
1502
+ `Last output (do NOT repeat this — build on it):`,
1503
+ state.lastOutput.slice(0, 1000),
1504
+ '',
1505
+ 'Continue where you left off. Do not redo previous work. If the task is already complete, respond with TASK_COMPLETE.',
1506
+ ].join('\n');
1507
+ return [{ role: 'user', content }];
1508
+ }
1509
+ /**
1510
+ * Upsert a record to sync long-task progress to the dashboard.
1511
+ * Creates the record on first call, updates it on subsequent calls.
1512
+ */
1513
+ async syncProgressRecord(state, existingRecordId) {
1514
+ const metadata = {
1515
+ agentId: state.agentId,
1516
+ agentName: state.agentName,
1517
+ status: state.status,
1518
+ sessionCount: state.sessionCount,
1519
+ totalCost: state.totalCost,
1520
+ lastStopReason: state.lastStopReason,
1521
+ lastOutputPreview: state.lastOutput.slice(0, 500),
1522
+ sessions: state.sessions.slice(-10), // Keep last 10 in the record
1523
+ startedAt: state.startedAt,
1524
+ updatedAt: state.updatedAt,
1525
+ };
1526
+ try {
1527
+ if (existingRecordId) {
1528
+ // Update existing record
1529
+ const record = await this.client.put(`/records/${existingRecordId}`, { metadata });
1530
+ return record.id;
1531
+ }
1532
+ else {
1533
+ // Try to find existing record by type + name first
1534
+ const existing = await this.client.get('/records', { type: 'agent-task', name: state.taskName, limit: 1 });
1535
+ if (existing.data.length > 0) {
1536
+ const record = await this.client.put(`/records/${existing.data[0].id}`, { metadata });
1537
+ return record.id;
1538
+ }
1539
+ // Create new record
1540
+ const record = await this.client.post('/records', {
1541
+ type: 'agent-task',
1542
+ name: state.taskName,
1543
+ metadata,
1544
+ });
1545
+ return record.id;
1546
+ }
1547
+ }
1548
+ catch {
1549
+ // Record sync is best-effort — don't fail the task
1550
+ return existingRecordId || '';
1551
+ }
1552
+ }
1051
1553
  }
1052
1554
  exports.AgentsEndpoint = AgentsEndpoint;
1555
+ /** Stop phrases that indicate the agent considers its task complete. */
1556
+ AgentsEndpoint.STOP_PHRASES = [
1557
+ 'DONE:',
1558
+ 'TASK_COMPLETE',
1559
+ 'FINISHED',
1560
+ '[COMPLETE]',
1561
+ 'STATUS: RESOLVED',
1562
+ 'STATUS: COMPLETE',
1563
+ ];
1053
1564
  //# sourceMappingURL=endpoints.js.map