@arcadeai/hypotenuse 0.20.0 → 0.21.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/hypotenuse.js +396 -199
- package/package.json +1 -1
- package/skills/hypotenuse.md +3 -1
package/dist/hypotenuse.js
CHANGED
|
@@ -343,7 +343,7 @@ import { Command } from "commander";
|
|
|
343
343
|
// package.json
|
|
344
344
|
var package_default = {
|
|
345
345
|
name: "@arcadeai/hypotenuse",
|
|
346
|
-
version: "0.
|
|
346
|
+
version: "0.21.0",
|
|
347
347
|
description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
|
|
348
348
|
type: "module",
|
|
349
349
|
license: "Apache-2.0",
|
|
@@ -1097,172 +1097,55 @@ var RunnerLimits = z2.strictObject({
|
|
|
1097
1097
|
agents: z2.record(identifier, cap).optional()
|
|
1098
1098
|
});
|
|
1099
1099
|
// ../protocol/src/complete.ts
|
|
1100
|
-
import { z as z3 } from "zod";
|
|
1101
|
-
var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
1102
|
-
var CompleteRequest = z3.strictObject({
|
|
1103
|
-
workItemId: z3.number().int().positive(),
|
|
1104
|
-
outcome: z3.enum(COMPLETE_OUTCOMES),
|
|
1105
|
-
error: z3.string().max(4096).nullable().optional(),
|
|
1106
|
-
acpSessionId: z3.string().max(255).nullable().optional(),
|
|
1107
|
-
branch: z3.string().max(255).nullable().optional(),
|
|
1108
|
-
answersConsumed: z3.array(z3.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt")
|
|
1109
|
-
});
|
|
1110
|
-
var CompleteResponse = z3.looseObject({
|
|
1111
|
-
alreadyTerminal: z3.boolean()
|
|
1112
|
-
});
|
|
1113
|
-
// ../protocol/src/connect.ts
|
|
1114
|
-
import { z as z5 } from "zod";
|
|
1115
|
-
|
|
1116
|
-
// ../protocol/src/heartbeat.ts
|
|
1117
1100
|
import { z as z4 } from "zod";
|
|
1118
|
-
var ACTIVE_STATES = ["starting", "running", "cancelling"];
|
|
1119
|
-
var ActiveItem = z4.strictObject({
|
|
1120
|
-
workItemId: z4.number().int().positive(),
|
|
1121
|
-
agentSessionId: z4.number().int().positive(),
|
|
1122
|
-
role: RunnerRole,
|
|
1123
|
-
agentId: z4.string().min(1).max(64),
|
|
1124
|
-
state: z4.enum(ACTIVE_STATES)
|
|
1125
|
-
});
|
|
1126
|
-
var MachineId = z4.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
|
|
1127
|
-
var HeartbeatRequest = z4.strictObject({
|
|
1128
|
-
protocolVersion: z4.number().int().positive(),
|
|
1129
|
-
daemonVersion: z4.string().trim().min(1).max(64),
|
|
1130
|
-
platform: z4.string().trim().min(1).max(64),
|
|
1131
|
-
hostname: z4.string().trim().min(1).max(256),
|
|
1132
|
-
draining: z4.boolean(),
|
|
1133
|
-
machineId: MachineId,
|
|
1134
|
-
limits: RunnerLimits,
|
|
1135
|
-
active: z4.array(ActiveItem).max(1000),
|
|
1136
|
-
capabilities: RunnerCapabilities,
|
|
1137
|
-
dryRun: z4.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
|
|
1138
|
-
});
|
|
1139
|
-
var DryRunRunner = z4.looseObject({
|
|
1140
|
-
id: z4.number().int().positive(),
|
|
1141
|
-
name: z4.string(),
|
|
1142
|
-
projectId: z4.number().int().positive(),
|
|
1143
|
-
projectSlug: z4.string()
|
|
1144
|
-
});
|
|
1145
|
-
var OFFER_KINDS = [
|
|
1146
|
-
"session_created",
|
|
1147
|
-
"prompt",
|
|
1148
|
-
"dispatch",
|
|
1149
|
-
"review"
|
|
1150
|
-
];
|
|
1151
|
-
var Offer = z4.looseObject({
|
|
1152
|
-
workItemId: z4.number().int().positive(),
|
|
1153
|
-
agentSessionId: z4.number().int().positive(),
|
|
1154
|
-
kind: z4.enum(OFFER_KINDS),
|
|
1155
|
-
role: RunnerRole,
|
|
1156
|
-
offerExpiresAt: z4.number().int()
|
|
1157
|
-
});
|
|
1158
|
-
var CANCEL_REASONS = [
|
|
1159
|
-
"stop",
|
|
1160
|
-
"dashboard",
|
|
1161
|
-
"delegate_removed",
|
|
1162
|
-
"issue_closed",
|
|
1163
|
-
"unassigned",
|
|
1164
|
-
"integration_revoked",
|
|
1165
|
-
"superseded"
|
|
1166
|
-
];
|
|
1167
|
-
var Cancel = z4.looseObject({
|
|
1168
|
-
workItemId: z4.number().int().positive(),
|
|
1169
|
-
reason: z4.union([z4.enum(CANCEL_REASONS), z4.string().min(1).max(64)]),
|
|
1170
|
-
by: z4.string().min(1).max(200).optional(),
|
|
1171
|
-
said: z4.string().min(1).max(2000).optional()
|
|
1172
|
-
});
|
|
1173
|
-
var STOP_QUOTE_MAX = 500;
|
|
1174
|
-
function stopQuote(said) {
|
|
1175
|
-
const text = said?.trim();
|
|
1176
|
-
if (!text || /^stop[.!]*$/i.test(text))
|
|
1177
|
-
return "";
|
|
1178
|
-
const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
|
|
1179
|
-
return `
|
|
1180
|
-
|
|
1181
|
-
> You said: ${cut.split(`
|
|
1182
|
-
`).join(`
|
|
1183
|
-
> `)}`;
|
|
1184
|
-
}
|
|
1185
|
-
var Answer = z4.looseObject({
|
|
1186
|
-
workItemId: z4.number().int().positive(),
|
|
1187
|
-
activityId: z4.number().int().positive(),
|
|
1188
|
-
body: z4.string()
|
|
1189
|
-
});
|
|
1190
|
-
var HeartbeatResponse = z4.looseObject({
|
|
1191
|
-
serverTime: z4.number().int(),
|
|
1192
|
-
heartbeatIntervalMs: z4.number().int().positive(),
|
|
1193
|
-
leaseMs: z4.number().int().positive(),
|
|
1194
|
-
offers: z4.array(Offer),
|
|
1195
|
-
cancels: z4.array(Cancel),
|
|
1196
|
-
unknownActive: z4.array(z4.number().int().positive()),
|
|
1197
|
-
answers: z4.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
|
|
1198
|
-
runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
|
|
1199
|
-
unknownSubscriptions: z4.looseObject({ teams: z4.array(z4.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
|
|
1200
|
-
});
|
|
1201
1101
|
|
|
1202
|
-
// ../protocol/src/connect.ts
|
|
1203
|
-
var ConnectRequest = z5.strictObject({
|
|
1204
|
-
machineId: MachineId
|
|
1205
|
-
});
|
|
1206
|
-
var ConnectResponse = z5.looseObject({
|
|
1207
|
-
runnerId: z5.number().int().positive(),
|
|
1208
|
-
channel: z5.string().regex(/^runner:\d+$/),
|
|
1209
|
-
heartbeatIntervalMs: z5.number().int().positive(),
|
|
1210
|
-
leaseMs: z5.number().int().positive(),
|
|
1211
|
-
serverTime: z5.number().int()
|
|
1212
|
-
});
|
|
1213
|
-
var MACHINE_IN_USE = "MACHINE_IN_USE";
|
|
1214
|
-
var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
|
|
1215
|
-
var RunnerFrame = z5.object({ event: z5.enum(RUNNER_FRAME_EVENTS) });
|
|
1216
|
-
function runnerChannel(runnerId) {
|
|
1217
|
-
return `runner:${runnerId}`;
|
|
1218
|
-
}
|
|
1219
1102
|
// ../protocol/src/events.ts
|
|
1220
|
-
import { z as
|
|
1103
|
+
import { z as z3 } from "zod";
|
|
1221
1104
|
var MAX_EVENTS_PER_BATCH = 100;
|
|
1222
1105
|
var MAX_EVENTS_BATCH_BYTES = 256 * 1024;
|
|
1223
1106
|
var NOT_YOUR_WORK = "NOT_YOUR_WORK";
|
|
1224
1107
|
var SEQ_REGRESSION = "SEQ_REGRESSION";
|
|
1225
|
-
var body =
|
|
1108
|
+
var body = z3.string().trim().min(1).max(64 * 1024);
|
|
1226
1109
|
var envelope = {
|
|
1227
|
-
eventId:
|
|
1228
|
-
seq:
|
|
1229
|
-
at:
|
|
1110
|
+
eventId: z3.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
|
|
1111
|
+
seq: z3.number().int().positive().describe("Per work item, from 1, never going backwards"),
|
|
1112
|
+
at: z3.number().int().describe("When the daemon saw it, epoch ms")
|
|
1230
1113
|
};
|
|
1231
|
-
var SelectMetadata =
|
|
1232
|
-
options:
|
|
1233
|
-
label:
|
|
1234
|
-
value:
|
|
1114
|
+
var SelectMetadata = z3.strictObject({
|
|
1115
|
+
options: z3.array(z3.strictObject({
|
|
1116
|
+
label: z3.string().trim().min(1).max(255).optional(),
|
|
1117
|
+
value: z3.string().trim().min(1).max(1024)
|
|
1235
1118
|
})).min(1).max(25)
|
|
1236
1119
|
});
|
|
1237
|
-
var AuthProvider =
|
|
1238
|
-
var AuthMetadata =
|
|
1239
|
-
url:
|
|
1120
|
+
var AuthProvider = z3.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
|
|
1121
|
+
var AuthMetadata = z3.strictObject({
|
|
1122
|
+
url: z3.url().optional(),
|
|
1240
1123
|
provider: AuthProvider.optional(),
|
|
1241
|
-
userId:
|
|
1242
|
-
providerName:
|
|
1124
|
+
userId: z3.string().max(255).optional(),
|
|
1125
|
+
providerName: z3.string().max(255).optional()
|
|
1243
1126
|
}).refine((m) => m.url !== undefined || m.provider !== undefined, {
|
|
1244
1127
|
message: "an auth elicitation needs a url or a provider"
|
|
1245
1128
|
});
|
|
1246
|
-
var ThoughtEvent =
|
|
1129
|
+
var ThoughtEvent = z3.strictObject({
|
|
1247
1130
|
...envelope,
|
|
1248
|
-
type:
|
|
1131
|
+
type: z3.literal("thought"),
|
|
1249
1132
|
body,
|
|
1250
|
-
ephemeral:
|
|
1133
|
+
ephemeral: z3.boolean().default(false)
|
|
1251
1134
|
});
|
|
1252
|
-
var ActionEvent =
|
|
1135
|
+
var ActionEvent = z3.strictObject({
|
|
1253
1136
|
...envelope,
|
|
1254
|
-
type:
|
|
1255
|
-
action:
|
|
1256
|
-
parameter:
|
|
1257
|
-
result:
|
|
1258
|
-
ephemeral:
|
|
1137
|
+
type: z3.literal("action"),
|
|
1138
|
+
action: z3.string().trim().min(1).max(255),
|
|
1139
|
+
parameter: z3.string().trim().min(1).max(4096),
|
|
1140
|
+
result: z3.string().max(64 * 1024).optional(),
|
|
1141
|
+
ephemeral: z3.boolean().default(false)
|
|
1259
1142
|
});
|
|
1260
|
-
var ElicitationEvent =
|
|
1143
|
+
var ElicitationEvent = z3.strictObject({
|
|
1261
1144
|
...envelope,
|
|
1262
|
-
type:
|
|
1145
|
+
type: z3.literal("elicitation"),
|
|
1263
1146
|
body,
|
|
1264
|
-
signal:
|
|
1265
|
-
signalMetadata:
|
|
1147
|
+
signal: z3.enum(["select", "auth"]).optional(),
|
|
1148
|
+
signalMetadata: z3.record(z3.string(), z3.unknown()).optional()
|
|
1266
1149
|
}).superRefine((event, ctx) => {
|
|
1267
1150
|
if (!event.signal) {
|
|
1268
1151
|
if (event.signalMetadata) {
|
|
@@ -1284,14 +1167,14 @@ var ElicitationEvent = z6.strictObject({
|
|
|
1284
1167
|
});
|
|
1285
1168
|
}
|
|
1286
1169
|
});
|
|
1287
|
-
var ResponseEvent =
|
|
1170
|
+
var ResponseEvent = z3.strictObject({
|
|
1288
1171
|
...envelope,
|
|
1289
|
-
type:
|
|
1172
|
+
type: z3.literal("response"),
|
|
1290
1173
|
body
|
|
1291
1174
|
});
|
|
1292
|
-
var ErrorEvent =
|
|
1175
|
+
var ErrorEvent = z3.strictObject({
|
|
1293
1176
|
...envelope,
|
|
1294
|
-
type:
|
|
1177
|
+
type: z3.literal("error"),
|
|
1295
1178
|
body
|
|
1296
1179
|
});
|
|
1297
1180
|
var PLAN_ENTRY_STATUSES = [
|
|
@@ -1300,33 +1183,36 @@ var PLAN_ENTRY_STATUSES = [
|
|
|
1300
1183
|
"completed",
|
|
1301
1184
|
"canceled"
|
|
1302
1185
|
];
|
|
1303
|
-
var PlanEvent =
|
|
1186
|
+
var PlanEvent = z3.strictObject({
|
|
1304
1187
|
...envelope,
|
|
1305
|
-
type:
|
|
1306
|
-
entries:
|
|
1307
|
-
content:
|
|
1308
|
-
status:
|
|
1188
|
+
type: z3.literal("plan"),
|
|
1189
|
+
entries: z3.array(z3.strictObject({
|
|
1190
|
+
content: z3.string().trim().min(1).max(1024),
|
|
1191
|
+
status: z3.enum(PLAN_ENTRY_STATUSES)
|
|
1309
1192
|
})).max(100)
|
|
1310
1193
|
});
|
|
1311
|
-
var SummaryEvent =
|
|
1194
|
+
var SummaryEvent = z3.strictObject({
|
|
1312
1195
|
...envelope,
|
|
1313
|
-
type:
|
|
1314
|
-
title:
|
|
1196
|
+
type: z3.literal("summary"),
|
|
1197
|
+
title: z3.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
|
|
1315
1198
|
});
|
|
1316
|
-
var ExternalUrlEvent =
|
|
1199
|
+
var ExternalUrlEvent = z3.strictObject({
|
|
1317
1200
|
...envelope,
|
|
1318
|
-
type:
|
|
1319
|
-
label:
|
|
1320
|
-
url:
|
|
1201
|
+
type: z3.literal("external_url"),
|
|
1202
|
+
label: z3.string().trim().min(1).max(255),
|
|
1203
|
+
url: z3.url()
|
|
1321
1204
|
});
|
|
1322
|
-
var
|
|
1205
|
+
var TurnUsage = z3.strictObject({
|
|
1206
|
+
inputTokens: z3.number().int().min(0).optional(),
|
|
1207
|
+
outputTokens: z3.number().int().min(0).optional(),
|
|
1208
|
+
costUsd: z3.number().min(0).optional().describe("This turn's cost in US dollars, as the agent reported it")
|
|
1209
|
+
});
|
|
1210
|
+
var UsageEvent = z3.strictObject({
|
|
1323
1211
|
...envelope,
|
|
1324
|
-
type:
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
contextUsed: z6.number().int().min(0).optional(),
|
|
1329
|
-
contextSize: z6.number().int().min(0).optional()
|
|
1212
|
+
type: z3.literal("usage"),
|
|
1213
|
+
...TurnUsage.shape,
|
|
1214
|
+
contextUsed: z3.number().int().min(0).optional(),
|
|
1215
|
+
contextSize: z3.number().int().min(0).optional()
|
|
1330
1216
|
});
|
|
1331
1217
|
var STATUS_EVENT_STATES = [
|
|
1332
1218
|
"starting",
|
|
@@ -1334,12 +1220,12 @@ var STATUS_EVENT_STATES = [
|
|
|
1334
1220
|
"waiting_permission",
|
|
1335
1221
|
"finishing"
|
|
1336
1222
|
];
|
|
1337
|
-
var StatusEvent =
|
|
1223
|
+
var StatusEvent = z3.strictObject({
|
|
1338
1224
|
...envelope,
|
|
1339
|
-
type:
|
|
1340
|
-
state:
|
|
1225
|
+
type: z3.literal("status"),
|
|
1226
|
+
state: z3.enum(STATUS_EVENT_STATES)
|
|
1341
1227
|
});
|
|
1342
|
-
var SessionEvent =
|
|
1228
|
+
var SessionEvent = z3.discriminatedUnion("type", [
|
|
1343
1229
|
ThoughtEvent,
|
|
1344
1230
|
ActionEvent,
|
|
1345
1231
|
ElicitationEvent,
|
|
@@ -1351,16 +1237,137 @@ var SessionEvent = z6.discriminatedUnion("type", [
|
|
|
1351
1237
|
UsageEvent,
|
|
1352
1238
|
StatusEvent
|
|
1353
1239
|
]);
|
|
1354
|
-
var EventsRequest =
|
|
1355
|
-
workItemId:
|
|
1356
|
-
events:
|
|
1240
|
+
var EventsRequest = z3.strictObject({
|
|
1241
|
+
workItemId: z3.number().int().positive(),
|
|
1242
|
+
events: z3.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
|
|
1243
|
+
});
|
|
1244
|
+
var EventsResponse = z3.looseObject({
|
|
1245
|
+
accepted: z3.number().int().min(0),
|
|
1246
|
+
duplicates: z3.number().int().min(0),
|
|
1247
|
+
dropped: z3.number().int().min(0),
|
|
1248
|
+
cancelRequested: z3.boolean()
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// ../protocol/src/complete.ts
|
|
1252
|
+
var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
1253
|
+
var CompleteRequest = z4.strictObject({
|
|
1254
|
+
workItemId: z4.number().int().positive(),
|
|
1255
|
+
outcome: z4.enum(COMPLETE_OUTCOMES),
|
|
1256
|
+
error: z4.string().max(4096).nullable().optional(),
|
|
1257
|
+
acpSessionId: z4.string().max(255).nullable().optional(),
|
|
1258
|
+
branch: z4.string().max(255).nullable().optional(),
|
|
1259
|
+
answersConsumed: z4.array(z4.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt"),
|
|
1260
|
+
usage: TurnUsage.optional().describe("What the turn spent, in full; recorded even when the turn was stopped and its events were dropped")
|
|
1261
|
+
});
|
|
1262
|
+
var CompleteResponse = z4.looseObject({
|
|
1263
|
+
alreadyTerminal: z4.boolean()
|
|
1264
|
+
});
|
|
1265
|
+
// ../protocol/src/connect.ts
|
|
1266
|
+
import { z as z6 } from "zod";
|
|
1267
|
+
|
|
1268
|
+
// ../protocol/src/heartbeat.ts
|
|
1269
|
+
import { z as z5 } from "zod";
|
|
1270
|
+
var ACTIVE_STATES = ["starting", "running", "cancelling"];
|
|
1271
|
+
var ActiveItem = z5.strictObject({
|
|
1272
|
+
workItemId: z5.number().int().positive(),
|
|
1273
|
+
agentSessionId: z5.number().int().positive(),
|
|
1274
|
+
role: RunnerRole,
|
|
1275
|
+
agentId: z5.string().min(1).max(64),
|
|
1276
|
+
state: z5.enum(ACTIVE_STATES)
|
|
1277
|
+
});
|
|
1278
|
+
var MachineId = z5.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
|
|
1279
|
+
var HeartbeatRequest = z5.strictObject({
|
|
1280
|
+
protocolVersion: z5.number().int().positive(),
|
|
1281
|
+
daemonVersion: z5.string().trim().min(1).max(64),
|
|
1282
|
+
platform: z5.string().trim().min(1).max(64),
|
|
1283
|
+
hostname: z5.string().trim().min(1).max(256),
|
|
1284
|
+
draining: z5.boolean(),
|
|
1285
|
+
machineId: MachineId,
|
|
1286
|
+
limits: RunnerLimits,
|
|
1287
|
+
active: z5.array(ActiveItem).max(1000),
|
|
1288
|
+
capabilities: RunnerCapabilities,
|
|
1289
|
+
dryRun: z5.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
|
|
1290
|
+
});
|
|
1291
|
+
var DryRunRunner = z5.looseObject({
|
|
1292
|
+
id: z5.number().int().positive(),
|
|
1293
|
+
name: z5.string(),
|
|
1294
|
+
projectId: z5.number().int().positive(),
|
|
1295
|
+
projectSlug: z5.string()
|
|
1296
|
+
});
|
|
1297
|
+
var OFFER_KINDS = [
|
|
1298
|
+
"session_created",
|
|
1299
|
+
"prompt",
|
|
1300
|
+
"dispatch",
|
|
1301
|
+
"review"
|
|
1302
|
+
];
|
|
1303
|
+
var Offer = z5.looseObject({
|
|
1304
|
+
workItemId: z5.number().int().positive(),
|
|
1305
|
+
agentSessionId: z5.number().int().positive(),
|
|
1306
|
+
kind: z5.enum(OFFER_KINDS),
|
|
1307
|
+
role: RunnerRole,
|
|
1308
|
+
offerExpiresAt: z5.number().int()
|
|
1309
|
+
});
|
|
1310
|
+
var CANCEL_REASONS = [
|
|
1311
|
+
"stop",
|
|
1312
|
+
"dashboard",
|
|
1313
|
+
"delegate_removed",
|
|
1314
|
+
"issue_closed",
|
|
1315
|
+
"unassigned",
|
|
1316
|
+
"integration_revoked",
|
|
1317
|
+
"superseded"
|
|
1318
|
+
];
|
|
1319
|
+
var Cancel = z5.looseObject({
|
|
1320
|
+
workItemId: z5.number().int().positive(),
|
|
1321
|
+
reason: z5.union([z5.enum(CANCEL_REASONS), z5.string().min(1).max(64)]),
|
|
1322
|
+
by: z5.string().min(1).max(200).optional(),
|
|
1323
|
+
said: z5.string().min(1).max(2000).optional()
|
|
1324
|
+
});
|
|
1325
|
+
var STOP_QUOTE_MAX = 500;
|
|
1326
|
+
function stopQuote(said) {
|
|
1327
|
+
const text = said?.trim();
|
|
1328
|
+
if (!text || /^stop[.!]*$/i.test(text))
|
|
1329
|
+
return "";
|
|
1330
|
+
const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
|
|
1331
|
+
return `
|
|
1332
|
+
|
|
1333
|
+
> You said: ${cut.split(`
|
|
1334
|
+
`).join(`
|
|
1335
|
+
> `)}`;
|
|
1336
|
+
}
|
|
1337
|
+
var Answer = z5.looseObject({
|
|
1338
|
+
workItemId: z5.number().int().positive(),
|
|
1339
|
+
activityId: z5.number().int().positive(),
|
|
1340
|
+
body: z5.string()
|
|
1341
|
+
});
|
|
1342
|
+
var HeartbeatResponse = z5.looseObject({
|
|
1343
|
+
serverTime: z5.number().int(),
|
|
1344
|
+
heartbeatIntervalMs: z5.number().int().positive(),
|
|
1345
|
+
leaseMs: z5.number().int().positive(),
|
|
1346
|
+
offers: z5.array(Offer),
|
|
1347
|
+
cancels: z5.array(Cancel),
|
|
1348
|
+
unknownActive: z5.array(z5.number().int().positive()),
|
|
1349
|
+
answers: z5.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
|
|
1350
|
+
runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
|
|
1351
|
+
unknownSubscriptions: z5.looseObject({ teams: z5.array(z5.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
|
|
1357
1352
|
});
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
cancelRequested: z6.boolean()
|
|
1353
|
+
|
|
1354
|
+
// ../protocol/src/connect.ts
|
|
1355
|
+
var ConnectRequest = z6.strictObject({
|
|
1356
|
+
machineId: MachineId
|
|
1363
1357
|
});
|
|
1358
|
+
var ConnectResponse = z6.looseObject({
|
|
1359
|
+
runnerId: z6.number().int().positive(),
|
|
1360
|
+
channel: z6.string().regex(/^runner:\d+$/),
|
|
1361
|
+
heartbeatIntervalMs: z6.number().int().positive(),
|
|
1362
|
+
leaseMs: z6.number().int().positive(),
|
|
1363
|
+
serverTime: z6.number().int()
|
|
1364
|
+
});
|
|
1365
|
+
var MACHINE_IN_USE = "MACHINE_IN_USE";
|
|
1366
|
+
var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
|
|
1367
|
+
var RunnerFrame = z6.object({ event: z6.enum(RUNNER_FRAME_EVENTS) });
|
|
1368
|
+
function runnerChannel(runnerId) {
|
|
1369
|
+
return `runner:${runnerId}`;
|
|
1370
|
+
}
|
|
1364
1371
|
// ../protocol/src/otlp.ts
|
|
1365
1372
|
import protobuf from "protobufjs/light.js";
|
|
1366
1373
|
var SPAN_KIND = {
|
|
@@ -5740,6 +5747,11 @@ class TurnMapper {
|
|
|
5740
5747
|
lastPlan = "";
|
|
5741
5748
|
unknown = new Set;
|
|
5742
5749
|
lastActionSummary = null;
|
|
5750
|
+
costBaseline = null;
|
|
5751
|
+
replayedCost = null;
|
|
5752
|
+
costChecked = false;
|
|
5753
|
+
sessionCost = null;
|
|
5754
|
+
usage = {};
|
|
5743
5755
|
recorded = {};
|
|
5744
5756
|
constructor(options) {
|
|
5745
5757
|
this.options = options;
|
|
@@ -5751,8 +5763,14 @@ class TurnMapper {
|
|
|
5751
5763
|
return [...this.messages.values()].some((m) => m.trim());
|
|
5752
5764
|
}
|
|
5753
5765
|
update(update) {
|
|
5754
|
-
if (this.replaying)
|
|
5766
|
+
if (this.replaying) {
|
|
5767
|
+
if (update.sessionUpdate === "usage_update") {
|
|
5768
|
+
const cost = usdCost(update.cost);
|
|
5769
|
+
if (cost !== null)
|
|
5770
|
+
this.replayedCost = cost;
|
|
5771
|
+
}
|
|
5755
5772
|
return [];
|
|
5773
|
+
}
|
|
5756
5774
|
const kind = update.sessionUpdate;
|
|
5757
5775
|
switch (update.sessionUpdate) {
|
|
5758
5776
|
case "agent_thought_chunk": {
|
|
@@ -5838,14 +5856,10 @@ class TurnMapper {
|
|
|
5838
5856
|
return [{ type: "summary", title: truncate(title, 255) }];
|
|
5839
5857
|
}
|
|
5840
5858
|
case "usage_update": {
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
contextSize: Math.max(0, Math.round(update.size)),
|
|
5846
|
-
...update.cost && update.cost.currency.toUpperCase() === "USD" ? { costUsd: Math.max(0, update.cost.amount) } : {}
|
|
5847
|
-
}
|
|
5848
|
-
];
|
|
5859
|
+
this.usage.contextUsed = Math.max(0, Math.round(update.used));
|
|
5860
|
+
this.usage.contextSize = Math.max(0, Math.round(update.size));
|
|
5861
|
+
this.recordCost(usdCost(update.cost));
|
|
5862
|
+
return [{ type: "usage", ...this.usage }];
|
|
5849
5863
|
}
|
|
5850
5864
|
case "current_mode_update":
|
|
5851
5865
|
this.recorded.mode = update.currentModeId;
|
|
@@ -5861,6 +5875,47 @@ class TurnMapper {
|
|
|
5861
5875
|
return [];
|
|
5862
5876
|
}
|
|
5863
5877
|
}
|
|
5878
|
+
acceptReplay() {
|
|
5879
|
+
if (this.replayedCost !== null)
|
|
5880
|
+
this.costBaseline = this.replayedCost;
|
|
5881
|
+
this.replayedCost = null;
|
|
5882
|
+
}
|
|
5883
|
+
recordCost(cumulative) {
|
|
5884
|
+
if (cumulative === null)
|
|
5885
|
+
return;
|
|
5886
|
+
if (!this.costChecked) {
|
|
5887
|
+
this.costChecked = true;
|
|
5888
|
+
if (this.costBaseline !== null && cumulative < this.costBaseline)
|
|
5889
|
+
this.costBaseline = 0;
|
|
5890
|
+
}
|
|
5891
|
+
this.sessionCost = cumulative;
|
|
5892
|
+
if (this.costBaseline !== null)
|
|
5893
|
+
this.usage.costUsd = roundUsd(Math.max(0, cumulative - this.costBaseline));
|
|
5894
|
+
}
|
|
5895
|
+
tokens(tokens) {
|
|
5896
|
+
const count = (n) => typeof n === "number" && Number.isFinite(n) ? Math.max(0, Math.round(n)) : undefined;
|
|
5897
|
+
const input = count(tokens?.inputTokens);
|
|
5898
|
+
const output = count(tokens?.outputTokens);
|
|
5899
|
+
if (input === undefined && output === undefined)
|
|
5900
|
+
return [];
|
|
5901
|
+
if (input !== undefined)
|
|
5902
|
+
this.usage.inputTokens = input;
|
|
5903
|
+
if (output !== undefined)
|
|
5904
|
+
this.usage.outputTokens = output;
|
|
5905
|
+
return [{ type: "usage", ...this.usage }];
|
|
5906
|
+
}
|
|
5907
|
+
turnUsage() {
|
|
5908
|
+
const { inputTokens, outputTokens, costUsd } = this.usage;
|
|
5909
|
+
const usage = {
|
|
5910
|
+
...inputTokens !== undefined ? { inputTokens } : {},
|
|
5911
|
+
...outputTokens !== undefined ? { outputTokens } : {},
|
|
5912
|
+
...costUsd !== undefined ? { costUsd } : {}
|
|
5913
|
+
};
|
|
5914
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
5915
|
+
}
|
|
5916
|
+
sessionCostUsd() {
|
|
5917
|
+
return this.sessionCost ?? this.costBaseline;
|
|
5918
|
+
}
|
|
5864
5919
|
tick() {
|
|
5865
5920
|
if (!this.thought)
|
|
5866
5921
|
return [];
|
|
@@ -5963,6 +6018,14 @@ function callText(call) {
|
|
|
5963
6018
|
return parts.join(`
|
|
5964
6019
|
`);
|
|
5965
6020
|
}
|
|
6021
|
+
function usdCost(cost) {
|
|
6022
|
+
if (!cost || cost.currency.toUpperCase() !== "USD")
|
|
6023
|
+
return null;
|
|
6024
|
+
return Number.isFinite(cost.amount) ? Math.max(0, cost.amount) : null;
|
|
6025
|
+
}
|
|
6026
|
+
function roundUsd(usd) {
|
|
6027
|
+
return Math.round(usd * 1e6) / 1e6;
|
|
6028
|
+
}
|
|
5966
6029
|
|
|
5967
6030
|
// src/acp/mcp-servers.ts
|
|
5968
6031
|
import { isAbsolute as isAbsolute7 } from "path";
|
|
@@ -6706,7 +6769,9 @@ class Turn {
|
|
|
6706
6769
|
outcome,
|
|
6707
6770
|
error: error ? error.slice(0, 4000) : null,
|
|
6708
6771
|
acpSessionId: this.acpSessionId,
|
|
6709
|
-
branch: this.branch
|
|
6772
|
+
branch: this.branch,
|
|
6773
|
+
usage: this.mapper.turnUsage(),
|
|
6774
|
+
acpCostUsd: this.acpSessionId ? this.mapper.sessionCostUsd() : null
|
|
6710
6775
|
};
|
|
6711
6776
|
}
|
|
6712
6777
|
async start() {
|
|
@@ -6881,6 +6946,7 @@ class Turn {
|
|
|
6881
6946
|
const directories = extra.length && caps.sessionCapabilities?.additionalDirectories ? { additionalDirectories: extra } : {};
|
|
6882
6947
|
const mcpServers = this.mcpServers(caps);
|
|
6883
6948
|
this.resumed = false;
|
|
6949
|
+
this.mapper.costBaseline = this.ctx.previousAcpCostUsd ?? null;
|
|
6884
6950
|
if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
|
|
6885
6951
|
try {
|
|
6886
6952
|
if (caps.sessionCapabilities?.resume) {
|
|
@@ -6905,6 +6971,7 @@ class Turn {
|
|
|
6905
6971
|
mcpServers,
|
|
6906
6972
|
...directories
|
|
6907
6973
|
}), 0, ""));
|
|
6974
|
+
this.mapper.acceptReplay();
|
|
6908
6975
|
this.resumed = true;
|
|
6909
6976
|
await this.applyMode(answer);
|
|
6910
6977
|
return;
|
|
@@ -6922,6 +6989,7 @@ class Turn {
|
|
|
6922
6989
|
}
|
|
6923
6990
|
const created = await this.otel.around("acp.session.new", { "hypotenuse.mcp.servers": mcpServers.length }, () => this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, ""));
|
|
6924
6991
|
this.acpSessionId = created.sessionId;
|
|
6992
|
+
this.mapper.costBaseline = 0;
|
|
6925
6993
|
await this.applyMode(created);
|
|
6926
6994
|
}
|
|
6927
6995
|
resumed = false;
|
|
@@ -7038,6 +7106,7 @@ class Turn {
|
|
|
7038
7106
|
throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
|
|
7039
7107
|
});
|
|
7040
7108
|
const usage = answer.usage;
|
|
7109
|
+
this.emitAll(this.mapper.tokens(usage));
|
|
7041
7110
|
this.otel.promptEnded({
|
|
7042
7111
|
"gen_ai.response.finish_reasons": answer.stopReason,
|
|
7043
7112
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
@@ -7836,7 +7905,8 @@ var MIGRATIONS = [
|
|
|
7836
7905
|
ALTER TABLE worktrees ADD COLUMN removed_at INTEGER;
|
|
7837
7906
|
ALTER TABLE local_sessions ADD COLUMN last_answer TEXT;
|
|
7838
7907
|
ALTER TABLE local_sessions ADD COLUMN ended_at INTEGER;
|
|
7839
|
-
ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]'
|
|
7908
|
+
ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]';`,
|
|
7909
|
+
`ALTER TABLE local_sessions ADD COLUMN acp_cost_usd REAL;`
|
|
7840
7910
|
];
|
|
7841
7911
|
function toSession(row) {
|
|
7842
7912
|
return {
|
|
@@ -7850,6 +7920,7 @@ function toSession(row) {
|
|
|
7850
7920
|
nextSeq: row.next_seq,
|
|
7851
7921
|
acpAgentId: row.acp_agent_id ?? null,
|
|
7852
7922
|
acpSessionId: row.acp_session_id ?? null,
|
|
7923
|
+
acpCostUsd: row.acp_cost_usd ?? null,
|
|
7853
7924
|
pid: row.pid ?? null,
|
|
7854
7925
|
worktreeId: row.worktree_id ?? null,
|
|
7855
7926
|
createdAt: row.created_at,
|
|
@@ -7959,7 +8030,8 @@ class StateDb {
|
|
|
7959
8030
|
setSessionState(agentSessionId, state, extra = {}) {
|
|
7960
8031
|
this.db.query(`UPDATE local_sessions SET state = $state, updated_at = $now,
|
|
7961
8032
|
pid = CASE WHEN $setPid THEN $pid ELSE pid END,
|
|
7962
|
-
acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END
|
|
8033
|
+
acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END,
|
|
8034
|
+
acp_cost_usd = CASE WHEN $setCost THEN $cost ELSE acp_cost_usd END
|
|
7963
8035
|
WHERE agent_session_id = $id`).run({
|
|
7964
8036
|
state,
|
|
7965
8037
|
now: this.now(),
|
|
@@ -7967,7 +8039,9 @@ class StateDb {
|
|
|
7967
8039
|
setPid: extra.pid !== undefined ? 1 : 0,
|
|
7968
8040
|
pid: extra.pid ?? null,
|
|
7969
8041
|
setAcp: extra.acpSessionId !== undefined ? 1 : 0,
|
|
7970
|
-
acp: extra.acpSessionId ?? null
|
|
8042
|
+
acp: extra.acpSessionId ?? null,
|
|
8043
|
+
setCost: extra.acpCostUsd !== undefined ? 1 : 0,
|
|
8044
|
+
cost: extra.acpCostUsd ?? null
|
|
7971
8045
|
});
|
|
7972
8046
|
}
|
|
7973
8047
|
session(agentSessionId) {
|
|
@@ -10226,7 +10300,9 @@ class Supervisor {
|
|
|
10226
10300
|
...assignment,
|
|
10227
10301
|
agentId: assignment.agentId ?? this.agentFor(assignment.role)
|
|
10228
10302
|
}, this.config).agentId;
|
|
10229
|
-
const
|
|
10303
|
+
const local = state.session(assignment.agentSessionId);
|
|
10304
|
+
const previousAcpSessionId = local?.acpSessionId ?? null;
|
|
10305
|
+
const previousAcpCostUsd = local?.acpCostUsd ?? null;
|
|
10230
10306
|
state.claimSession({
|
|
10231
10307
|
agentSessionId: assignment.agentSessionId,
|
|
10232
10308
|
linearSessionId: assignment.externalSessionId,
|
|
@@ -10322,6 +10398,7 @@ class Supervisor {
|
|
|
10322
10398
|
stateDir: this.options.stateDir,
|
|
10323
10399
|
env: this.options.env,
|
|
10324
10400
|
previousAcpSessionId,
|
|
10401
|
+
previousAcpCostUsd,
|
|
10325
10402
|
onReply: (handler) => {
|
|
10326
10403
|
slot.onReply = handler;
|
|
10327
10404
|
},
|
|
@@ -10382,9 +10459,11 @@ class Supervisor {
|
|
|
10382
10459
|
this.slots.delete(slot.workItemId);
|
|
10383
10460
|
if (slot.nonce)
|
|
10384
10461
|
this.broker.end(slot.nonce);
|
|
10462
|
+
const acp = acpSessionAfterTurn(result, slot.silent, this.options.state.session(slot.agentSessionId)?.acpSessionId ?? null);
|
|
10385
10463
|
if (slot.silent) {
|
|
10386
10464
|
this.options.state.setSessionState(slot.agentSessionId, "interrupted", {
|
|
10387
|
-
pid: null
|
|
10465
|
+
pid: null,
|
|
10466
|
+
...acp
|
|
10388
10467
|
});
|
|
10389
10468
|
} else {
|
|
10390
10469
|
this.sink.complete({
|
|
@@ -10394,6 +10473,7 @@ class Supervisor {
|
|
|
10394
10473
|
error: result.error ?? null,
|
|
10395
10474
|
acpSessionId: result.acpSessionId ?? null,
|
|
10396
10475
|
branch: result.branch ?? null,
|
|
10476
|
+
...result.usage ? { usage: result.usage } : {},
|
|
10397
10477
|
...slot.consumed.length ? { answersConsumed: slot.consumed.slice(0, 100) } : {}
|
|
10398
10478
|
},
|
|
10399
10479
|
active: {
|
|
@@ -10405,10 +10485,7 @@ class Supervisor {
|
|
|
10405
10485
|
}
|
|
10406
10486
|
});
|
|
10407
10487
|
this.options.state.recordTurnEnd(slot.agentSessionId, slot.lastAnswer);
|
|
10408
|
-
this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", {
|
|
10409
|
-
pid: null,
|
|
10410
|
-
...result.acpSessionId ? { acpSessionId: result.acpSessionId } : {}
|
|
10411
|
-
});
|
|
10488
|
+
this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", { pid: null, ...acp });
|
|
10412
10489
|
}
|
|
10413
10490
|
this.options.log.info(`turn ended: ${slot.silent ? "taken away" : result.outcome}`, {
|
|
10414
10491
|
session: slot.agentSessionId,
|
|
@@ -10655,6 +10732,17 @@ class Supervisor {
|
|
|
10655
10732
|
this.telemetryQueue.stop();
|
|
10656
10733
|
}
|
|
10657
10734
|
}
|
|
10735
|
+
function acpSessionAfterTurn(result, silent, recordedAcpSessionId) {
|
|
10736
|
+
if (!result.acpSessionId)
|
|
10737
|
+
return {};
|
|
10738
|
+
if (!silent) {
|
|
10739
|
+
return {
|
|
10740
|
+
acpSessionId: result.acpSessionId,
|
|
10741
|
+
acpCostUsd: result.acpCostUsd ?? null
|
|
10742
|
+
};
|
|
10743
|
+
}
|
|
10744
|
+
return result.acpSessionId === recordedAcpSessionId ? { acpCostUsd: result.acpCostUsd ?? null } : {};
|
|
10745
|
+
}
|
|
10658
10746
|
function changedKeys(a, b, prefix = "") {
|
|
10659
10747
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10660
10748
|
if (isObject(a) && isObject(b)) {
|
|
@@ -11942,6 +12030,109 @@ function registerConfigCommands(program) {
|
|
|
11942
12030
|
});
|
|
11943
12031
|
}
|
|
11944
12032
|
|
|
12033
|
+
// src/commands/cost.ts
|
|
12034
|
+
var COST_DIMENSIONS = [
|
|
12035
|
+
"session",
|
|
12036
|
+
"root-session",
|
|
12037
|
+
"issue",
|
|
12038
|
+
"runner",
|
|
12039
|
+
"owner",
|
|
12040
|
+
"issuer",
|
|
12041
|
+
"role",
|
|
12042
|
+
"agent",
|
|
12043
|
+
"repository",
|
|
12044
|
+
"day"
|
|
12045
|
+
];
|
|
12046
|
+
function usd(usd) {
|
|
12047
|
+
if (usd === null)
|
|
12048
|
+
return "-";
|
|
12049
|
+
if (usd !== 0 && Math.abs(usd) < 0.01)
|
|
12050
|
+
return `$${usd.toPrecision(2)}`;
|
|
12051
|
+
return `$${usd.toFixed(2)}`;
|
|
12052
|
+
}
|
|
12053
|
+
function describeTotals(totals) {
|
|
12054
|
+
const turns = `${totals.turns} turn${totals.turns === 1 ? "" : "s"}`;
|
|
12055
|
+
const unpriced = totals.unpricedTurns ? ` (${totals.unpricedTurns} unpriced)` : "";
|
|
12056
|
+
const tokens = `${totals.inputTokens.toLocaleString("en-US")} in / ${totals.outputTokens.toLocaleString("en-US")} out`;
|
|
12057
|
+
return `${usd(totals.costUsd)} over ${turns}${unpriced}, ${tokens}`;
|
|
12058
|
+
}
|
|
12059
|
+
function withFilters(command) {
|
|
12060
|
+
return command.option("--session <id>", "only this agent session's turns").option("--root-session <id>", "only this manager session's turns and its dispatched workers'").option("--runner <id>", "only turns this runner ran").option("--owner <userId>", "only turns on runners this user owns").option("--issuer <userId>", "only turns this user asked for").option("--role <role>", "only turns in this role: manager or worker").option("--since <time>", "only turns that ended since: ISO-8601, epoch ms, or 7d / 12h / 30m").option("--until <time>", "only turns that ended before: ISO-8601, epoch ms, or 7d / 12h / 30m");
|
|
12061
|
+
}
|
|
12062
|
+
function filterQuery(command) {
|
|
12063
|
+
const opts = command.opts();
|
|
12064
|
+
return {
|
|
12065
|
+
agentSessionId: opts.session,
|
|
12066
|
+
rootAgentSessionId: opts.rootSession,
|
|
12067
|
+
runnerId: opts.runner,
|
|
12068
|
+
runnerUserId: opts.owner,
|
|
12069
|
+
issuerUserId: opts.issuer,
|
|
12070
|
+
role: opts.role,
|
|
12071
|
+
since: opts.since ? String(parseTime(opts.since)) : undefined,
|
|
12072
|
+
until: opts.until ? String(parseTime(opts.until)) : undefined
|
|
12073
|
+
};
|
|
12074
|
+
}
|
|
12075
|
+
function registerCostCommands(program) {
|
|
12076
|
+
const cost = program.command("cost").description("What the selected project's agent sessions cost, and whom it is attributed to");
|
|
12077
|
+
withPaging(withFilters(cost.command("list").description("List each turn's cost, newest first, with its session, runner, and role"))).action(async function() {
|
|
12078
|
+
await runCommand(this, async (ctx) => {
|
|
12079
|
+
const projectId = await ctx.projectId();
|
|
12080
|
+
const result = await ctx.signedInClient().request("GET", "/costs", {
|
|
12081
|
+
query: { projectId, ...filterQuery(this), ...pageQuery(this) }
|
|
12082
|
+
});
|
|
12083
|
+
emit(ctx, result, () => `${table(result.costRecords.map((r) => ({
|
|
12084
|
+
ended: when(r.endedAt),
|
|
12085
|
+
issue: r.issueIdentifier ?? "-",
|
|
12086
|
+
session: r.agentSessionId,
|
|
12087
|
+
role: r.role ?? "-",
|
|
12088
|
+
kind: r.kind,
|
|
12089
|
+
runner: r.runnerName,
|
|
12090
|
+
agent: r.agentId ?? "-",
|
|
12091
|
+
ending: r.ending,
|
|
12092
|
+
tokens: `${r.inputTokens ?? "-"} / ${r.outputTokens ?? "-"}`,
|
|
12093
|
+
cost: usd(r.costUsd)
|
|
12094
|
+
})), [
|
|
12095
|
+
{ key: "ended" },
|
|
12096
|
+
{ key: "issue" },
|
|
12097
|
+
{ key: "session" },
|
|
12098
|
+
{ key: "role" },
|
|
12099
|
+
{ key: "kind" },
|
|
12100
|
+
{ key: "runner" },
|
|
12101
|
+
{ key: "agent" },
|
|
12102
|
+
{ key: "ending" },
|
|
12103
|
+
{ key: "tokens" },
|
|
12104
|
+
{ key: "cost" }
|
|
12105
|
+
])}
|
|
12106
|
+
total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
|
|
12107
|
+
});
|
|
12108
|
+
});
|
|
12109
|
+
withPaging(withFilters(cost.command("summary").description("Total the turns' cost by one dimension, most expensive first").option("--by <dimension>", `what to group by: ${COST_DIMENSIONS.join(", ")}`, "session"))).action(async function() {
|
|
12110
|
+
await runCommand(this, async (ctx) => {
|
|
12111
|
+
const projectId = await ctx.projectId();
|
|
12112
|
+
const { by } = this.opts();
|
|
12113
|
+
const result = await ctx.signedInClient().request("GET", "/costs/summary", {
|
|
12114
|
+
query: { projectId, by, ...filterQuery(this), ...pageQuery(this) }
|
|
12115
|
+
});
|
|
12116
|
+
emit(ctx, result, () => `${table(result.groups.map((g) => ({
|
|
12117
|
+
[result.by]: g.label ?? g.key ?? "(none)",
|
|
12118
|
+
turns: g.turns,
|
|
12119
|
+
unpriced: g.unpricedTurns,
|
|
12120
|
+
"input tokens": g.inputTokens,
|
|
12121
|
+
"output tokens": g.outputTokens,
|
|
12122
|
+
cost: usd(g.costUsd)
|
|
12123
|
+
})), [
|
|
12124
|
+
{ key: result.by, maxWidth: 50 },
|
|
12125
|
+
{ key: "turns" },
|
|
12126
|
+
{ key: "unpriced" },
|
|
12127
|
+
{ key: "input tokens" },
|
|
12128
|
+
{ key: "output tokens" },
|
|
12129
|
+
{ key: "cost" }
|
|
12130
|
+
])}
|
|
12131
|
+
total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
|
|
12132
|
+
});
|
|
12133
|
+
});
|
|
12134
|
+
}
|
|
12135
|
+
|
|
11945
12136
|
// src/commands/doctor.ts
|
|
11946
12137
|
import { existsSync as existsSync21, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
|
|
11947
12138
|
import { hostname as hostname6 } from "os";
|
|
@@ -13709,6 +13900,11 @@ function describeSession(session, activities, tree = {}) {
|
|
|
13709
13900
|
if (tree.issuer?.user) {
|
|
13710
13901
|
lines.push(`issuer ${tree.issuer.user.name}`);
|
|
13711
13902
|
}
|
|
13903
|
+
if (tree.cost?.turns)
|
|
13904
|
+
lines.push(`cost ${describeTotals(tree.cost)}`);
|
|
13905
|
+
if (tree.treeCost && tree.treeCost.turns > (tree.cost?.turns ?? 0)) {
|
|
13906
|
+
lines.push(`tree cost ${describeTotals(tree.treeCost)} with its workers`);
|
|
13907
|
+
}
|
|
13712
13908
|
if (session.summary)
|
|
13713
13909
|
lines.push(`summary ${session.summary}`);
|
|
13714
13910
|
if (session.externalUrl)
|
|
@@ -14443,6 +14639,7 @@ function buildProgram() {
|
|
|
14443
14639
|
registerIntegrationCommands(program);
|
|
14444
14640
|
registerSessionCommands(program);
|
|
14445
14641
|
registerEventCommands(program);
|
|
14642
|
+
registerCostCommands(program);
|
|
14446
14643
|
registerRunnerCommands(program);
|
|
14447
14644
|
registerRoutingCommands(program);
|
|
14448
14645
|
registerTelemetryCommands(program);
|
package/package.json
CHANGED
package/skills/hypotenuse.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hypotenuse
|
|
3
|
-
description: Operate a Hypotenuse server from the terminal with the `hypotenuse` CLI — sign in, pick a project, manage members, invites, tags, connect trigger integrations (Linear), register and manage runners, run this machine as a runner (config file, daemon, user service, doctor), edit routing rules and settings, point the project's telemetry at OpenTelemetry collectors, link Linear users to members, read and stop agent sessions, and read the audit log. Use when asked to administer Hypotenuse projects, check what @hypotenuse did on an issue, or check who changed what.
|
|
3
|
+
description: Operate a Hypotenuse server from the terminal with the `hypotenuse` CLI — sign in, pick a project, manage members, invites, tags, connect trigger integrations (Linear), register and manage runners, run this machine as a runner (config file, daemon, user service, doctor), edit routing rules and settings, point the project's telemetry at OpenTelemetry collectors, link Linear users to members, read and stop agent sessions, see what they cost, and read the audit log. Use when asked to administer Hypotenuse projects, check what @hypotenuse did on an issue, or check who changed what.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hypotenuse
|
|
@@ -38,6 +38,7 @@ server. It calls the same HTTP actions the dashboard does, as the person who sig
|
|
|
38
38
|
| `hypotenuse event list\|view` | The selected project's event log: every webhook its Linear workspace sent in the last 14 days and how each was routed. `list [--type AgentSessionEvent\|Issue\|…] [--outcome handled\|ignored\|duplicate_of_dispatch\|failed\|pending] [--hide-ignored] [--session <id>] [--since 12h]`, newest first, shows each event's issue, outcome, what it did, the role it was routed as and why (`worker (default)`, `manager (rule /manage)`, `worker (dispatched)`), and where its turn is now (`queued: no_capacity`, `running on <runner>`); `view <id>` shows one in full, with the payload Linear sent for project admins only. |
|
|
39
39
|
| `hypotenuse runner register\|list\|view\|edit\|revoke` | The selected project's runners — machines that run coding agents. `register [--name <name>] [--replace] [--force]` registers **this** machine as the signed-in person, saves the runner token to `credentials.json` (never printed), and sends a first heartbeat; `--replace` revokes the runner of that name first. `list [--status online\|offline\|draining\|revoked] [--all]`; `view <id\|name>` shows capabilities, subscriptions, and whether its socket is attached; `edit <id\|name> [--name] [--enable\|--disable]` renames or pauses it; `revoke <id\|name> --yes` revokes it for good (its owner or an admin). |
|
|
40
40
|
| `hypotenuse routing list\|add\|edit\|remove\|reorder\|preview\|settings` | The selected project's routing rules — the first enabled rule whose match holds decides whether a new session gets a **manager** or a **worker** (no match: worker). `list`; `add --name <name> --role manager\|worker [--label <l>] [--team <key>] [--linear-project <id>] [--prefix /manage] [--has-parent\|--no-parent] [--origin delegation\|mention] [--repository <repo>] [--disabled]` (labels and team keys are checked against the connected workspace); `edit <id>` with the same flags (any match flag replaces the whole match) plus `--enable\|--disable` and `--clear-repository`; `remove <id>`; `reorder <id...>` lists every rule id in the new order; `preview <ENG-123> [--prompt "/manage ..."] [--with-repository]` shows how the issue would route if delegated now — the matching rule, the role, every runner with whether it could take the work and its reason codes, and the repository (any member; `--with-repository` asks Linear to rank repositories, spending its API budget); `settings` shows the routing timers, `settings [--queue-timeout 30m] [--sticky-timeout 10m] [--offer-ttl 10s] [--stop-grace 30s] [--repository-confidence 0.7] [--max-review-rounds 3] [--reset <key>]` changes them. Changes are admin-only. |
|
|
41
|
+
| `hypotenuse cost list\|summary` | What the selected project's turns cost and whom it is attributed to, as the coding agents reported it (US dollars and tokens; Hypotenuse never prices tokens itself). `list`, newest first, prints one record per runner's claim of a turn — issue, session, role, kind, runner, agent, how it ended, tokens, cost — and the total; `summary --by session\|root-session\|issue\|runner\|owner\|issuer\|role\|agent\|repository\|day` totals them per group, most expensive first, counting turns with no reported cost as `unpriced`. Both take `--session <id>`, `--root-session <id>` (a manager and its workers), `--runner <id>`, `--owner <userId>`, `--issuer <userId>`, `--role manager\|worker`, `--since 30d`, `--until <time>`, and paging. Any member. |
|
|
41
42
|
| `hypotenuse telemetry exporter list\|view\|add\|edit\|remove\|test` | Where the selected project's OpenTelemetry goes: each exporter is an OTLP/HTTP collector that receives every turn as one trace, at a content level (`metadata`, `content`, or `full` — full sends prompts and source code). `list` and `view <id>` show endpoint, level, signals, sample share, header names (never values), and the last export or error (any member); `add --name <name> --endpoint <https-url> [--header KEY=VALUE...] [--content <level>] [--signals traces,metrics,logs] [--sample 0.5] [--attribute KEY=VALUE...] [--no-agent-native] [--disabled]`; `edit <id>` with the same flags plus `--clear-headers` and `--enable\|--disable` (headers are kept unless given); `remove <id>`; `test <id>` sends a probe and prints each signal's answer, exiting 1 on a failure. Changes and tests are admin-only; header values are never printed. |
|
|
42
43
|
| `hypotenuse identity list\|link\|unlink` | Links between people in the project's Linear workspace and project members; routing prefers the runners of whoever started a session. `list [--user <id>]` shows each link and how it was made (e-mail match, Linear sign-in, admin); `link --user <id> --linear-user <id> [--name] [--email]` links one (admin); `unlink <id>` removes your own link, or anyone's as an admin. |
|
|
43
44
|
| `hypotenuse config init\|validate\|schema\|path` | The daemon's TOML config (`-c <path>`, else `HYPOTENUSE_CONFIG`, else `~/.config/hypotenuse/config.toml`). `init [--repo <path>...] [--force]` writes a commented starter filled in from this machine; `validate [--online] [--offline]` checks shape, references, and this machine (git clones, prompt files, agent commands), and with `--online` the server, runner token, project, and team keys — exit 0 valid (warnings allowed), 1 errors, 2 unreadable; `--json` prints `{ valid, errors: [{ path, message, hint? }], warnings }`. `schema` prints the JSON Schema; `path` prints the config, state directory, and credentials file in use. |
|
|
@@ -63,6 +64,7 @@ hypotenuse --json integration view linear | jq '.integration | {type, status, wo
|
|
|
63
64
|
hypotenuse --json session list --issue ENG-123 | jq '.agentSessions[] | {id, status}'
|
|
64
65
|
hypotenuse --json runner list | jq '.runners[] | {name, status, owner: .owner.name}'
|
|
65
66
|
hypotenuse --json session view 42 | jq '.activities[] | {seq, direction, contentType, deliveryStatus}'
|
|
67
|
+
hypotenuse --json cost summary --by owner --since 7d | jq '.groups[] | {owner: .label, costUsd, turns}'
|
|
66
68
|
hypotenuse --json config validate | jq -r '.errors[] | "\(.path): \(.message)"'
|
|
67
69
|
hypotenuse daemon start && hypotenuse --json daemon status | jq '{connected: .server.connected, slots}'
|
|
68
70
|
```
|