@arcadeai/hypotenuse 0.13.1 → 0.14.1
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 +2173 -394
- package/package.json +2 -1
- package/skills/hypotenuse.md +3 -2
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.14.1",
|
|
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",
|
|
@@ -385,6 +385,7 @@ var package_default = {
|
|
|
385
385
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
386
386
|
commander: "^14.0.3",
|
|
387
387
|
nanospinner: "^1.2.2",
|
|
388
|
+
protobufjs: "^8.8.1",
|
|
388
389
|
"smol-toml": "^1.9.1",
|
|
389
390
|
upgradr: "^0.4.1",
|
|
390
391
|
zod: "^4.3.6"
|
|
@@ -753,86 +754,226 @@ function isPreset(name) {
|
|
|
753
754
|
import { basename } from "path";
|
|
754
755
|
|
|
755
756
|
// ../protocol/src/capabilities.ts
|
|
757
|
+
import { z as z2 } from "zod";
|
|
758
|
+
|
|
759
|
+
// ../protocol/src/telemetry.ts
|
|
756
760
|
import { z } from "zod";
|
|
761
|
+
var CONTENT_LEVELS = ["metadata", "content", "full"];
|
|
762
|
+
var ContentLevel = z.enum(CONTENT_LEVELS);
|
|
763
|
+
function levelRank(level) {
|
|
764
|
+
return CONTENT_LEVELS.indexOf(level);
|
|
765
|
+
}
|
|
766
|
+
function minLevel(a, b) {
|
|
767
|
+
return levelRank(a) <= levelRank(b) ? a : b;
|
|
768
|
+
}
|
|
769
|
+
var LEVEL_ATTRIBUTE = "hypotenuse.telemetry.level";
|
|
770
|
+
var CONTENT_ATTRIBUTE_PREFIX = "hypotenuse.content.";
|
|
771
|
+
var CONTENT_CAP_BYTES = 64 * 1024;
|
|
772
|
+
var TRUNCATED_SUFFIX = ".original_bytes";
|
|
773
|
+
var TELEMETRY_WORK_ITEM_HEADER = "X-Hypotenuse-Work-Item";
|
|
774
|
+
var TELEMETRY_SIGNAL_HEADER = "X-Hypotenuse-Signal";
|
|
775
|
+
var TRACEPARENT_HEADER = "traceparent";
|
|
776
|
+
var MAX_TELEMETRY_BODY_BYTES = 1024 * 1024;
|
|
777
|
+
var MAX_TELEMETRY_INFLATED_BYTES = 8 * 1024 * 1024;
|
|
778
|
+
var ClaimTelemetry = z.looseObject({
|
|
779
|
+
traceparent: z.string().describe("The turn's root span; every daemon:* call for it carries it"),
|
|
780
|
+
sampled: z.boolean().describe("Whether any exporter keeps this turn's spans and logs"),
|
|
781
|
+
content: ContentLevel.describe("The level to collect at, already lowered to this runner's cap"),
|
|
782
|
+
agentNative: z.boolean().describe("Whether to capture the coding agent's own telemetry")
|
|
783
|
+
});
|
|
784
|
+
var TelemetryCapability = z.strictObject({
|
|
785
|
+
enabled: z.boolean(),
|
|
786
|
+
maxContent: ContentLevel,
|
|
787
|
+
agentNative: z.boolean()
|
|
788
|
+
});
|
|
789
|
+
var TelemetryResponse = z.looseObject({
|
|
790
|
+
accepted: z.boolean()
|
|
791
|
+
});
|
|
792
|
+
function randomHex(bytes) {
|
|
793
|
+
for (;; ) {
|
|
794
|
+
const buf = crypto.getRandomValues(new Uint8Array(bytes));
|
|
795
|
+
if (buf.some((b) => b !== 0))
|
|
796
|
+
return Buffer.from(buf).toString("hex");
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function newSpanId() {
|
|
800
|
+
return randomHex(8);
|
|
801
|
+
}
|
|
802
|
+
function formatTraceparent(ctx) {
|
|
803
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}`;
|
|
804
|
+
}
|
|
805
|
+
function parseTraceparent(value) {
|
|
806
|
+
const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec((value ?? "").trim().toLowerCase());
|
|
807
|
+
if (!match)
|
|
808
|
+
return null;
|
|
809
|
+
const [, traceId, spanId, flags] = match;
|
|
810
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId))
|
|
811
|
+
return null;
|
|
812
|
+
return { traceId, spanId, sampled: (Number.parseInt(flags, 16) & 1) === 1 };
|
|
813
|
+
}
|
|
814
|
+
function capContent(text) {
|
|
815
|
+
const bytes = Buffer.byteLength(text);
|
|
816
|
+
if (bytes <= CONTENT_CAP_BYTES)
|
|
817
|
+
return { text, originalBytes: null };
|
|
818
|
+
let cut = Buffer.from(text).subarray(0, CONTENT_CAP_BYTES).toString("utf8");
|
|
819
|
+
if (cut.endsWith("\uFFFD"))
|
|
820
|
+
cut = cut.slice(0, -1);
|
|
821
|
+
return { text: cut, originalBytes: bytes };
|
|
822
|
+
}
|
|
823
|
+
var SECRET_PATTERNS = [
|
|
824
|
+
/hyp_(?:rt|cli)_[A-Za-z0-9_-]{8,}/g,
|
|
825
|
+
/sk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
|
|
826
|
+
/lin_(?:api|oauth)_[A-Za-z0-9]{16,}/g,
|
|
827
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
828
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
829
|
+
/xox[abposr]-[A-Za-z0-9-]{10,}/g,
|
|
830
|
+
/AIza[0-9A-Za-z_-]{30,}/g,
|
|
831
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
832
|
+
/eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
833
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
834
|
+
/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/g
|
|
835
|
+
];
|
|
836
|
+
var REDACTED = "[REDACTED]";
|
|
837
|
+
var SECRET_KEY = /((^|[._-])(authorization|cookie|set-cookie|token|secret|password|passwd)$)|([a-z0-9][_-](key|token)$)/i;
|
|
838
|
+
var MIN_SECRET_LENGTH = 6;
|
|
839
|
+
function redactText(text, values) {
|
|
840
|
+
let out = text;
|
|
841
|
+
for (const v of values)
|
|
842
|
+
if (out.includes(v))
|
|
843
|
+
out = out.split(v).join(REDACTED);
|
|
844
|
+
for (const p of SECRET_PATTERNS)
|
|
845
|
+
out = out.replace(p, REDACTED);
|
|
846
|
+
return out;
|
|
847
|
+
}
|
|
848
|
+
function redactOtlp(request, options = {}) {
|
|
849
|
+
const values = [...new Set(options.values ?? [])].filter((v) => v.length >= MIN_SECRET_LENGTH).sort((a, b) => b.length - a.length);
|
|
850
|
+
const keys = new Set(options.keys ?? []);
|
|
851
|
+
const isSecretKey = (k) => keys.has(k) || SECRET_KEY.test(k);
|
|
852
|
+
const walk = (value, key) => {
|
|
853
|
+
if (typeof value === "string") {
|
|
854
|
+
if (key === "traceId" || key === "spanId" || key === "parentSpanId")
|
|
855
|
+
return value;
|
|
856
|
+
return redactText(value, values);
|
|
857
|
+
}
|
|
858
|
+
if (Array.isArray(value))
|
|
859
|
+
return value.map((v) => walk(v, key));
|
|
860
|
+
if (value && typeof value === "object") {
|
|
861
|
+
const obj = value;
|
|
862
|
+
if (typeof obj.key === "string" && "value" in obj && isSecretKey(obj.key))
|
|
863
|
+
return { key: obj.key, value: { stringValue: REDACTED } };
|
|
864
|
+
const out = {};
|
|
865
|
+
for (const [k, v] of Object.entries(obj))
|
|
866
|
+
out[k] = walk(v, k);
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
return value;
|
|
870
|
+
};
|
|
871
|
+
return walk(request, "");
|
|
872
|
+
}
|
|
873
|
+
function stampResource(signal, request, attrs) {
|
|
874
|
+
const groups = signal === "traces" ? request.resourceSpans : signal === "logs" ? request.resourceLogs : request.resourceMetrics;
|
|
875
|
+
const keys = new Set(attrs.map((a) => a.key));
|
|
876
|
+
for (const group of groups ?? []) {
|
|
877
|
+
const existing = (group.resource?.attributes ?? []).filter((a) => !keys.has(a.key));
|
|
878
|
+
group.resource = { ...group.resource, attributes: [...existing, ...attrs] };
|
|
879
|
+
}
|
|
880
|
+
return request;
|
|
881
|
+
}
|
|
882
|
+
function mergeRequests(signal, requests) {
|
|
883
|
+
if (signal === "traces")
|
|
884
|
+
return {
|
|
885
|
+
resourceSpans: requests.flatMap((r) => r.resourceSpans ?? [])
|
|
886
|
+
};
|
|
887
|
+
if (signal === "logs")
|
|
888
|
+
return {
|
|
889
|
+
resourceLogs: requests.flatMap((r) => r.resourceLogs ?? [])
|
|
890
|
+
};
|
|
891
|
+
return {
|
|
892
|
+
resourceMetrics: requests.flatMap((r) => r.resourceMetrics ?? [])
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ../protocol/src/capabilities.ts
|
|
757
897
|
var RUNNER_ROLES = ["manager", "worker"];
|
|
758
|
-
var RunnerRole =
|
|
759
|
-
var identifier =
|
|
760
|
-
var AgentCapability =
|
|
898
|
+
var RunnerRole = z2.enum(RUNNER_ROLES);
|
|
899
|
+
var identifier = z2.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`");
|
|
900
|
+
var AgentCapability = z2.strictObject({
|
|
761
901
|
id: identifier.describe("The agent's id in the config file"),
|
|
762
902
|
preset: identifier.describe("The preset it is built on, e.g. claude"),
|
|
763
|
-
available:
|
|
764
|
-
version:
|
|
765
|
-
roles:
|
|
766
|
-
error:
|
|
903
|
+
available: z2.boolean().describe("Whether the daemon's probe succeeded"),
|
|
904
|
+
version: z2.string().max(128).optional().describe("The agent's version"),
|
|
905
|
+
roles: z2.array(RunnerRole).max(RUNNER_ROLES.length).optional().describe("The roles this agent plays; absent means every role"),
|
|
906
|
+
error: z2.string().max(1024).optional().describe("Why the probe failed, when it did")
|
|
767
907
|
});
|
|
768
|
-
var RepositoryCapability =
|
|
908
|
+
var RepositoryCapability = z2.strictObject({
|
|
769
909
|
name: identifier.describe("The repository's name in the config file"),
|
|
770
|
-
remote:
|
|
771
|
-
baseBranch:
|
|
910
|
+
remote: z2.string().trim().min(1).max(512).describe("Its git remote; the server normalizes it to host/owner/name"),
|
|
911
|
+
baseBranch: z2.string().trim().min(1).max(255).describe("The base branch")
|
|
772
912
|
});
|
|
773
|
-
var subscriptionValues =
|
|
774
|
-
var Subscriptions =
|
|
913
|
+
var subscriptionValues = z2.array(z2.string().trim().min(1).max(255)).max(200).default([]);
|
|
914
|
+
var Subscriptions = z2.strictObject({
|
|
775
915
|
teams: subscriptionValues.describe("Team keys, e.g. ENG"),
|
|
776
916
|
projects: subscriptionValues.describe("Project ids"),
|
|
777
917
|
labels: subscriptionValues.describe("Label names (matched lowercased)")
|
|
778
918
|
});
|
|
779
|
-
var RunnerCapabilities =
|
|
780
|
-
roles:
|
|
781
|
-
agents:
|
|
782
|
-
repositories:
|
|
783
|
-
subscriptions: Subscriptions
|
|
919
|
+
var RunnerCapabilities = z2.strictObject({
|
|
920
|
+
roles: z2.array(RunnerRole).max(RUNNER_ROLES.length),
|
|
921
|
+
agents: z2.array(AgentCapability).max(64),
|
|
922
|
+
repositories: z2.array(RepositoryCapability).max(256),
|
|
923
|
+
subscriptions: Subscriptions,
|
|
924
|
+
telemetry: TelemetryCapability.optional().describe("The runner owner's cap on what telemetry leaves the machine; absent from older daemons, which send none")
|
|
784
925
|
});
|
|
785
|
-
var cap =
|
|
786
|
-
var RunnerLimits =
|
|
787
|
-
maxParallelJobs:
|
|
788
|
-
roles:
|
|
789
|
-
agents:
|
|
926
|
+
var cap = z2.number().int().min(0).max(1000);
|
|
927
|
+
var RunnerLimits = z2.strictObject({
|
|
928
|
+
maxParallelJobs: z2.number().int().min(1).max(1000),
|
|
929
|
+
roles: z2.partialRecord(RunnerRole, cap).optional(),
|
|
930
|
+
agents: z2.record(identifier, cap).optional()
|
|
790
931
|
});
|
|
791
932
|
// ../protocol/src/complete.ts
|
|
792
|
-
import { z as
|
|
933
|
+
import { z as z3 } from "zod";
|
|
793
934
|
var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
794
|
-
var CompleteRequest =
|
|
795
|
-
workItemId:
|
|
796
|
-
outcome:
|
|
797
|
-
error:
|
|
798
|
-
acpSessionId:
|
|
799
|
-
branch:
|
|
800
|
-
answersConsumed:
|
|
935
|
+
var CompleteRequest = z3.strictObject({
|
|
936
|
+
workItemId: z3.number().int().positive(),
|
|
937
|
+
outcome: z3.enum(COMPLETE_OUTCOMES),
|
|
938
|
+
error: z3.string().max(4096).nullable().optional(),
|
|
939
|
+
acpSessionId: z3.string().max(255).nullable().optional(),
|
|
940
|
+
branch: z3.string().max(255).nullable().optional(),
|
|
941
|
+
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")
|
|
801
942
|
});
|
|
802
|
-
var CompleteResponse =
|
|
803
|
-
alreadyTerminal:
|
|
943
|
+
var CompleteResponse = z3.looseObject({
|
|
944
|
+
alreadyTerminal: z3.boolean()
|
|
804
945
|
});
|
|
805
946
|
// ../protocol/src/connect.ts
|
|
806
|
-
import { z as
|
|
947
|
+
import { z as z5 } from "zod";
|
|
807
948
|
|
|
808
949
|
// ../protocol/src/heartbeat.ts
|
|
809
|
-
import { z as
|
|
950
|
+
import { z as z4 } from "zod";
|
|
810
951
|
var ACTIVE_STATES = ["starting", "running", "cancelling"];
|
|
811
|
-
var ActiveItem =
|
|
812
|
-
workItemId:
|
|
813
|
-
agentSessionId:
|
|
952
|
+
var ActiveItem = z4.strictObject({
|
|
953
|
+
workItemId: z4.number().int().positive(),
|
|
954
|
+
agentSessionId: z4.number().int().positive(),
|
|
814
955
|
role: RunnerRole,
|
|
815
|
-
agentId:
|
|
816
|
-
state:
|
|
956
|
+
agentId: z4.string().min(1).max(64),
|
|
957
|
+
state: z4.enum(ACTIVE_STATES)
|
|
817
958
|
});
|
|
818
|
-
var MachineId =
|
|
819
|
-
var HeartbeatRequest =
|
|
820
|
-
protocolVersion:
|
|
821
|
-
daemonVersion:
|
|
822
|
-
platform:
|
|
823
|
-
hostname:
|
|
824
|
-
draining:
|
|
959
|
+
var MachineId = z4.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
|
|
960
|
+
var HeartbeatRequest = z4.strictObject({
|
|
961
|
+
protocolVersion: z4.number().int().positive(),
|
|
962
|
+
daemonVersion: z4.string().trim().min(1).max(64),
|
|
963
|
+
platform: z4.string().trim().min(1).max(64),
|
|
964
|
+
hostname: z4.string().trim().min(1).max(256),
|
|
965
|
+
draining: z4.boolean(),
|
|
825
966
|
machineId: MachineId,
|
|
826
967
|
limits: RunnerLimits,
|
|
827
|
-
active:
|
|
968
|
+
active: z4.array(ActiveItem).max(1000),
|
|
828
969
|
capabilities: RunnerCapabilities,
|
|
829
|
-
dryRun:
|
|
970
|
+
dryRun: z4.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
|
|
830
971
|
});
|
|
831
|
-
var DryRunRunner =
|
|
832
|
-
id:
|
|
833
|
-
name:
|
|
834
|
-
projectId:
|
|
835
|
-
projectSlug:
|
|
972
|
+
var DryRunRunner = z4.looseObject({
|
|
973
|
+
id: z4.number().int().positive(),
|
|
974
|
+
name: z4.string(),
|
|
975
|
+
projectId: z4.number().int().positive(),
|
|
976
|
+
projectSlug: z4.string()
|
|
836
977
|
});
|
|
837
978
|
var OFFER_KINDS = [
|
|
838
979
|
"session_created",
|
|
@@ -840,12 +981,12 @@ var OFFER_KINDS = [
|
|
|
840
981
|
"dispatch",
|
|
841
982
|
"review"
|
|
842
983
|
];
|
|
843
|
-
var Offer =
|
|
844
|
-
workItemId:
|
|
845
|
-
agentSessionId:
|
|
846
|
-
kind:
|
|
984
|
+
var Offer = z4.looseObject({
|
|
985
|
+
workItemId: z4.number().int().positive(),
|
|
986
|
+
agentSessionId: z4.number().int().positive(),
|
|
987
|
+
kind: z4.enum(OFFER_KINDS),
|
|
847
988
|
role: RunnerRole,
|
|
848
|
-
offerExpiresAt:
|
|
989
|
+
offerExpiresAt: z4.number().int()
|
|
849
990
|
});
|
|
850
991
|
var CANCEL_REASONS = [
|
|
851
992
|
"stop",
|
|
@@ -856,11 +997,11 @@ var CANCEL_REASONS = [
|
|
|
856
997
|
"integration_revoked",
|
|
857
998
|
"superseded"
|
|
858
999
|
];
|
|
859
|
-
var Cancel =
|
|
860
|
-
workItemId:
|
|
861
|
-
reason:
|
|
862
|
-
by:
|
|
863
|
-
said:
|
|
1000
|
+
var Cancel = z4.looseObject({
|
|
1001
|
+
workItemId: z4.number().int().positive(),
|
|
1002
|
+
reason: z4.union([z4.enum(CANCEL_REASONS), z4.string().min(1).max(64)]),
|
|
1003
|
+
by: z4.string().min(1).max(200).optional(),
|
|
1004
|
+
said: z4.string().min(1).max(2000).optional()
|
|
864
1005
|
});
|
|
865
1006
|
var STOP_QUOTE_MAX = 500;
|
|
866
1007
|
function stopQuote(said) {
|
|
@@ -874,87 +1015,87 @@ function stopQuote(said) {
|
|
|
874
1015
|
`).join(`
|
|
875
1016
|
> `)}`;
|
|
876
1017
|
}
|
|
877
|
-
var Answer =
|
|
878
|
-
workItemId:
|
|
879
|
-
activityId:
|
|
880
|
-
body:
|
|
1018
|
+
var Answer = z4.looseObject({
|
|
1019
|
+
workItemId: z4.number().int().positive(),
|
|
1020
|
+
activityId: z4.number().int().positive(),
|
|
1021
|
+
body: z4.string()
|
|
881
1022
|
});
|
|
882
|
-
var HeartbeatResponse =
|
|
883
|
-
serverTime:
|
|
884
|
-
heartbeatIntervalMs:
|
|
885
|
-
leaseMs:
|
|
886
|
-
offers:
|
|
887
|
-
cancels:
|
|
888
|
-
unknownActive:
|
|
889
|
-
answers:
|
|
1023
|
+
var HeartbeatResponse = z4.looseObject({
|
|
1024
|
+
serverTime: z4.number().int(),
|
|
1025
|
+
heartbeatIntervalMs: z4.number().int().positive(),
|
|
1026
|
+
leaseMs: z4.number().int().positive(),
|
|
1027
|
+
offers: z4.array(Offer),
|
|
1028
|
+
cancels: z4.array(Cancel),
|
|
1029
|
+
unknownActive: z4.array(z4.number().int().positive()),
|
|
1030
|
+
answers: z4.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
|
|
890
1031
|
runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
|
|
891
|
-
unknownSubscriptions:
|
|
1032
|
+
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")
|
|
892
1033
|
});
|
|
893
1034
|
|
|
894
1035
|
// ../protocol/src/connect.ts
|
|
895
|
-
var ConnectRequest =
|
|
1036
|
+
var ConnectRequest = z5.strictObject({
|
|
896
1037
|
machineId: MachineId
|
|
897
1038
|
});
|
|
898
|
-
var ConnectResponse =
|
|
899
|
-
runnerId:
|
|
900
|
-
channel:
|
|
901
|
-
heartbeatIntervalMs:
|
|
902
|
-
leaseMs:
|
|
903
|
-
serverTime:
|
|
1039
|
+
var ConnectResponse = z5.looseObject({
|
|
1040
|
+
runnerId: z5.number().int().positive(),
|
|
1041
|
+
channel: z5.string().regex(/^runner:\d+$/),
|
|
1042
|
+
heartbeatIntervalMs: z5.number().int().positive(),
|
|
1043
|
+
leaseMs: z5.number().int().positive(),
|
|
1044
|
+
serverTime: z5.number().int()
|
|
904
1045
|
});
|
|
905
1046
|
var MACHINE_IN_USE = "MACHINE_IN_USE";
|
|
906
1047
|
var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
|
|
907
|
-
var RunnerFrame =
|
|
1048
|
+
var RunnerFrame = z5.object({ event: z5.enum(RUNNER_FRAME_EVENTS) });
|
|
908
1049
|
function runnerChannel(runnerId) {
|
|
909
1050
|
return `runner:${runnerId}`;
|
|
910
1051
|
}
|
|
911
1052
|
// ../protocol/src/events.ts
|
|
912
|
-
import { z as
|
|
1053
|
+
import { z as z6 } from "zod";
|
|
913
1054
|
var MAX_EVENTS_PER_BATCH = 100;
|
|
914
1055
|
var MAX_EVENTS_BATCH_BYTES = 256 * 1024;
|
|
915
1056
|
var NOT_YOUR_WORK = "NOT_YOUR_WORK";
|
|
916
1057
|
var SEQ_REGRESSION = "SEQ_REGRESSION";
|
|
917
|
-
var body =
|
|
1058
|
+
var body = z6.string().trim().min(1).max(64 * 1024);
|
|
918
1059
|
var envelope = {
|
|
919
|
-
eventId:
|
|
920
|
-
seq:
|
|
921
|
-
at:
|
|
1060
|
+
eventId: z6.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
|
|
1061
|
+
seq: z6.number().int().positive().describe("Per work item, from 1, never going backwards"),
|
|
1062
|
+
at: z6.number().int().describe("When the daemon saw it, epoch ms")
|
|
922
1063
|
};
|
|
923
|
-
var SelectMetadata =
|
|
924
|
-
options:
|
|
925
|
-
label:
|
|
926
|
-
value:
|
|
1064
|
+
var SelectMetadata = z6.strictObject({
|
|
1065
|
+
options: z6.array(z6.strictObject({
|
|
1066
|
+
label: z6.string().trim().min(1).max(255).optional(),
|
|
1067
|
+
value: z6.string().trim().min(1).max(1024)
|
|
927
1068
|
})).min(1).max(25)
|
|
928
1069
|
});
|
|
929
|
-
var AuthProvider =
|
|
930
|
-
var AuthMetadata =
|
|
931
|
-
url:
|
|
1070
|
+
var AuthProvider = z6.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
|
|
1071
|
+
var AuthMetadata = z6.strictObject({
|
|
1072
|
+
url: z6.url().optional(),
|
|
932
1073
|
provider: AuthProvider.optional(),
|
|
933
|
-
userId:
|
|
934
|
-
providerName:
|
|
1074
|
+
userId: z6.string().max(255).optional(),
|
|
1075
|
+
providerName: z6.string().max(255).optional()
|
|
935
1076
|
}).refine((m) => m.url !== undefined || m.provider !== undefined, {
|
|
936
1077
|
message: "an auth elicitation needs a url or a provider"
|
|
937
1078
|
});
|
|
938
|
-
var ThoughtEvent =
|
|
1079
|
+
var ThoughtEvent = z6.strictObject({
|
|
939
1080
|
...envelope,
|
|
940
|
-
type:
|
|
1081
|
+
type: z6.literal("thought"),
|
|
941
1082
|
body,
|
|
942
|
-
ephemeral:
|
|
1083
|
+
ephemeral: z6.boolean().default(false)
|
|
943
1084
|
});
|
|
944
|
-
var ActionEvent =
|
|
1085
|
+
var ActionEvent = z6.strictObject({
|
|
945
1086
|
...envelope,
|
|
946
|
-
type:
|
|
947
|
-
action:
|
|
948
|
-
parameter:
|
|
949
|
-
result:
|
|
950
|
-
ephemeral:
|
|
1087
|
+
type: z6.literal("action"),
|
|
1088
|
+
action: z6.string().trim().min(1).max(255),
|
|
1089
|
+
parameter: z6.string().trim().min(1).max(4096),
|
|
1090
|
+
result: z6.string().max(64 * 1024).optional(),
|
|
1091
|
+
ephemeral: z6.boolean().default(false)
|
|
951
1092
|
});
|
|
952
|
-
var ElicitationEvent =
|
|
1093
|
+
var ElicitationEvent = z6.strictObject({
|
|
953
1094
|
...envelope,
|
|
954
|
-
type:
|
|
1095
|
+
type: z6.literal("elicitation"),
|
|
955
1096
|
body,
|
|
956
|
-
signal:
|
|
957
|
-
signalMetadata:
|
|
1097
|
+
signal: z6.enum(["select", "auth"]).optional(),
|
|
1098
|
+
signalMetadata: z6.record(z6.string(), z6.unknown()).optional()
|
|
958
1099
|
}).superRefine((event, ctx) => {
|
|
959
1100
|
if (!event.signal) {
|
|
960
1101
|
if (event.signalMetadata) {
|
|
@@ -976,14 +1117,14 @@ var ElicitationEvent = z5.strictObject({
|
|
|
976
1117
|
});
|
|
977
1118
|
}
|
|
978
1119
|
});
|
|
979
|
-
var ResponseEvent =
|
|
1120
|
+
var ResponseEvent = z6.strictObject({
|
|
980
1121
|
...envelope,
|
|
981
|
-
type:
|
|
1122
|
+
type: z6.literal("response"),
|
|
982
1123
|
body
|
|
983
1124
|
});
|
|
984
|
-
var ErrorEvent =
|
|
1125
|
+
var ErrorEvent = z6.strictObject({
|
|
985
1126
|
...envelope,
|
|
986
|
-
type:
|
|
1127
|
+
type: z6.literal("error"),
|
|
987
1128
|
body
|
|
988
1129
|
});
|
|
989
1130
|
var PLAN_ENTRY_STATUSES = [
|
|
@@ -992,33 +1133,33 @@ var PLAN_ENTRY_STATUSES = [
|
|
|
992
1133
|
"completed",
|
|
993
1134
|
"canceled"
|
|
994
1135
|
];
|
|
995
|
-
var PlanEvent =
|
|
1136
|
+
var PlanEvent = z6.strictObject({
|
|
996
1137
|
...envelope,
|
|
997
|
-
type:
|
|
998
|
-
entries:
|
|
999
|
-
content:
|
|
1000
|
-
status:
|
|
1138
|
+
type: z6.literal("plan"),
|
|
1139
|
+
entries: z6.array(z6.strictObject({
|
|
1140
|
+
content: z6.string().trim().min(1).max(1024),
|
|
1141
|
+
status: z6.enum(PLAN_ENTRY_STATUSES)
|
|
1001
1142
|
})).max(100)
|
|
1002
1143
|
});
|
|
1003
|
-
var SummaryEvent =
|
|
1144
|
+
var SummaryEvent = z6.strictObject({
|
|
1004
1145
|
...envelope,
|
|
1005
|
-
type:
|
|
1006
|
-
title:
|
|
1146
|
+
type: z6.literal("summary"),
|
|
1147
|
+
title: z6.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
|
|
1007
1148
|
});
|
|
1008
|
-
var ExternalUrlEvent =
|
|
1149
|
+
var ExternalUrlEvent = z6.strictObject({
|
|
1009
1150
|
...envelope,
|
|
1010
|
-
type:
|
|
1011
|
-
label:
|
|
1012
|
-
url:
|
|
1151
|
+
type: z6.literal("external_url"),
|
|
1152
|
+
label: z6.string().trim().min(1).max(255),
|
|
1153
|
+
url: z6.url()
|
|
1013
1154
|
});
|
|
1014
|
-
var UsageEvent =
|
|
1155
|
+
var UsageEvent = z6.strictObject({
|
|
1015
1156
|
...envelope,
|
|
1016
|
-
type:
|
|
1017
|
-
inputTokens:
|
|
1018
|
-
outputTokens:
|
|
1019
|
-
costUsd:
|
|
1020
|
-
contextUsed:
|
|
1021
|
-
contextSize:
|
|
1157
|
+
type: z6.literal("usage"),
|
|
1158
|
+
inputTokens: z6.number().int().min(0).optional(),
|
|
1159
|
+
outputTokens: z6.number().int().min(0).optional(),
|
|
1160
|
+
costUsd: z6.number().min(0).optional(),
|
|
1161
|
+
contextUsed: z6.number().int().min(0).optional(),
|
|
1162
|
+
contextSize: z6.number().int().min(0).optional()
|
|
1022
1163
|
});
|
|
1023
1164
|
var STATUS_EVENT_STATES = [
|
|
1024
1165
|
"starting",
|
|
@@ -1026,12 +1167,12 @@ var STATUS_EVENT_STATES = [
|
|
|
1026
1167
|
"waiting_permission",
|
|
1027
1168
|
"finishing"
|
|
1028
1169
|
];
|
|
1029
|
-
var StatusEvent =
|
|
1170
|
+
var StatusEvent = z6.strictObject({
|
|
1030
1171
|
...envelope,
|
|
1031
|
-
type:
|
|
1032
|
-
state:
|
|
1172
|
+
type: z6.literal("status"),
|
|
1173
|
+
state: z6.enum(STATUS_EVENT_STATES)
|
|
1033
1174
|
});
|
|
1034
|
-
var SessionEvent =
|
|
1175
|
+
var SessionEvent = z6.discriminatedUnion("type", [
|
|
1035
1176
|
ThoughtEvent,
|
|
1036
1177
|
ActionEvent,
|
|
1037
1178
|
ElicitationEvent,
|
|
@@ -1043,16 +1184,302 @@ var SessionEvent = z5.discriminatedUnion("type", [
|
|
|
1043
1184
|
UsageEvent,
|
|
1044
1185
|
StatusEvent
|
|
1045
1186
|
]);
|
|
1046
|
-
var EventsRequest =
|
|
1047
|
-
workItemId:
|
|
1048
|
-
events:
|
|
1187
|
+
var EventsRequest = z6.strictObject({
|
|
1188
|
+
workItemId: z6.number().int().positive(),
|
|
1189
|
+
events: z6.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
|
|
1049
1190
|
});
|
|
1050
|
-
var EventsResponse =
|
|
1051
|
-
accepted:
|
|
1052
|
-
duplicates:
|
|
1053
|
-
dropped:
|
|
1054
|
-
cancelRequested:
|
|
1191
|
+
var EventsResponse = z6.looseObject({
|
|
1192
|
+
accepted: z6.number().int().min(0),
|
|
1193
|
+
duplicates: z6.number().int().min(0),
|
|
1194
|
+
dropped: z6.number().int().min(0),
|
|
1195
|
+
cancelRequested: z6.boolean()
|
|
1055
1196
|
});
|
|
1197
|
+
// ../protocol/src/otlp.ts
|
|
1198
|
+
import protobuf from "protobufjs/light.js";
|
|
1199
|
+
var SPAN_KIND = {
|
|
1200
|
+
internal: 1,
|
|
1201
|
+
server: 2,
|
|
1202
|
+
client: 3,
|
|
1203
|
+
producer: 4,
|
|
1204
|
+
consumer: 5
|
|
1205
|
+
};
|
|
1206
|
+
var STATUS_CODE = { unset: 0, ok: 1, error: 2 };
|
|
1207
|
+
var TEMPORALITY = { delta: 1, cumulative: 2 };
|
|
1208
|
+
var SEVERITY = { debug: 5, info: 9, warn: 13, error: 17 };
|
|
1209
|
+
var rep = (type, id) => ({ rule: "repeated", type, id });
|
|
1210
|
+
var DESCRIPTOR = {
|
|
1211
|
+
nested: {
|
|
1212
|
+
AnyValue: {
|
|
1213
|
+
oneofs: {
|
|
1214
|
+
value: {
|
|
1215
|
+
oneof: [
|
|
1216
|
+
"stringValue",
|
|
1217
|
+
"boolValue",
|
|
1218
|
+
"intValue",
|
|
1219
|
+
"doubleValue",
|
|
1220
|
+
"arrayValue",
|
|
1221
|
+
"kvlistValue",
|
|
1222
|
+
"bytesValue"
|
|
1223
|
+
]
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
fields: {
|
|
1227
|
+
stringValue: { type: "string", id: 1 },
|
|
1228
|
+
boolValue: { type: "bool", id: 2 },
|
|
1229
|
+
intValue: { type: "int64", id: 3 },
|
|
1230
|
+
doubleValue: { type: "double", id: 4 },
|
|
1231
|
+
arrayValue: { type: "ArrayValue", id: 5 },
|
|
1232
|
+
kvlistValue: { type: "KeyValueList", id: 6 },
|
|
1233
|
+
bytesValue: { type: "bytes", id: 7 }
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
ArrayValue: { fields: { values: rep("AnyValue", 1) } },
|
|
1237
|
+
KeyValueList: { fields: { values: rep("KeyValue", 1) } },
|
|
1238
|
+
KeyValue: {
|
|
1239
|
+
fields: {
|
|
1240
|
+
key: { type: "string", id: 1 },
|
|
1241
|
+
value: { type: "AnyValue", id: 2 }
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
InstrumentationScope: {
|
|
1245
|
+
fields: {
|
|
1246
|
+
name: { type: "string", id: 1 },
|
|
1247
|
+
version: { type: "string", id: 2 },
|
|
1248
|
+
attributes: rep("KeyValue", 3),
|
|
1249
|
+
droppedAttributesCount: { type: "uint32", id: 4 }
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
Resource: {
|
|
1253
|
+
fields: {
|
|
1254
|
+
attributes: rep("KeyValue", 1),
|
|
1255
|
+
droppedAttributesCount: { type: "uint32", id: 2 }
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
ExportTraceServiceRequest: {
|
|
1259
|
+
fields: { resourceSpans: rep("ResourceSpans", 1) }
|
|
1260
|
+
},
|
|
1261
|
+
ResourceSpans: {
|
|
1262
|
+
fields: {
|
|
1263
|
+
resource: { type: "Resource", id: 1 },
|
|
1264
|
+
scopeSpans: rep("ScopeSpans", 2),
|
|
1265
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
ScopeSpans: {
|
|
1269
|
+
fields: {
|
|
1270
|
+
scope: { type: "InstrumentationScope", id: 1 },
|
|
1271
|
+
spans: rep("Span", 2),
|
|
1272
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
Span: {
|
|
1276
|
+
fields: {
|
|
1277
|
+
traceId: { type: "bytes", id: 1 },
|
|
1278
|
+
spanId: { type: "bytes", id: 2 },
|
|
1279
|
+
traceState: { type: "string", id: 3 },
|
|
1280
|
+
parentSpanId: { type: "bytes", id: 4 },
|
|
1281
|
+
flags: { type: "fixed32", id: 16 },
|
|
1282
|
+
name: { type: "string", id: 5 },
|
|
1283
|
+
kind: { type: "int32", id: 6 },
|
|
1284
|
+
startTimeUnixNano: { type: "fixed64", id: 7 },
|
|
1285
|
+
endTimeUnixNano: { type: "fixed64", id: 8 },
|
|
1286
|
+
attributes: rep("KeyValue", 9),
|
|
1287
|
+
droppedAttributesCount: { type: "uint32", id: 10 },
|
|
1288
|
+
events: rep("SpanEvent", 11),
|
|
1289
|
+
droppedEventsCount: { type: "uint32", id: 12 },
|
|
1290
|
+
links: rep("SpanLink", 13),
|
|
1291
|
+
droppedLinksCount: { type: "uint32", id: 14 },
|
|
1292
|
+
status: { type: "SpanStatus", id: 15 }
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
SpanEvent: {
|
|
1296
|
+
fields: {
|
|
1297
|
+
timeUnixNano: { type: "fixed64", id: 1 },
|
|
1298
|
+
name: { type: "string", id: 2 },
|
|
1299
|
+
attributes: rep("KeyValue", 3),
|
|
1300
|
+
droppedAttributesCount: { type: "uint32", id: 4 }
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
SpanLink: {
|
|
1304
|
+
fields: {
|
|
1305
|
+
traceId: { type: "bytes", id: 1 },
|
|
1306
|
+
spanId: { type: "bytes", id: 2 },
|
|
1307
|
+
traceState: { type: "string", id: 3 },
|
|
1308
|
+
attributes: rep("KeyValue", 4),
|
|
1309
|
+
droppedAttributesCount: { type: "uint32", id: 5 },
|
|
1310
|
+
flags: { type: "fixed32", id: 6 }
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
SpanStatus: {
|
|
1314
|
+
fields: {
|
|
1315
|
+
message: { type: "string", id: 2 },
|
|
1316
|
+
code: { type: "int32", id: 3 }
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
ExportLogsServiceRequest: {
|
|
1320
|
+
fields: { resourceLogs: rep("ResourceLogs", 1) }
|
|
1321
|
+
},
|
|
1322
|
+
ResourceLogs: {
|
|
1323
|
+
fields: {
|
|
1324
|
+
resource: { type: "Resource", id: 1 },
|
|
1325
|
+
scopeLogs: rep("ScopeLogs", 2),
|
|
1326
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
ScopeLogs: {
|
|
1330
|
+
fields: {
|
|
1331
|
+
scope: { type: "InstrumentationScope", id: 1 },
|
|
1332
|
+
logRecords: rep("LogRecord", 2),
|
|
1333
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
LogRecord: {
|
|
1337
|
+
fields: {
|
|
1338
|
+
timeUnixNano: { type: "fixed64", id: 1 },
|
|
1339
|
+
observedTimeUnixNano: { type: "fixed64", id: 11 },
|
|
1340
|
+
severityNumber: { type: "int32", id: 2 },
|
|
1341
|
+
severityText: { type: "string", id: 3 },
|
|
1342
|
+
body: { type: "AnyValue", id: 5 },
|
|
1343
|
+
attributes: rep("KeyValue", 6),
|
|
1344
|
+
droppedAttributesCount: { type: "uint32", id: 7 },
|
|
1345
|
+
flags: { type: "fixed32", id: 8 },
|
|
1346
|
+
traceId: { type: "bytes", id: 9 },
|
|
1347
|
+
spanId: { type: "bytes", id: 10 },
|
|
1348
|
+
eventName: { type: "string", id: 12 }
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
ExportMetricsServiceRequest: {
|
|
1352
|
+
fields: { resourceMetrics: rep("ResourceMetrics", 1) }
|
|
1353
|
+
},
|
|
1354
|
+
ResourceMetrics: {
|
|
1355
|
+
fields: {
|
|
1356
|
+
resource: { type: "Resource", id: 1 },
|
|
1357
|
+
scopeMetrics: rep("ScopeMetrics", 2),
|
|
1358
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
ScopeMetrics: {
|
|
1362
|
+
fields: {
|
|
1363
|
+
scope: { type: "InstrumentationScope", id: 1 },
|
|
1364
|
+
metrics: rep("Metric", 2),
|
|
1365
|
+
schemaUrl: { type: "string", id: 3 }
|
|
1366
|
+
}
|
|
1367
|
+
},
|
|
1368
|
+
Metric: {
|
|
1369
|
+
oneofs: { data: { oneof: ["gauge", "sum", "histogram"] } },
|
|
1370
|
+
fields: {
|
|
1371
|
+
name: { type: "string", id: 1 },
|
|
1372
|
+
description: { type: "string", id: 2 },
|
|
1373
|
+
unit: { type: "string", id: 3 },
|
|
1374
|
+
gauge: { type: "Gauge", id: 5 },
|
|
1375
|
+
sum: { type: "Sum", id: 7 },
|
|
1376
|
+
histogram: { type: "Histogram", id: 9 },
|
|
1377
|
+
metadata: rep("KeyValue", 12)
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
Gauge: { fields: { dataPoints: rep("NumberDataPoint", 1) } },
|
|
1381
|
+
Sum: {
|
|
1382
|
+
fields: {
|
|
1383
|
+
dataPoints: rep("NumberDataPoint", 1),
|
|
1384
|
+
aggregationTemporality: { type: "int32", id: 2 },
|
|
1385
|
+
isMonotonic: { type: "bool", id: 3 }
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
Histogram: {
|
|
1389
|
+
fields: {
|
|
1390
|
+
dataPoints: rep("HistogramDataPoint", 1),
|
|
1391
|
+
aggregationTemporality: { type: "int32", id: 2 }
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
NumberDataPoint: {
|
|
1395
|
+
oneofs: { value: { oneof: ["asDouble", "asInt"] } },
|
|
1396
|
+
fields: {
|
|
1397
|
+
attributes: rep("KeyValue", 7),
|
|
1398
|
+
startTimeUnixNano: { type: "fixed64", id: 2 },
|
|
1399
|
+
timeUnixNano: { type: "fixed64", id: 3 },
|
|
1400
|
+
asDouble: { type: "double", id: 4 },
|
|
1401
|
+
asInt: { type: "sfixed64", id: 6 },
|
|
1402
|
+
flags: { type: "uint32", id: 8 }
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
HistogramDataPoint: {
|
|
1406
|
+
fields: {
|
|
1407
|
+
attributes: rep("KeyValue", 9),
|
|
1408
|
+
startTimeUnixNano: { type: "fixed64", id: 2 },
|
|
1409
|
+
timeUnixNano: { type: "fixed64", id: 3 },
|
|
1410
|
+
count: { type: "fixed64", id: 4 },
|
|
1411
|
+
sum: { type: "double", id: 5, options: { proto3_optional: true } },
|
|
1412
|
+
bucketCounts: { ...rep("fixed64", 6), options: { packed: true } },
|
|
1413
|
+
explicitBounds: { ...rep("double", 7), options: { packed: true } },
|
|
1414
|
+
flags: { type: "uint32", id: 10 },
|
|
1415
|
+
min: { type: "double", id: 11, options: { proto3_optional: true } },
|
|
1416
|
+
max: { type: "double", id: 12, options: { proto3_optional: true } }
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
var root = protobuf.Root.fromJSON(DESCRIPTOR);
|
|
1422
|
+
var REQUEST_TYPES = {
|
|
1423
|
+
traces: root.lookupType("ExportTraceServiceRequest"),
|
|
1424
|
+
metrics: root.lookupType("ExportMetricsServiceRequest"),
|
|
1425
|
+
logs: root.lookupType("ExportLogsServiceRequest")
|
|
1426
|
+
};
|
|
1427
|
+
var HEX_ID_FIELDS = new Set(["traceId", "spanId", "parentSpanId"]);
|
|
1428
|
+
function toJson(value, key = "") {
|
|
1429
|
+
if (value instanceof Uint8Array) {
|
|
1430
|
+
return HEX_ID_FIELDS.has(key) ? Buffer.from(value).toString("hex") : Buffer.from(value).toString("base64");
|
|
1431
|
+
}
|
|
1432
|
+
if (Array.isArray(value))
|
|
1433
|
+
return value.map((v) => toJson(v, key));
|
|
1434
|
+
if (value && typeof value === "object") {
|
|
1435
|
+
const out = {};
|
|
1436
|
+
for (const [k, v] of Object.entries(value))
|
|
1437
|
+
out[k] = toJson(v, k);
|
|
1438
|
+
return out;
|
|
1439
|
+
}
|
|
1440
|
+
return value;
|
|
1441
|
+
}
|
|
1442
|
+
function decodeOtlpProtobuf(signal, bytes) {
|
|
1443
|
+
const type = REQUEST_TYPES[signal];
|
|
1444
|
+
const object = type.toObject(type.decode(bytes), {
|
|
1445
|
+
longs: String,
|
|
1446
|
+
defaults: false,
|
|
1447
|
+
arrays: false
|
|
1448
|
+
});
|
|
1449
|
+
return toJson(object);
|
|
1450
|
+
}
|
|
1451
|
+
function decodeOtlpBody(signal, bytes, contentType) {
|
|
1452
|
+
if ((contentType ?? "").toLowerCase().includes("json")) {
|
|
1453
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
1454
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1455
|
+
throw new Error("not an OTLP request");
|
|
1456
|
+
return parsed;
|
|
1457
|
+
}
|
|
1458
|
+
return decodeOtlpProtobuf(signal, bytes);
|
|
1459
|
+
}
|
|
1460
|
+
function signalOfPath(path) {
|
|
1461
|
+
const match = /\/v1\/(traces|metrics|logs)\/?$/.exec(path);
|
|
1462
|
+
return match ? match[1] : null;
|
|
1463
|
+
}
|
|
1464
|
+
function attribute(key, value) {
|
|
1465
|
+
if (value === null || value === undefined)
|
|
1466
|
+
return null;
|
|
1467
|
+
if (typeof value === "boolean")
|
|
1468
|
+
return { key, value: { boolValue: value } };
|
|
1469
|
+
if (typeof value === "number") {
|
|
1470
|
+
return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
|
|
1471
|
+
}
|
|
1472
|
+
return { key, value: { stringValue: value } };
|
|
1473
|
+
}
|
|
1474
|
+
function attributes(values) {
|
|
1475
|
+
return Object.entries(values).flatMap(([k, v]) => {
|
|
1476
|
+
const a = attribute(k, v);
|
|
1477
|
+
return a ? [a] : [];
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function unixNano(ms) {
|
|
1481
|
+
return (BigInt(Math.round(ms * 1000)) * 1000n).toString();
|
|
1482
|
+
}
|
|
1056
1483
|
// ../protocol/src/remote.ts
|
|
1057
1484
|
function normalizeRemote(remote) {
|
|
1058
1485
|
let rest = remote.trim();
|
|
@@ -1076,14 +1503,14 @@ function normalizeRemote(remote) {
|
|
|
1076
1503
|
return path ? `${host.toLowerCase()}/${path}` : host.toLowerCase();
|
|
1077
1504
|
}
|
|
1078
1505
|
// ../protocol/src/tools.ts
|
|
1079
|
-
import { z as
|
|
1080
|
-
var issueRef =
|
|
1081
|
-
var teamRef =
|
|
1082
|
-
var projectRef =
|
|
1083
|
-
var markdown = (what) =>
|
|
1084
|
-
var labels =
|
|
1085
|
-
var priority =
|
|
1086
|
-
var first =
|
|
1506
|
+
import { z as z7 } from "zod";
|
|
1507
|
+
var issueRef = z7.string().trim().min(1).max(100).describe("An issue identifier like ENG-123, or the issue's id");
|
|
1508
|
+
var teamRef = z7.string().trim().min(1).max(100).describe("A team key like ENG, or the team's id");
|
|
1509
|
+
var projectRef = z7.string().trim().min(1).max(255).describe("A project's id or its exact name");
|
|
1510
|
+
var markdown = (what) => z7.string().min(1).describe(`${what}, in Markdown. Mention people and issues by their Linear URLs, not @names; put long logs or diffs in a collapsible section (+++ Title \u2026 +++)`);
|
|
1511
|
+
var labels = z7.array(z7.string().trim().min(1).max(255)).max(50).describe("Label names or ids (see linear_list_labels); replaces the issue's labels");
|
|
1512
|
+
var priority = z7.number().int().min(0).max(4).describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
|
|
1513
|
+
var first = z7.number().int().min(1).max(25).optional().describe("How many to return, at most 25 (default 25)");
|
|
1087
1514
|
var ISSUE_STATE_TYPES = [
|
|
1088
1515
|
"triage",
|
|
1089
1516
|
"backlog",
|
|
@@ -1111,7 +1538,7 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1111
1538
|
roles: BOTH,
|
|
1112
1539
|
kind: "read",
|
|
1113
1540
|
description: "Read a Linear issue: title, description, state, priority, labels, assignee, delegate, parent, sub-issues, relations, project, team, and URL. Defaults to the issue you are working on. People are listed with their profile URLs; use those URLs to mention someone.",
|
|
1114
|
-
input:
|
|
1541
|
+
input: z7.strictObject({ issue: issueRef.optional() })
|
|
1115
1542
|
}),
|
|
1116
1543
|
tool({
|
|
1117
1544
|
name: "linear_list_comments",
|
|
@@ -1120,10 +1547,10 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1120
1547
|
roles: BOTH,
|
|
1121
1548
|
kind: "read",
|
|
1122
1549
|
description: "Read the comments on a Linear issue, oldest first, replies under the comment they answer. Defaults to the issue you are working on. Pass `after` (from the last page) to read further.",
|
|
1123
|
-
input:
|
|
1550
|
+
input: z7.strictObject({
|
|
1124
1551
|
issue: issueRef.optional(),
|
|
1125
1552
|
first,
|
|
1126
|
-
after:
|
|
1553
|
+
after: z7.string().trim().min(1).max(512).optional().describe("The cursor the previous page returned")
|
|
1127
1554
|
})
|
|
1128
1555
|
}),
|
|
1129
1556
|
tool({
|
|
@@ -1133,10 +1560,10 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1133
1560
|
roles: BOTH,
|
|
1134
1561
|
kind: "write",
|
|
1135
1562
|
description: "Post a comment on a Linear issue, as Hypotenuse. Your final answer already appears in Linear as a comment in this session's thread, so never use this to repeat it: use it to answer a question someone left elsewhere on the issue, or to note a blocker for someone who is not following this session. Pass `parentId` to reply in an existing thread.",
|
|
1136
|
-
input:
|
|
1563
|
+
input: z7.strictObject({
|
|
1137
1564
|
body: markdown("The comment"),
|
|
1138
1565
|
issue: issueRef.optional().describe("The issue to comment on; defaults to the issue you are working on"),
|
|
1139
|
-
parentId:
|
|
1566
|
+
parentId: z7.string().trim().min(1).max(100).optional().describe("The id of the comment to reply to")
|
|
1140
1567
|
})
|
|
1141
1568
|
}),
|
|
1142
1569
|
tool({
|
|
@@ -1146,8 +1573,8 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1146
1573
|
roles: BOTH,
|
|
1147
1574
|
kind: "write",
|
|
1148
1575
|
description: `Move a Linear issue to a workflow state, by the state's name ("In Review") or its type (triage, backlog, unstarted, started, completed, canceled). Defaults to the issue you are working on. A worker cannot complete or cancel an issue, and a manager can only where the project allows it: a person decides when work is done.`,
|
|
1149
|
-
input:
|
|
1150
|
-
state:
|
|
1576
|
+
input: z7.strictObject({
|
|
1577
|
+
state: z7.string().trim().min(1).max(255).describe("A state name, or a state type"),
|
|
1151
1578
|
issue: issueRef.optional()
|
|
1152
1579
|
})
|
|
1153
1580
|
}),
|
|
@@ -1158,10 +1585,10 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1158
1585
|
roles: BOTH,
|
|
1159
1586
|
kind: "write",
|
|
1160
1587
|
description: "Replace this session's plan in Linear with the whole list of steps. Only needed if your own plan updates do not already appear there.",
|
|
1161
|
-
input:
|
|
1162
|
-
steps:
|
|
1163
|
-
content:
|
|
1164
|
-
status:
|
|
1588
|
+
input: z7.strictObject({
|
|
1589
|
+
steps: z7.array(z7.strictObject({
|
|
1590
|
+
content: z7.string().trim().min(1).max(1024).describe("The step"),
|
|
1591
|
+
status: z7.enum(PLAN_STEP_STATUSES).describe("pending, inProgress, completed, or canceled")
|
|
1165
1592
|
})).min(1).max(100).describe("Every step, in order")
|
|
1166
1593
|
})
|
|
1167
1594
|
}),
|
|
@@ -1172,11 +1599,11 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1172
1599
|
roles: BOTH,
|
|
1173
1600
|
kind: "write",
|
|
1174
1601
|
description: "Ask the person a question in Linear when you need their decision to go on. With `options`, Linear shows them as choices (a GitHub repository URL as a value renders as that repository). Their answer arrives as your next prompt: after calling this, end your turn at once without a further message.",
|
|
1175
|
-
input:
|
|
1176
|
-
question:
|
|
1177
|
-
options:
|
|
1178
|
-
label:
|
|
1179
|
-
value:
|
|
1602
|
+
input: z7.strictObject({
|
|
1603
|
+
question: z7.string().trim().min(1).max(1e4).describe("The question"),
|
|
1604
|
+
options: z7.array(z7.strictObject({
|
|
1605
|
+
label: z7.string().trim().min(1).max(255).describe("What the person sees"),
|
|
1606
|
+
value: z7.string().trim().min(1).max(2048).optional().describe("What you get back; defaults to the label")
|
|
1180
1607
|
})).min(2).max(10).optional().describe("2 to 10 choices; omit for a free-text answer")
|
|
1181
1608
|
})
|
|
1182
1609
|
}),
|
|
@@ -1187,8 +1614,8 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1187
1614
|
roles: BOTH,
|
|
1188
1615
|
kind: "write",
|
|
1189
1616
|
description: "Link the pull request you opened to this session in Linear. Call it as soon as the pull request exists. It must be a pull request on this session's repository.",
|
|
1190
|
-
input:
|
|
1191
|
-
url:
|
|
1617
|
+
input: z7.strictObject({
|
|
1618
|
+
url: z7.url().max(2048).describe("The pull request's URL")
|
|
1192
1619
|
})
|
|
1193
1620
|
}),
|
|
1194
1621
|
tool({
|
|
@@ -1198,11 +1625,11 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1198
1625
|
roles: BOTH,
|
|
1199
1626
|
kind: "read",
|
|
1200
1627
|
description: "Search Linear issues by text in their title or description, narrowed by team, state type, label, or project. At most 25 results.",
|
|
1201
|
-
input:
|
|
1202
|
-
query:
|
|
1628
|
+
input: z7.strictObject({
|
|
1629
|
+
query: z7.string().trim().min(1).max(255).optional().describe("Text to find in titles and descriptions"),
|
|
1203
1630
|
team: teamRef.optional(),
|
|
1204
|
-
stateType:
|
|
1205
|
-
label:
|
|
1631
|
+
stateType: z7.enum(ISSUE_STATE_TYPES).optional().describe("Only issues in states of this type"),
|
|
1632
|
+
label: z7.string().trim().min(1).max(255).optional().describe("Only issues with this label name"),
|
|
1206
1633
|
project: projectRef.optional(),
|
|
1207
1634
|
first
|
|
1208
1635
|
})
|
|
@@ -1214,8 +1641,8 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1214
1641
|
roles: MANAGER,
|
|
1215
1642
|
kind: "write",
|
|
1216
1643
|
description: "Create a Linear issue. Defaults to the team of the issue you are working on. Never assigns anyone: hand work to a worker with hypotenuse_dispatch.",
|
|
1217
|
-
input:
|
|
1218
|
-
title:
|
|
1644
|
+
input: z7.strictObject({
|
|
1645
|
+
title: z7.string().trim().min(1).max(255).describe("The title"),
|
|
1219
1646
|
description: markdown("The description").optional(),
|
|
1220
1647
|
team: teamRef.optional(),
|
|
1221
1648
|
labels: labels.optional(),
|
|
@@ -1231,14 +1658,14 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1231
1658
|
roles: MANAGER,
|
|
1232
1659
|
kind: "write",
|
|
1233
1660
|
description: "Edit a Linear issue you manage: its title, description, labels, priority, project, or estimate. It cannot change the assignee.",
|
|
1234
|
-
input:
|
|
1661
|
+
input: z7.strictObject({
|
|
1235
1662
|
issue: issueRef,
|
|
1236
|
-
title:
|
|
1663
|
+
title: z7.string().trim().min(1).max(255).optional().describe("The title"),
|
|
1237
1664
|
description: markdown("The description").optional(),
|
|
1238
1665
|
labels: labels.optional(),
|
|
1239
1666
|
priority: priority.optional(),
|
|
1240
1667
|
project: projectRef.optional(),
|
|
1241
|
-
estimate:
|
|
1668
|
+
estimate: z7.number().int().min(0).max(100).optional().describe("The estimate, in the team's points")
|
|
1242
1669
|
})
|
|
1243
1670
|
}),
|
|
1244
1671
|
tool({
|
|
@@ -1248,9 +1675,9 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1248
1675
|
roles: MANAGER,
|
|
1249
1676
|
kind: "write",
|
|
1250
1677
|
description: "Create a sub-issue under an issue you manage \u2014 how a plan is broken into tickets. It goes in the parent's team unless you name another.",
|
|
1251
|
-
input:
|
|
1678
|
+
input: z7.strictObject({
|
|
1252
1679
|
parent: issueRef.describe("The parent issue"),
|
|
1253
|
-
title:
|
|
1680
|
+
title: z7.string().trim().min(1).max(255).describe("The title"),
|
|
1254
1681
|
description: markdown("The description").optional(),
|
|
1255
1682
|
team: teamRef.optional(),
|
|
1256
1683
|
labels: labels.optional(),
|
|
@@ -1265,10 +1692,10 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1265
1692
|
roles: MANAGER,
|
|
1266
1693
|
kind: "write",
|
|
1267
1694
|
description: "Relate two Linear issues: `blocks` (issue blocks relatedIssue), `related`, or `duplicate` (issue duplicates relatedIssue). At least one of them must be an issue you manage.",
|
|
1268
|
-
input:
|
|
1695
|
+
input: z7.strictObject({
|
|
1269
1696
|
issue: issueRef,
|
|
1270
1697
|
relatedIssue: issueRef.describe("The other issue"),
|
|
1271
|
-
type:
|
|
1698
|
+
type: z7.enum(["blocks", "related", "duplicate"]).describe("blocks, related, or duplicate")
|
|
1272
1699
|
})
|
|
1273
1700
|
}),
|
|
1274
1701
|
tool({
|
|
@@ -1278,7 +1705,7 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1278
1705
|
roles: MANAGER,
|
|
1279
1706
|
kind: "read",
|
|
1280
1707
|
description: "Read a Linear project: its name, summary, content (the PRD), status, lead, how many issues are in each state, and its documents. Defaults to the project of the issue you are working on.",
|
|
1281
|
-
input:
|
|
1708
|
+
input: z7.strictObject({ project: projectRef.optional() })
|
|
1282
1709
|
}),
|
|
1283
1710
|
tool({
|
|
1284
1711
|
name: "linear_update_project",
|
|
@@ -1287,11 +1714,11 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1287
1714
|
roles: MANAGER,
|
|
1288
1715
|
kind: "write",
|
|
1289
1716
|
description: "Write a Linear project's content (the PRD, in Markdown), its one-line summary, or its status. Only the project of the issue you are working on. It never changes the lead or the members.",
|
|
1290
|
-
input:
|
|
1717
|
+
input: z7.strictObject({
|
|
1291
1718
|
project: projectRef.optional(),
|
|
1292
1719
|
content: markdown("The project's content (the PRD)").optional(),
|
|
1293
|
-
summary:
|
|
1294
|
-
status:
|
|
1720
|
+
summary: z7.string().trim().min(1).max(255).optional().describe("The one-line summary shown under the project's name"),
|
|
1721
|
+
status: z7.string().trim().min(1).max(255).optional().describe("A project status name, or its type (backlog, planned, started, paused, completed, canceled)")
|
|
1295
1722
|
})
|
|
1296
1723
|
}),
|
|
1297
1724
|
tool({
|
|
@@ -1301,8 +1728,8 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1301
1728
|
roles: MANAGER,
|
|
1302
1729
|
kind: "write",
|
|
1303
1730
|
description: "Create a Linear document attached to a project or an issue you manage. Defaults to the project of the issue you are working on.",
|
|
1304
|
-
input:
|
|
1305
|
-
title:
|
|
1731
|
+
input: z7.strictObject({
|
|
1732
|
+
title: z7.string().trim().min(1).max(255).describe("The title"),
|
|
1306
1733
|
content: markdown("The document"),
|
|
1307
1734
|
project: projectRef.optional(),
|
|
1308
1735
|
issue: issueRef.optional().describe("Attach it to this issue instead of a project")
|
|
@@ -1315,10 +1742,10 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1315
1742
|
roles: MANAGER,
|
|
1316
1743
|
kind: "write",
|
|
1317
1744
|
description: "Replace a Linear document's content (and optionally its title): one this session created, or one attached to the project of the issue you are working on.",
|
|
1318
|
-
input:
|
|
1319
|
-
document:
|
|
1745
|
+
input: z7.strictObject({
|
|
1746
|
+
document: z7.string().trim().min(1).max(100).describe("The document's id"),
|
|
1320
1747
|
content: markdown("The document's new content"),
|
|
1321
|
-
title:
|
|
1748
|
+
title: z7.string().trim().min(1).max(255).optional().describe("A new title")
|
|
1322
1749
|
})
|
|
1323
1750
|
}),
|
|
1324
1751
|
tool({
|
|
@@ -1328,7 +1755,7 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1328
1755
|
roles: MANAGER,
|
|
1329
1756
|
kind: "read",
|
|
1330
1757
|
description: "List the labels you can put on a team's issues (the team's own and the workspace's). Defaults to the team of the issue you are working on.",
|
|
1331
|
-
input:
|
|
1758
|
+
input: z7.strictObject({ team: teamRef.optional() })
|
|
1332
1759
|
}),
|
|
1333
1760
|
tool({
|
|
1334
1761
|
name: "hypotenuse_dispatch",
|
|
@@ -1337,11 +1764,11 @@ var HYPOTENUSE_TOOLS = [
|
|
|
1337
1764
|
roles: MANAGER,
|
|
1338
1765
|
kind: "write",
|
|
1339
1766
|
description: 'Hand an issue you manage to a Hypotenuse worker: it is delegated to Hypotenuse and a worker session starts on it on whichever runner can take it. Only dispatch an issue whose acceptance criteria are written, one call per issue. When the worker finishes, you get a review turn with its result: accept it, request changes with mode "changes" and specific instructions, or file follow-up issues. Workers cannot dispatch, you cannot dispatch the issue you are working on, and you may have only a few dispatched issues in progress at once.',
|
|
1340
|
-
input:
|
|
1767
|
+
input: z7.strictObject({
|
|
1341
1768
|
issue: issueRef,
|
|
1342
|
-
instructions:
|
|
1343
|
-
agentPreference:
|
|
1344
|
-
mode:
|
|
1769
|
+
instructions: z7.string().trim().min(1).max(8192).optional().describe('Context for the worker that does not belong in the issue; for mode "changes", exactly what to change'),
|
|
1770
|
+
agentPreference: z7.string().trim().min(1).max(64).optional().describe("An agent id the worker's runner should prefer (a preference, not a requirement)"),
|
|
1771
|
+
mode: z7.enum(["new", "changes"]).optional().describe("new (default): start a worker on the issue. changes: send a finished worker back to the same session and branch with your instructions")
|
|
1345
1772
|
})
|
|
1346
1773
|
})
|
|
1347
1774
|
];
|
|
@@ -1351,10 +1778,10 @@ function toolsForRole(role) {
|
|
|
1351
1778
|
function toolByName(name) {
|
|
1352
1779
|
return HYPOTENUSE_TOOLS.find((t) => t.name === name);
|
|
1353
1780
|
}
|
|
1354
|
-
var ToolResult =
|
|
1355
|
-
result:
|
|
1356
|
-
text:
|
|
1357
|
-
data:
|
|
1781
|
+
var ToolResult = z7.looseObject({
|
|
1782
|
+
result: z7.looseObject({
|
|
1783
|
+
text: z7.string(),
|
|
1784
|
+
data: z7.record(z7.string(), z7.unknown())
|
|
1358
1785
|
})
|
|
1359
1786
|
});
|
|
1360
1787
|
var TOOL_RESULT_MAX_BYTES = 16 * 1024;
|
|
@@ -1364,98 +1791,99 @@ var PROTOCOL_VERSION = 1;
|
|
|
1364
1791
|
var PROTOCOL_HEADER = "X-Hypotenuse-Protocol";
|
|
1365
1792
|
var PROTOCOL_UNSUPPORTED = "PROTOCOL_UNSUPPORTED";
|
|
1366
1793
|
// ../protocol/src/work.ts
|
|
1367
|
-
import { z as
|
|
1368
|
-
var ClaimRequest =
|
|
1369
|
-
workItemIds:
|
|
1794
|
+
import { z as z8 } from "zod";
|
|
1795
|
+
var ClaimRequest = z8.strictObject({
|
|
1796
|
+
workItemIds: z8.array(z8.number().int().positive()).max(100).optional().describe("Claim only these work items; omitted means any offer")
|
|
1370
1797
|
});
|
|
1371
|
-
var AssignmentRepository =
|
|
1372
|
-
remote:
|
|
1373
|
-
baseBranch:
|
|
1798
|
+
var AssignmentRepository = z8.looseObject({
|
|
1799
|
+
remote: z8.string(),
|
|
1800
|
+
baseBranch: z8.string().nullable()
|
|
1374
1801
|
});
|
|
1375
|
-
var AssignmentIssue =
|
|
1376
|
-
id:
|
|
1377
|
-
identifier:
|
|
1378
|
-
title:
|
|
1379
|
-
description:
|
|
1380
|
-
url:
|
|
1381
|
-
teamKey:
|
|
1382
|
-
labels:
|
|
1383
|
-
state:
|
|
1384
|
-
parent:
|
|
1385
|
-
project:
|
|
1802
|
+
var AssignmentIssue = z8.looseObject({
|
|
1803
|
+
id: z8.string(),
|
|
1804
|
+
identifier: z8.string().nullable(),
|
|
1805
|
+
title: z8.string().nullable(),
|
|
1806
|
+
description: z8.string().nullable(),
|
|
1807
|
+
url: z8.string().nullable(),
|
|
1808
|
+
teamKey: z8.string().nullable(),
|
|
1809
|
+
labels: z8.array(z8.string()),
|
|
1810
|
+
state: z8.looseObject({ name: z8.string(), type: z8.string() }).nullable(),
|
|
1811
|
+
parent: z8.looseObject({ id: z8.string() }).nullable(),
|
|
1812
|
+
project: z8.looseObject({ id: z8.string(), name: z8.string() }).nullable()
|
|
1386
1813
|
});
|
|
1387
|
-
var AssignmentGuidance =
|
|
1388
|
-
origin:
|
|
1389
|
-
teamKey:
|
|
1390
|
-
body:
|
|
1814
|
+
var AssignmentGuidance = z8.looseObject({
|
|
1815
|
+
origin: z8.enum(["team", "workspace"]),
|
|
1816
|
+
teamKey: z8.string().nullable().optional(),
|
|
1817
|
+
body: z8.string()
|
|
1391
1818
|
});
|
|
1392
|
-
var HistoryEntry =
|
|
1393
|
-
seq:
|
|
1394
|
-
activityId:
|
|
1395
|
-
direction:
|
|
1396
|
-
type:
|
|
1397
|
-
body:
|
|
1398
|
-
signal:
|
|
1819
|
+
var HistoryEntry = z8.looseObject({
|
|
1820
|
+
seq: z8.number().int(),
|
|
1821
|
+
activityId: z8.number().int(),
|
|
1822
|
+
direction: z8.enum(["inbound", "outbound"]),
|
|
1823
|
+
type: z8.string(),
|
|
1824
|
+
body: z8.string().nullable(),
|
|
1825
|
+
signal: z8.string().nullable()
|
|
1399
1826
|
});
|
|
1400
|
-
var PromptEntry =
|
|
1401
|
-
activityId:
|
|
1402
|
-
body:
|
|
1827
|
+
var PromptEntry = z8.looseObject({
|
|
1828
|
+
activityId: z8.number().int(),
|
|
1829
|
+
body: z8.string()
|
|
1403
1830
|
});
|
|
1404
|
-
var AssignmentLink =
|
|
1405
|
-
label:
|
|
1406
|
-
url:
|
|
1831
|
+
var AssignmentLink = z8.looseObject({
|
|
1832
|
+
label: z8.string(),
|
|
1833
|
+
url: z8.string()
|
|
1407
1834
|
});
|
|
1408
1835
|
var DISPATCH_MODES = ["new", "changes"];
|
|
1409
|
-
var AssignmentDispatch =
|
|
1410
|
-
mode:
|
|
1411
|
-
reviewRound:
|
|
1412
|
-
instructions:
|
|
1413
|
-
agentPreference:
|
|
1414
|
-
parent:
|
|
1415
|
-
agentSessionId:
|
|
1416
|
-
issueIdentifier:
|
|
1417
|
-
issueUrl:
|
|
1836
|
+
var AssignmentDispatch = z8.looseObject({
|
|
1837
|
+
mode: z8.enum(DISPATCH_MODES),
|
|
1838
|
+
reviewRound: z8.number().int().min(0),
|
|
1839
|
+
instructions: z8.string().nullable().describe("Extra context from the planning agent; for `changes`, what to change"),
|
|
1840
|
+
agentPreference: z8.string().nullable().describe("An [agents] id the planning agent prefers; a preference only"),
|
|
1841
|
+
parent: z8.looseObject({
|
|
1842
|
+
agentSessionId: z8.number().int().positive(),
|
|
1843
|
+
issueIdentifier: z8.string().nullable(),
|
|
1844
|
+
issueUrl: z8.string().nullable()
|
|
1418
1845
|
})
|
|
1419
1846
|
});
|
|
1420
1847
|
var REVIEW_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
1421
|
-
var AssignmentReview =
|
|
1422
|
-
childAgentSessionId:
|
|
1423
|
-
childIssueIdentifier:
|
|
1424
|
-
childIssueUrl:
|
|
1425
|
-
reviewRound:
|
|
1426
|
-
outcome:
|
|
1427
|
-
finalResponse:
|
|
1428
|
-
plan:
|
|
1429
|
-
pullRequests:
|
|
1848
|
+
var AssignmentReview = z8.looseObject({
|
|
1849
|
+
childAgentSessionId: z8.number().int().positive(),
|
|
1850
|
+
childIssueIdentifier: z8.string().nullable(),
|
|
1851
|
+
childIssueUrl: z8.string().nullable(),
|
|
1852
|
+
reviewRound: z8.number().int().min(0),
|
|
1853
|
+
outcome: z8.enum(REVIEW_OUTCOMES),
|
|
1854
|
+
finalResponse: z8.string().nullable().describe("The body of the worker's last response or error"),
|
|
1855
|
+
plan: z8.array(z8.looseObject({ content: z8.string(), status: z8.string() })).nullable(),
|
|
1856
|
+
pullRequests: z8.array(AssignmentLink)
|
|
1430
1857
|
});
|
|
1431
|
-
var WorkAssignment =
|
|
1432
|
-
workItemId:
|
|
1433
|
-
agentSessionId:
|
|
1434
|
-
type:
|
|
1435
|
-
externalSessionId:
|
|
1436
|
-
externalSessionUrl:
|
|
1437
|
-
dashboardUrl:
|
|
1438
|
-
kind:
|
|
1858
|
+
var WorkAssignment = z8.looseObject({
|
|
1859
|
+
workItemId: z8.number().int().positive(),
|
|
1860
|
+
agentSessionId: z8.number().int().positive(),
|
|
1861
|
+
type: z8.string(),
|
|
1862
|
+
externalSessionId: z8.string(),
|
|
1863
|
+
externalSessionUrl: z8.string().nullable(),
|
|
1864
|
+
dashboardUrl: z8.string(),
|
|
1865
|
+
kind: z8.enum(OFFER_KINDS),
|
|
1439
1866
|
role: RunnerRole,
|
|
1440
|
-
agentId:
|
|
1441
|
-
fresh:
|
|
1867
|
+
agentId: z8.string().nullable(),
|
|
1868
|
+
fresh: z8.boolean(),
|
|
1442
1869
|
repository: AssignmentRepository.nullable(),
|
|
1443
|
-
branch:
|
|
1870
|
+
branch: z8.string().nullable(),
|
|
1444
1871
|
issue: AssignmentIssue,
|
|
1445
|
-
instruction:
|
|
1446
|
-
promptContext:
|
|
1447
|
-
guidance:
|
|
1448
|
-
previousComments:
|
|
1449
|
-
history:
|
|
1450
|
-
prompt:
|
|
1451
|
-
origin:
|
|
1452
|
-
creator:
|
|
1453
|
-
humanDelegated:
|
|
1454
|
-
leaseExpiresAt:
|
|
1872
|
+
instruction: z8.string().nullable().describe("What the mention said, a matched role prefix stripped; null for a delegation"),
|
|
1873
|
+
promptContext: z8.string().nullable(),
|
|
1874
|
+
guidance: z8.array(AssignmentGuidance),
|
|
1875
|
+
previousComments: z8.array(z8.record(z8.string(), z8.unknown())).nullable(),
|
|
1876
|
+
history: z8.array(HistoryEntry),
|
|
1877
|
+
prompt: z8.array(PromptEntry).nullable(),
|
|
1878
|
+
origin: z8.string(),
|
|
1879
|
+
creator: z8.looseObject({ name: z8.string().nullable(), email: z8.string().nullable() }).nullable().optional().describe("Who started the session; null when an automation did"),
|
|
1880
|
+
humanDelegated: z8.boolean(),
|
|
1881
|
+
leaseExpiresAt: z8.number().int(),
|
|
1455
1882
|
dispatch: AssignmentDispatch.nullable().optional().describe("Set when a manager session dispatched this worker turn"),
|
|
1456
|
-
review: AssignmentReview.nullable().optional().describe("Set on a manager's review turn: the worker result to review")
|
|
1883
|
+
review: AssignmentReview.nullable().optional().describe("Set on a manager's review turn: the worker result to review"),
|
|
1884
|
+
telemetry: ClaimTelemetry.nullable().optional().describe("The turn's trace and what to collect for the project's exporters; null when nothing is exported")
|
|
1457
1885
|
});
|
|
1458
|
-
var ClaimResponse =
|
|
1886
|
+
var ClaimResponse = z8.looseObject({
|
|
1459
1887
|
assignment: WorkAssignment.nullable()
|
|
1460
1888
|
});
|
|
1461
1889
|
// src/config/resolve.ts
|
|
@@ -1625,7 +2053,7 @@ function objectKeys(schema) {
|
|
|
1625
2053
|
|
|
1626
2054
|
// src/config/schema.ts
|
|
1627
2055
|
import { cpus, hostname } from "os";
|
|
1628
|
-
import { z as
|
|
2056
|
+
import { z as z9 } from "zod";
|
|
1629
2057
|
|
|
1630
2058
|
// src/config/durations.ts
|
|
1631
2059
|
var DURATION_UNITS = {
|
|
@@ -1772,83 +2200,83 @@ function machineContext(baseDir, env = process.env) {
|
|
|
1772
2200
|
return { baseDir, env, cpuCount: cpus().length || 1, hostname: hostname() };
|
|
1773
2201
|
}
|
|
1774
2202
|
function buildConfigSchema(ctx) {
|
|
1775
|
-
const path = () =>
|
|
1776
|
-
const duration = () =>
|
|
1777
|
-
const size = () =>
|
|
1778
|
-
const parallel = () =>
|
|
1779
|
-
const strings = () =>
|
|
1780
|
-
const stringTable = () =>
|
|
1781
|
-
const permissionPolicy = (fallback) =>
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
default:
|
|
1785
|
-
...Object.fromEntries(TOOL_KINDS.map((kind) => [kind,
|
|
2203
|
+
const path = () => z9.string().trim().min(1).transform((value) => expandPath(value, ctx.baseDir, ctx.env));
|
|
2204
|
+
const duration = () => z9.string().regex(DURATION_PATTERN, "expected a duration such as 45s, 10m, 2h, or 7d").transform(parseDuration);
|
|
2205
|
+
const size = () => z9.string().regex(SIZE_PATTERN, "expected a size such as 500MB or 2GB").transform(parseSize);
|
|
2206
|
+
const parallel = () => z9.number().int().min(1).max(32);
|
|
2207
|
+
const strings = () => z9.array(z9.string().trim().min(1));
|
|
2208
|
+
const stringTable = () => z9.record(z9.string(), z9.string());
|
|
2209
|
+
const permissionPolicy = (fallback) => z9.union([
|
|
2210
|
+
z9.enum(POLICIES),
|
|
2211
|
+
z9.strictObject({
|
|
2212
|
+
default: z9.enum(POLICIES).optional(),
|
|
2213
|
+
...Object.fromEntries(TOOL_KINDS.map((kind) => [kind, z9.enum(POLICIES).optional()]))
|
|
1786
2214
|
})
|
|
1787
2215
|
]).default(typeof fallback === "string" ? fallback : { ...fallback }).describe('allow, ask, or deny every permission request \u2014 or a table by ACP tool kind, e.g. { default = "allow", execute = "ask" }. Defaults: worker "allow"; manager read-only, { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }');
|
|
1788
|
-
const mcpServer =
|
|
1789
|
-
name:
|
|
2216
|
+
const mcpServer = z9.strictObject({
|
|
2217
|
+
name: z9.string().trim().min(1).describe("Unique within the role; `hypotenuse` is reserved"),
|
|
1790
2218
|
command: strings().min(1).optional().describe("A stdio MCP server's command"),
|
|
1791
2219
|
env: stringTable().optional().describe("Added to the server's environment; $VAR allowed"),
|
|
1792
|
-
url:
|
|
2220
|
+
url: z9.url().optional().describe("An HTTP MCP server's URL"),
|
|
1793
2221
|
headers: stringTable().optional().describe("HTTP headers; $VAR allowed")
|
|
1794
2222
|
});
|
|
1795
|
-
const role = (fallbackPolicy) =>
|
|
1796
|
-
agent:
|
|
1797
|
-
prompt:
|
|
2223
|
+
const role = (fallbackPolicy) => z9.strictObject({
|
|
2224
|
+
agent: z9.string().trim().min(1).describe("An [agents.<id>] id or a preset name"),
|
|
2225
|
+
prompt: z9.string().min(1).optional().describe("The role's entry prompt template, inline"),
|
|
1798
2226
|
prompt_file: path().optional().describe("A Markdown prompt template file"),
|
|
1799
2227
|
permission_policy: permissionPolicy(fallbackPolicy),
|
|
1800
2228
|
max_parallel: parallel().optional().describe("Jobs of this role at once"),
|
|
1801
|
-
mode:
|
|
2229
|
+
mode: z9.string().trim().min(1).optional().describe("The ACP session mode to select"),
|
|
1802
2230
|
turn_timeout: duration().optional().describe("Overrides [sessions] turn_timeout"),
|
|
1803
|
-
additional_directories:
|
|
1804
|
-
extra_mcp_servers:
|
|
2231
|
+
additional_directories: z9.array(path()).default([]).describe("Extra directories the agent may read and write"),
|
|
2232
|
+
extra_mcp_servers: z9.array(mcpServer).default([]).describe("More MCP servers for the agent")
|
|
1805
2233
|
});
|
|
1806
|
-
const agent =
|
|
1807
|
-
preset:
|
|
2234
|
+
const agent = z9.strictObject({
|
|
2235
|
+
preset: z9.enum(PRESET_NAMES).optional().describe("A built-in agent launch command"),
|
|
1808
2236
|
command: strings().min(1).optional().describe("The full launch command of an ACP agent on stdio"),
|
|
1809
2237
|
args: strings().default([]).describe("Appended to the preset's or command's arguments"),
|
|
1810
2238
|
env: stringTable().default({}).describe("Added to the agent's environment; $VAR allowed"),
|
|
1811
|
-
auth_method:
|
|
2239
|
+
auth_method: z9.string().trim().min(1).optional().describe("Which advertised ACP authMethod to use"),
|
|
1812
2240
|
startup_timeout: duration().prefault("60s").describe("Time allowed to answer initialize"),
|
|
1813
2241
|
max_parallel: parallel().optional().describe("Jobs using this agent at once")
|
|
1814
2242
|
});
|
|
1815
|
-
const cleanup =
|
|
1816
|
-
const worktrees =
|
|
1817
|
-
const repository =
|
|
1818
|
-
name:
|
|
2243
|
+
const cleanup = z9.enum(["on-complete", "on-merge", "ttl", "never"]);
|
|
2244
|
+
const worktrees = z9.enum(["always", "never"]);
|
|
2245
|
+
const repository = z9.strictObject({
|
|
2246
|
+
name: z9.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`").max(64).optional().describe("Unique; used in worktree paths"),
|
|
1819
2247
|
path: path().describe("An existing git clone"),
|
|
1820
|
-
remote:
|
|
1821
|
-
base_branch:
|
|
2248
|
+
remote: z9.string().trim().min(1).optional().describe("The repository's remote; must match origin"),
|
|
2249
|
+
base_branch: z9.string().trim().min(1).optional().describe("The branch worktrees start from"),
|
|
1822
2250
|
setup: strings().default([]).describe("Shell commands run in each new worktree"),
|
|
1823
2251
|
setup_timeout: duration().prefault("10m").describe("Per setup command"),
|
|
1824
|
-
branch_prefix:
|
|
2252
|
+
branch_prefix: z9.string().trim().min(1).default("hypotenuse").describe("Branch name prefix"),
|
|
1825
2253
|
worktrees: worktrees.optional().describe("Overrides [sessions] worktrees"),
|
|
1826
2254
|
cleanup: cleanup.optional().describe("Overrides [sessions] cleanup"),
|
|
1827
2255
|
pr_url_patterns: strings().default([]).describe("Extra PR URL regexes for self-hosted forges")
|
|
1828
2256
|
});
|
|
1829
|
-
return
|
|
1830
|
-
config_version:
|
|
1831
|
-
server:
|
|
1832
|
-
url:
|
|
1833
|
-
project:
|
|
2257
|
+
return z9.strictObject({
|
|
2258
|
+
config_version: z9.number().int().describe(`The config format; must be ${CONFIG_VERSION}`),
|
|
2259
|
+
server: z9.strictObject({
|
|
2260
|
+
url: z9.url().optional().describe("The Hypotenuse server"),
|
|
2261
|
+
project: z9.string().trim().min(1).optional().describe("The runner's project, by slug or id")
|
|
1834
2262
|
}).prefault({}),
|
|
1835
|
-
runner:
|
|
1836
|
-
name:
|
|
1837
|
-
max_parallel_jobs:
|
|
1838
|
-
roles:
|
|
2263
|
+
runner: z9.strictObject({
|
|
2264
|
+
name: z9.string().trim().min(1).max(64).default(ctx.hostname.replace(/\.local$/i, "").slice(0, 64) || "runner").describe("Display name"),
|
|
2265
|
+
max_parallel_jobs: z9.number().int().min(1).max(32).default(defaultMaxParallelJobs(ctx.cpuCount)).describe("Jobs running at once on this machine"),
|
|
2266
|
+
roles: z9.array(z9.enum(RUNNER_ROLES)).min(1).default(["worker"]).describe("The roles this runner plays")
|
|
1839
2267
|
}).prefault({}),
|
|
1840
|
-
subscriptions:
|
|
2268
|
+
subscriptions: z9.strictObject({
|
|
1841
2269
|
teams: strings().default([]).describe("Linear team keys"),
|
|
1842
2270
|
projects: strings().default([]).describe("Linear project ids or names"),
|
|
1843
2271
|
labels: strings().default([]).describe("Label names; an issue needs one")
|
|
1844
2272
|
}).prefault({}),
|
|
1845
|
-
agents:
|
|
1846
|
-
roles:
|
|
2273
|
+
agents: z9.record(z9.string().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and `-`"), agent).default({}),
|
|
2274
|
+
roles: z9.strictObject({
|
|
1847
2275
|
manager: role(MANAGER_DEFAULT_POLICY).optional(),
|
|
1848
2276
|
worker: role("allow").optional()
|
|
1849
2277
|
}).prefault({}),
|
|
1850
|
-
repositories:
|
|
1851
|
-
sessions:
|
|
2278
|
+
repositories: z9.array(repository).default([]),
|
|
2279
|
+
sessions: z9.strictObject({
|
|
1852
2280
|
worktrees: worktrees.default("always").describe("Whether sessions get a worktree"),
|
|
1853
2281
|
worktree_root: path().prefault("~/.hypotenuse/worktrees").describe("Where worktrees live"),
|
|
1854
2282
|
cleanup: cleanup.default("on-complete").describe("When a worktree may be removed"),
|
|
@@ -1859,10 +2287,16 @@ function buildConfigSchema(ctx) {
|
|
|
1859
2287
|
keepalive_interval: duration().prefault("10m").describe("Silence before a keepalive thought"),
|
|
1860
2288
|
stop_grace: duration().prefault("10s").describe("Wind-down time after a stop"),
|
|
1861
2289
|
min_free_disk: size().prefault("2GB").describe("Free space needed for a new worktree"),
|
|
1862
|
-
co_author:
|
|
2290
|
+
co_author: z9.string().default("Hypotenuse <noreply@hypotenuse.cloud>").describe("Co-Authored-By trailer on the agent's commits; empty for none")
|
|
1863
2291
|
}).prefault({}),
|
|
1864
|
-
|
|
1865
|
-
|
|
2292
|
+
telemetry: z9.strictObject({
|
|
2293
|
+
enabled: z9.boolean().default(true).describe("Send this machine's telemetry for the project's exporters; false sends none"),
|
|
2294
|
+
max_content: z9.enum(CONTENT_LEVELS).default("full").describe("The most this runner sends: metadata, content, or full \u2014 lower than an exporter's level wins"),
|
|
2295
|
+
agent_native: z9.boolean().default(true).describe("Capture the coding agent's own OpenTelemetry; false never starts the receiver or sets OTEL_* variables"),
|
|
2296
|
+
buffer_mb: z9.number().int().min(1).max(1024).default(32).describe("The in-memory telemetry queue, in MB; when full the oldest logs, then spans, are dropped and counted")
|
|
2297
|
+
}).prefault({}),
|
|
2298
|
+
log: z9.strictObject({
|
|
2299
|
+
level: z9.enum(["error", "warn", "info", "debug", "trace"]).default("info").describe("Log level"),
|
|
1866
2300
|
file: path().optional().describe("The daemon log file")
|
|
1867
2301
|
}).prefault({})
|
|
1868
2302
|
});
|
|
@@ -1874,7 +2308,7 @@ var ConfigSchema = buildConfigSchema({
|
|
|
1874
2308
|
hostname: "runner"
|
|
1875
2309
|
});
|
|
1876
2310
|
function configJsonSchema() {
|
|
1877
|
-
const schema =
|
|
2311
|
+
const schema = z9.toJSONSchema(ConfigSchema, {
|
|
1878
2312
|
io: "input",
|
|
1879
2313
|
unrepresentable: "any"
|
|
1880
2314
|
});
|
|
@@ -2347,7 +2781,7 @@ function failedResult(call) {
|
|
|
2347
2781
|
`).find((l) => l.trim()) ?? "";
|
|
2348
2782
|
return first ? `Failed: ${truncate(first.trim(), 500)}` : "Failed";
|
|
2349
2783
|
}
|
|
2350
|
-
var
|
|
2784
|
+
var SECRET_PATTERNS2 = [
|
|
2351
2785
|
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
2352
2786
|
/\bsk_(?:live|test)_[A-Za-z0-9]{16,}/g,
|
|
2353
2787
|
/\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
@@ -2361,7 +2795,7 @@ var SECRET_PATTERNS = [
|
|
|
2361
2795
|
];
|
|
2362
2796
|
function redact(text) {
|
|
2363
2797
|
let out = text;
|
|
2364
|
-
for (const pattern of
|
|
2798
|
+
for (const pattern of SECRET_PATTERNS2) {
|
|
2365
2799
|
out = out.replace(pattern, (match, prefix) => typeof prefix === "string" && /^Bearer/i.test(match) ? `${prefix}[redacted]` : "[redacted]");
|
|
2366
2800
|
}
|
|
2367
2801
|
return out;
|
|
@@ -2556,7 +2990,7 @@ class ApiClient {
|
|
|
2556
2990
|
response = await fetch(url, {
|
|
2557
2991
|
method,
|
|
2558
2992
|
headers,
|
|
2559
|
-
body: options.body
|
|
2993
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : options.rawBody ? new Uint8Array(options.rawBody) : undefined,
|
|
2560
2994
|
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined
|
|
2561
2995
|
});
|
|
2562
2996
|
} catch (error) {
|
|
@@ -3061,32 +3495,32 @@ import {
|
|
|
3061
3495
|
writeFileSync
|
|
3062
3496
|
} from "fs";
|
|
3063
3497
|
import { dirname as dirname4 } from "path";
|
|
3064
|
-
import { z as
|
|
3498
|
+
import { z as z10 } from "zod";
|
|
3065
3499
|
function credentialsPathFor(env) {
|
|
3066
3500
|
return credentialsPath(env);
|
|
3067
3501
|
}
|
|
3068
|
-
var LoginSchema =
|
|
3069
|
-
token:
|
|
3070
|
-
cliTokenId:
|
|
3071
|
-
device:
|
|
3072
|
-
email:
|
|
3073
|
-
expiresAt:
|
|
3502
|
+
var LoginSchema = z10.object({
|
|
3503
|
+
token: z10.string(),
|
|
3504
|
+
cliTokenId: z10.number(),
|
|
3505
|
+
device: z10.string(),
|
|
3506
|
+
email: z10.string().optional(),
|
|
3507
|
+
expiresAt: z10.number().optional()
|
|
3074
3508
|
});
|
|
3075
|
-
var RunnerCredentialSchema =
|
|
3076
|
-
runnerId:
|
|
3077
|
-
projectId:
|
|
3078
|
-
name:
|
|
3079
|
-
token:
|
|
3509
|
+
var RunnerCredentialSchema = z10.object({
|
|
3510
|
+
runnerId: z10.number(),
|
|
3511
|
+
projectId: z10.number(),
|
|
3512
|
+
name: z10.string(),
|
|
3513
|
+
token: z10.string()
|
|
3080
3514
|
});
|
|
3081
|
-
var ServerEntrySchema =
|
|
3515
|
+
var ServerEntrySchema = z10.object({
|
|
3082
3516
|
login: LoginSchema.nullable().default(null),
|
|
3083
|
-
project:
|
|
3517
|
+
project: z10.number().nullable().default(null),
|
|
3084
3518
|
runner: RunnerCredentialSchema.nullable().default(null)
|
|
3085
3519
|
});
|
|
3086
|
-
var CredentialsSchema =
|
|
3087
|
-
version:
|
|
3088
|
-
current:
|
|
3089
|
-
servers:
|
|
3520
|
+
var CredentialsSchema = z10.object({
|
|
3521
|
+
version: z10.literal(1),
|
|
3522
|
+
current: z10.string().nullable().default(null),
|
|
3523
|
+
servers: z10.record(z10.string(), ServerEntrySchema).default({})
|
|
3090
3524
|
});
|
|
3091
3525
|
function emptyCredentials() {
|
|
3092
3526
|
return { version: 1, current: null, servers: {} };
|
|
@@ -3164,6 +3598,13 @@ async function resolveProjectId(client, selector) {
|
|
|
3164
3598
|
}
|
|
3165
3599
|
}
|
|
3166
3600
|
}
|
|
3601
|
+
async function onlyProject(client) {
|
|
3602
|
+
const { projects, pagination } = await client.request("GET", "/projects", { query: { page: 1, limit: 2 } });
|
|
3603
|
+
return {
|
|
3604
|
+
project: pagination.total === 1 ? projects[0] ?? null : null,
|
|
3605
|
+
total: pagination.total
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3167
3608
|
|
|
3168
3609
|
// src/api/url.ts
|
|
3169
3610
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
@@ -3318,10 +3759,14 @@ class CliContext {
|
|
|
3318
3759
|
}
|
|
3319
3760
|
async projectId() {
|
|
3320
3761
|
const selector = this.options.project ?? this.env.HYPOTENUSE_PROJECT ?? (this.credentials.servers[this.server.origin]?.project != null ? String(this.credentials.servers[this.server.origin]?.project) : undefined);
|
|
3321
|
-
if (
|
|
3322
|
-
|
|
3762
|
+
if (selector)
|
|
3763
|
+
return resolveProjectId(this.signedInClient(), selector);
|
|
3764
|
+
const { project, total } = await onlyProject(this.signedInClient());
|
|
3765
|
+
if (project) {
|
|
3766
|
+
logger.info(`Using project ${project.slug}, the only one you are in.`);
|
|
3767
|
+
return project.id;
|
|
3323
3768
|
}
|
|
3324
|
-
|
|
3769
|
+
throw new Error(total === 0 ? "You are not in any project yet. Create one in the dashboard, or accept an invite." : "No project selected, and you are in several. Pass --project <id|slug> or run `hypotenuse project use <id|slug>`.");
|
|
3325
3770
|
}
|
|
3326
3771
|
}
|
|
3327
3772
|
function contextFor(command) {
|
|
@@ -3602,6 +4047,389 @@ import { join as join10 } from "path";
|
|
|
3602
4047
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
|
|
3603
4048
|
import { join as join8 } from "path";
|
|
3604
4049
|
|
|
4050
|
+
// src/telemetry/acp.ts
|
|
4051
|
+
import { isAbsolute as isAbsolute4, relative as relative3 } from "path";
|
|
4052
|
+
class AcpTelemetry {
|
|
4053
|
+
turn;
|
|
4054
|
+
cwd;
|
|
4055
|
+
tools = new Map;
|
|
4056
|
+
spawn = null;
|
|
4057
|
+
prompt = null;
|
|
4058
|
+
chunk = null;
|
|
4059
|
+
promptSpanId = newSpanId();
|
|
4060
|
+
constructor(turn, cwd) {
|
|
4061
|
+
this.turn = turn;
|
|
4062
|
+
this.cwd = cwd;
|
|
4063
|
+
}
|
|
4064
|
+
get traceparent() {
|
|
4065
|
+
return this.turn?.traceparentFor(this.promptSpanId) ?? null;
|
|
4066
|
+
}
|
|
4067
|
+
rel(path) {
|
|
4068
|
+
if (!isAbsolute4(path))
|
|
4069
|
+
return path;
|
|
4070
|
+
const r = relative3(this.cwd, path);
|
|
4071
|
+
return r.startsWith("..") ? path : r || ".";
|
|
4072
|
+
}
|
|
4073
|
+
begin(name, attrs = {}) {
|
|
4074
|
+
return this.turn?.span(name, attrs, this.prompt) ?? null;
|
|
4075
|
+
}
|
|
4076
|
+
async around(name, attrs, fn) {
|
|
4077
|
+
const span = this.begin(name, attrs);
|
|
4078
|
+
try {
|
|
4079
|
+
return await fn(span);
|
|
4080
|
+
} catch (error) {
|
|
4081
|
+
span?.fail(error instanceof Error ? error.message : String(error));
|
|
4082
|
+
throw error;
|
|
4083
|
+
} finally {
|
|
4084
|
+
span?.end();
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
spawned(attrs, command) {
|
|
4088
|
+
this.spawn = this.begin("agent.spawn", attrs);
|
|
4089
|
+
this.spawn?.content("agent.command", command);
|
|
4090
|
+
}
|
|
4091
|
+
initialized(attrs) {
|
|
4092
|
+
this.spawn?.set(attrs);
|
|
4093
|
+
}
|
|
4094
|
+
exited(exitCode) {
|
|
4095
|
+
this.spawn?.end({ "process.exit.code": exitCode ?? null });
|
|
4096
|
+
}
|
|
4097
|
+
promptStarted(attrs, text) {
|
|
4098
|
+
if (!this.turn)
|
|
4099
|
+
return;
|
|
4100
|
+
this.prompt = this.turn.span("acp.prompt", { "gen_ai.operation.name": "invoke_agent", ...attrs }, null, "client", this.promptSpanId);
|
|
4101
|
+
this.prompt.content("prompt", text);
|
|
4102
|
+
this.turn.promptSpan = this.prompt;
|
|
4103
|
+
}
|
|
4104
|
+
promptEnded(attrs, error) {
|
|
4105
|
+
this.flushChunk();
|
|
4106
|
+
for (const [, span] of this.tools)
|
|
4107
|
+
span.end({ "hypotenuse.tool.finished": false });
|
|
4108
|
+
this.tools.clear();
|
|
4109
|
+
if (error)
|
|
4110
|
+
this.prompt?.fail(error);
|
|
4111
|
+
this.prompt?.end(attrs);
|
|
4112
|
+
if (this.turn)
|
|
4113
|
+
this.turn.promptSpan = null;
|
|
4114
|
+
const input = attrs["gen_ai.usage.input_tokens"];
|
|
4115
|
+
const output = attrs["gen_ai.usage.output_tokens"];
|
|
4116
|
+
if (typeof input === "number" || typeof output === "number")
|
|
4117
|
+
this.turn?.usage({
|
|
4118
|
+
inputTokens: typeof input === "number" ? input : undefined,
|
|
4119
|
+
outputTokens: typeof output === "number" ? output : undefined
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
flushChunk() {
|
|
4123
|
+
const chunk = this.chunk;
|
|
4124
|
+
this.chunk = null;
|
|
4125
|
+
if (!chunk || !this.prompt || !this.turn)
|
|
4126
|
+
return;
|
|
4127
|
+
this.prompt.event(`agent.${chunk.kind}`, this.turn.contentAttrs("text", chunk.text));
|
|
4128
|
+
}
|
|
4129
|
+
update(update) {
|
|
4130
|
+
if (!this.turn || !this.prompt)
|
|
4131
|
+
return;
|
|
4132
|
+
const u = update;
|
|
4133
|
+
const kind = String(u.sessionUpdate);
|
|
4134
|
+
if (kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
|
|
4135
|
+
const content = u.content;
|
|
4136
|
+
if (content?.type !== "text")
|
|
4137
|
+
return;
|
|
4138
|
+
const which = kind === "agent_thought_chunk" ? "thought" : "message";
|
|
4139
|
+
if (this.chunk && this.chunk.kind !== which)
|
|
4140
|
+
this.flushChunk();
|
|
4141
|
+
this.chunk ??= { kind: which, text: "" };
|
|
4142
|
+
this.chunk.text += content.text ?? "";
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
this.flushChunk();
|
|
4146
|
+
switch (kind) {
|
|
4147
|
+
case "tool_call": {
|
|
4148
|
+
const id = String(u.toolCallId);
|
|
4149
|
+
const span = this.turn.span("tool_call", {
|
|
4150
|
+
"gen_ai.operation.name": "execute_tool",
|
|
4151
|
+
"gen_ai.tool.call.id": id,
|
|
4152
|
+
"gen_ai.tool.type": typeof u.kind === "string" ? u.kind : null,
|
|
4153
|
+
"hypotenuse.tool.status": typeof u.status === "string" ? u.status : "pending"
|
|
4154
|
+
}, this.prompt);
|
|
4155
|
+
span.content("tool.title", typeof u.title === "string" ? u.title : null);
|
|
4156
|
+
this.toolDetail(span, u);
|
|
4157
|
+
this.tools.set(id, span);
|
|
4158
|
+
if (u.status === "completed" || u.status === "failed")
|
|
4159
|
+
this.endTool(id, u);
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
case "tool_call_update": {
|
|
4163
|
+
const id = String(u.toolCallId);
|
|
4164
|
+
const span = this.tools.get(id);
|
|
4165
|
+
if (!span)
|
|
4166
|
+
return;
|
|
4167
|
+
if (typeof u.kind === "string")
|
|
4168
|
+
span.set({ "gen_ai.tool.type": u.kind });
|
|
4169
|
+
if (typeof u.title === "string")
|
|
4170
|
+
span.content("tool.title", u.title);
|
|
4171
|
+
this.toolDetail(span, u);
|
|
4172
|
+
if (u.status === "completed" || u.status === "failed")
|
|
4173
|
+
this.endTool(id, u);
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
case "plan": {
|
|
4177
|
+
const entries = u.entries ?? [];
|
|
4178
|
+
this.prompt.event("plan", {
|
|
4179
|
+
"hypotenuse.plan.entries": entries.length,
|
|
4180
|
+
"hypotenuse.plan.completed": entries.filter((e) => e.status === "completed").length,
|
|
4181
|
+
...this.turn.contentAttrs("plan", JSON.stringify(entries))
|
|
4182
|
+
});
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
case "usage_update": {
|
|
4186
|
+
const cost = u.cost;
|
|
4187
|
+
this.prompt.event("usage", {
|
|
4188
|
+
"hypotenuse.context.used": typeof u.used === "number" ? u.used : null,
|
|
4189
|
+
"hypotenuse.context.size": typeof u.size === "number" ? u.size : null,
|
|
4190
|
+
"hypotenuse.usage.cost": cost?.amount ?? null,
|
|
4191
|
+
"hypotenuse.usage.currency": cost?.currency ?? null
|
|
4192
|
+
});
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
case "current_mode_update":
|
|
4196
|
+
this.prompt.event("mode", {
|
|
4197
|
+
"hypotenuse.mode": typeof u.currentModeId === "string" ? u.currentModeId : null
|
|
4198
|
+
});
|
|
4199
|
+
return;
|
|
4200
|
+
default:
|
|
4201
|
+
this.prompt.event("session_update", { "hypotenuse.update.kind": kind });
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
toolDetail(span, u) {
|
|
4205
|
+
const locations = Array.isArray(u.locations) ? u.locations : null;
|
|
4206
|
+
if (locations?.length)
|
|
4207
|
+
span.set({
|
|
4208
|
+
"hypotenuse.tool.locations": locations.map((l) => l.path ? this.rel(l.path) : "").filter(Boolean).join(",")
|
|
4209
|
+
});
|
|
4210
|
+
if (u.rawInput !== undefined)
|
|
4211
|
+
span.content("tool.input", JSON.stringify(u.rawInput));
|
|
4212
|
+
if (u.rawOutput !== undefined)
|
|
4213
|
+
span.content("tool.output", JSON.stringify(u.rawOutput));
|
|
4214
|
+
else if (Array.isArray(u.content) && u.content.length)
|
|
4215
|
+
span.content("tool.output", JSON.stringify(u.content));
|
|
4216
|
+
}
|
|
4217
|
+
endTool(id, u) {
|
|
4218
|
+
const span = this.tools.get(id);
|
|
4219
|
+
this.tools.delete(id);
|
|
4220
|
+
if (!span)
|
|
4221
|
+
return;
|
|
4222
|
+
if (u.status === "failed")
|
|
4223
|
+
span.fail("the tool call failed");
|
|
4224
|
+
span.end({ "hypotenuse.tool.status": String(u.status) });
|
|
4225
|
+
}
|
|
4226
|
+
permission(toolCall, policy, options) {
|
|
4227
|
+
const parent = this.tools.get(toolCall.toolCallId) ?? this.prompt;
|
|
4228
|
+
const span = this.turn?.span("permission.wait", {
|
|
4229
|
+
"gen_ai.tool.call.id": toolCall.toolCallId,
|
|
4230
|
+
"gen_ai.tool.type": toolCall.kind ?? null,
|
|
4231
|
+
"hypotenuse.permission.policy": policy,
|
|
4232
|
+
"hypotenuse.permission.options": options.map((o) => o.kind ?? o.optionId).join(",")
|
|
4233
|
+
}, parent) ?? null;
|
|
4234
|
+
span?.content("tool.title", toolCall.title ?? null);
|
|
4235
|
+
return (answer) => span?.end({
|
|
4236
|
+
"hypotenuse.permission.outcome": answer.outcome,
|
|
4237
|
+
"hypotenuse.permission.option": answer.optionId ?? null,
|
|
4238
|
+
"hypotenuse.permission.decided_by": answer.decidedBy
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
fs(op, path) {
|
|
4242
|
+
const span = this.begin(`fs.${op}`, { "file.path": this.rel(path) });
|
|
4243
|
+
return (result) => {
|
|
4244
|
+
if (result.refused)
|
|
4245
|
+
span?.fail(result.refused);
|
|
4246
|
+
span?.end({
|
|
4247
|
+
"hypotenuse.fs.bytes": result.bytes ?? null,
|
|
4248
|
+
"hypotenuse.fs.refused": result.refused ?? null
|
|
4249
|
+
});
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
frame(line, message) {
|
|
4253
|
+
this.turn?.frame(line, message, this.prompt);
|
|
4254
|
+
}
|
|
4255
|
+
stderr(line) {
|
|
4256
|
+
this.turn?.log({
|
|
4257
|
+
body: line,
|
|
4258
|
+
level: "content",
|
|
4259
|
+
eventName: "agent.stderr",
|
|
4260
|
+
severity: "warn",
|
|
4261
|
+
span: this.spawn
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
// src/telemetry/native.ts
|
|
4267
|
+
var NATIVE_TELEMETRY_PRESETS = {
|
|
4268
|
+
claude: "native",
|
|
4269
|
+
codex: "native",
|
|
4270
|
+
gemini: "native",
|
|
4271
|
+
qwen: "native",
|
|
4272
|
+
goose: "standard-otel",
|
|
4273
|
+
opencode: "standard-otel"
|
|
4274
|
+
};
|
|
4275
|
+
function nativeTelemetrySupport(preset) {
|
|
4276
|
+
return preset && NATIVE_TELEMETRY_PRESETS[preset] || "unknown";
|
|
4277
|
+
}
|
|
4278
|
+
function dropInheritedOtel(env) {
|
|
4279
|
+
for (const key of Object.keys(env))
|
|
4280
|
+
if (key.startsWith("OTEL_"))
|
|
4281
|
+
delete env[key];
|
|
4282
|
+
}
|
|
4283
|
+
function nativeTelemetryLaunch(preset, target) {
|
|
4284
|
+
const full = target.level === "full";
|
|
4285
|
+
const env = {
|
|
4286
|
+
HYPOTENUSE_OTLP_ENDPOINT: target.endpoint,
|
|
4287
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: target.endpoint,
|
|
4288
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
4289
|
+
OTEL_TRACES_EXPORTER: "otlp",
|
|
4290
|
+
OTEL_METRICS_EXPORTER: "otlp",
|
|
4291
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
4292
|
+
OTEL_METRIC_EXPORT_INTERVAL: "5000",
|
|
4293
|
+
OTEL_LOGS_EXPORT_INTERVAL: "2000",
|
|
4294
|
+
OTEL_BSP_SCHEDULE_DELAY: "1000"
|
|
4295
|
+
};
|
|
4296
|
+
const args = [];
|
|
4297
|
+
switch (preset) {
|
|
4298
|
+
case "claude":
|
|
4299
|
+
env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
|
4300
|
+
env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA = "1";
|
|
4301
|
+
if (full) {
|
|
4302
|
+
env.OTEL_LOG_USER_PROMPTS = "1";
|
|
4303
|
+
env.OTEL_LOG_TOOL_DETAILS = "1";
|
|
4304
|
+
}
|
|
4305
|
+
break;
|
|
4306
|
+
case "gemini":
|
|
4307
|
+
case "qwen": {
|
|
4308
|
+
const prefix = preset === "gemini" ? "GEMINI" : "QWEN";
|
|
4309
|
+
env[`${prefix}_TELEMETRY_ENABLED`] = "true";
|
|
4310
|
+
env[`${prefix}_TELEMETRY_TARGET`] = "local";
|
|
4311
|
+
env[`${prefix}_TELEMETRY_OTLP_ENDPOINT`] = target.endpoint;
|
|
4312
|
+
env[`${prefix}_TELEMETRY_OTLP_PROTOCOL`] = "http";
|
|
4313
|
+
env[`${prefix}_TELEMETRY_LOG_PROMPTS`] = full ? "true" : "false";
|
|
4314
|
+
break;
|
|
4315
|
+
}
|
|
4316
|
+
case "codex":
|
|
4317
|
+
args.push("-c", `otel.exporter={otlp-http={endpoint="${target.endpoint}",protocol="json"}}`, "-c", `otel.log_user_prompt=${full ? "true" : "false"}`);
|
|
4318
|
+
break;
|
|
4319
|
+
}
|
|
4320
|
+
return { env, args };
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
// src/telemetry/receiver.ts
|
|
4324
|
+
var MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
4325
|
+
var DRAIN_MS = 1500;
|
|
4326
|
+
|
|
4327
|
+
class NativeReceiver {
|
|
4328
|
+
turn;
|
|
4329
|
+
nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(18))).toString("base64url");
|
|
4330
|
+
server = null;
|
|
4331
|
+
received = {
|
|
4332
|
+
traces: 0,
|
|
4333
|
+
metrics: 0,
|
|
4334
|
+
logs: 0
|
|
4335
|
+
};
|
|
4336
|
+
lastReceivedAt = 0;
|
|
4337
|
+
constructor(turn) {
|
|
4338
|
+
this.turn = turn;
|
|
4339
|
+
}
|
|
4340
|
+
start() {
|
|
4341
|
+
this.server = Bun.serve({
|
|
4342
|
+
hostname: "127.0.0.1",
|
|
4343
|
+
port: 0,
|
|
4344
|
+
maxRequestBodySize: MAX_BODY_BYTES,
|
|
4345
|
+
fetch: (req) => this.handle(req)
|
|
4346
|
+
});
|
|
4347
|
+
return this.endpoint;
|
|
4348
|
+
}
|
|
4349
|
+
get endpoint() {
|
|
4350
|
+
return `http://127.0.0.1:${this.server?.port ?? 0}/${this.nonce}`;
|
|
4351
|
+
}
|
|
4352
|
+
async handle(req) {
|
|
4353
|
+
const url = new URL(req.url);
|
|
4354
|
+
const prefix = `/${this.nonce}/`;
|
|
4355
|
+
const signal = signalOfPath(url.pathname);
|
|
4356
|
+
if (req.method !== "POST" || !url.pathname.startsWith(prefix) || !signal)
|
|
4357
|
+
return new Response(null, { status: 404 });
|
|
4358
|
+
const contentType = req.headers.get("content-type");
|
|
4359
|
+
try {
|
|
4360
|
+
let bytes = new Uint8Array(await req.arrayBuffer());
|
|
4361
|
+
if ((req.headers.get("content-encoding") ?? "").includes("gzip"))
|
|
4362
|
+
bytes = new Uint8Array(Bun.gunzipSync(bytes));
|
|
4363
|
+
if (bytes.byteLength > MAX_BODY_BYTES)
|
|
4364
|
+
throw new Error("too large");
|
|
4365
|
+
this.accept(signal, decodeOtlpBody(signal, bytes, contentType));
|
|
4366
|
+
} catch {
|
|
4367
|
+
return new Response(null, { status: 400 });
|
|
4368
|
+
}
|
|
4369
|
+
const json = (contentType ?? "").includes("json");
|
|
4370
|
+
return new Response(json ? "{}" : new Uint8Array(0), {
|
|
4371
|
+
status: 200,
|
|
4372
|
+
headers: {
|
|
4373
|
+
"Content-Type": json ? "application/json" : "application/x-protobuf"
|
|
4374
|
+
}
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
accept(signal, request) {
|
|
4378
|
+
this.received[signal]++;
|
|
4379
|
+
this.lastReceivedAt = Date.now();
|
|
4380
|
+
const turn = this.turn;
|
|
4381
|
+
const level = {
|
|
4382
|
+
key: LEVEL_ATTRIBUTE,
|
|
4383
|
+
value: { stringValue: turn.level }
|
|
4384
|
+
};
|
|
4385
|
+
const tag = (attrs) => [
|
|
4386
|
+
...(attrs ?? []).filter((a) => a.key !== LEVEL_ATTRIBUTE),
|
|
4387
|
+
level
|
|
4388
|
+
];
|
|
4389
|
+
if (signal === "traces") {
|
|
4390
|
+
if (!turn.sampled)
|
|
4391
|
+
return;
|
|
4392
|
+
for (const rs of request.resourceSpans ?? [])
|
|
4393
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
4394
|
+
ss.spans = (ss.spans ?? []).map((s) => {
|
|
4395
|
+
const root = !s.parentSpanId || /^0+$/.test(s.parentSpanId);
|
|
4396
|
+
return {
|
|
4397
|
+
...s,
|
|
4398
|
+
traceId: turn.traceId,
|
|
4399
|
+
...root ? { parentSpanId: turn.turnSpan.spanId } : {},
|
|
4400
|
+
attributes: tag(s.attributes)
|
|
4401
|
+
};
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
} else if (signal === "logs") {
|
|
4405
|
+
if (!turn.sampled)
|
|
4406
|
+
return;
|
|
4407
|
+
for (const rl of request.resourceLogs ?? [])
|
|
4408
|
+
for (const sl of rl.scopeLogs ?? [])
|
|
4409
|
+
sl.logRecords = (sl.logRecords ?? []).map((l) => ({
|
|
4410
|
+
...l,
|
|
4411
|
+
traceId: turn.traceId,
|
|
4412
|
+
...l.traceId && l.traceId !== turn.traceId ? { spanId: turn.turnSpan.spanId } : {},
|
|
4413
|
+
attributes: tag(l.attributes)
|
|
4414
|
+
}));
|
|
4415
|
+
}
|
|
4416
|
+
stampResource(signal, request, [
|
|
4417
|
+
...turn.resourceAttributes.filter((a) => a.key !== "hypotenuse.source" && !a.key.startsWith("service.")),
|
|
4418
|
+
...attributes({ "hypotenuse.source": "agent_native" })
|
|
4419
|
+
]);
|
|
4420
|
+
turn.enqueue(signal, request);
|
|
4421
|
+
}
|
|
4422
|
+
async close(drainMs = DRAIN_MS) {
|
|
4423
|
+
if (!this.server)
|
|
4424
|
+
return;
|
|
4425
|
+
const quietFor = Date.now() - this.lastReceivedAt;
|
|
4426
|
+
if (quietFor < drainMs)
|
|
4427
|
+
await Bun.sleep(drainMs - quietFor);
|
|
4428
|
+
this.server.stop(true);
|
|
4429
|
+
this.server = null;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
|
|
3605
4433
|
// src/worktree/co-author.ts
|
|
3606
4434
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3607
4435
|
import { join as join5 } from "path";
|
|
@@ -4620,7 +5448,7 @@ import {
|
|
|
4620
5448
|
rmSync as rmSync3,
|
|
4621
5449
|
writeFileSync as writeFileSync4
|
|
4622
5450
|
} from "fs";
|
|
4623
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as
|
|
5451
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
|
|
4624
5452
|
|
|
4625
5453
|
class OutsideWorktree extends Error {
|
|
4626
5454
|
path;
|
|
@@ -4679,7 +5507,7 @@ class ConfinedFs {
|
|
|
4679
5507
|
const root = this.roots[0];
|
|
4680
5508
|
if (!root)
|
|
4681
5509
|
throw new OutsideWorktree(path);
|
|
4682
|
-
return
|
|
5510
|
+
return isAbsolute5(path) ? resolve2(path) : resolve2(root, path);
|
|
4683
5511
|
}
|
|
4684
5512
|
read(path, line, limit) {
|
|
4685
5513
|
const real = this.resolveExisting(path);
|
|
@@ -4973,7 +5801,7 @@ function callText(call) {
|
|
|
4973
5801
|
}
|
|
4974
5802
|
|
|
4975
5803
|
// src/acp/mcp-servers.ts
|
|
4976
|
-
import { isAbsolute as
|
|
5804
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
4977
5805
|
var HYPOTENUSE_SERVER_NAME = "hypotenuse";
|
|
4978
5806
|
var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
4979
5807
|
|
|
@@ -5023,7 +5851,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
|
|
|
5023
5851
|
const [command, ...args] = s.command;
|
|
5024
5852
|
servers.push({
|
|
5025
5853
|
name: s.name,
|
|
5026
|
-
command:
|
|
5854
|
+
command: isAbsolute6(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
|
|
5027
5855
|
args,
|
|
5028
5856
|
env: pairs(s.name, s.env, env)
|
|
5029
5857
|
});
|
|
@@ -5473,6 +6301,8 @@ class Turn {
|
|
|
5473
6301
|
where;
|
|
5474
6302
|
readOnly;
|
|
5475
6303
|
agentNote;
|
|
6304
|
+
otel;
|
|
6305
|
+
receiver = null;
|
|
5476
6306
|
constructor(ctx, options) {
|
|
5477
6307
|
this.ctx = ctx;
|
|
5478
6308
|
this.options = options;
|
|
@@ -5498,6 +6328,7 @@ class Turn {
|
|
|
5498
6328
|
}
|
|
5499
6329
|
};
|
|
5500
6330
|
this.mapper = new TurnMapper(this.mapperOptions);
|
|
6331
|
+
this.otel = new AcpTelemetry(ctx.telemetry ?? null, this.cwd);
|
|
5501
6332
|
this.questions = new QuestionQueue({
|
|
5502
6333
|
post: (event) => this.emit(event),
|
|
5503
6334
|
waiting: (waiting) => {
|
|
@@ -5609,6 +6440,7 @@ class Turn {
|
|
|
5609
6440
|
throw error;
|
|
5610
6441
|
} finally {
|
|
5611
6442
|
clearInterval(timer);
|
|
6443
|
+
this.otel.promptEnded({ "hypotenuse.end_reason": this.endReason }, this.endReason && this.endReason !== "stop" ? this.endReason : null);
|
|
5612
6444
|
await this.shutdown();
|
|
5613
6445
|
await this.workspace?.release().catch((error) => ctx.log.warn(`releasing the workspace failed: ${errorMessage(error)}`, {
|
|
5614
6446
|
session: ctx.assignment.agentSessionId
|
|
@@ -5619,10 +6451,28 @@ class Turn {
|
|
|
5619
6451
|
const { ctx } = this;
|
|
5620
6452
|
const { assignment } = ctx;
|
|
5621
6453
|
if (ctx.prepareWorkspace) {
|
|
6454
|
+
const prepare = ctx.prepareWorkspace;
|
|
5622
6455
|
try {
|
|
5623
|
-
this.workspace = await
|
|
5624
|
-
|
|
5625
|
-
|
|
6456
|
+
this.workspace = await this.otel.around("worktree.prepare", {
|
|
6457
|
+
"hypotenuse.repository": assignment.repository?.remote ?? null,
|
|
6458
|
+
"hypotenuse.work_item.fresh": assignment.fresh
|
|
6459
|
+
}, async (span) => {
|
|
6460
|
+
const workspace = await prepare({
|
|
6461
|
+
output: (line) => {
|
|
6462
|
+
this.log.note(line);
|
|
6463
|
+
this.otel.turn?.log({
|
|
6464
|
+
body: line,
|
|
6465
|
+
eventName: "worktree.output",
|
|
6466
|
+
span
|
|
6467
|
+
});
|
|
6468
|
+
},
|
|
6469
|
+
env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
|
|
6470
|
+
});
|
|
6471
|
+
span?.set({
|
|
6472
|
+
"hypotenuse.worktree.mode": workspace.mode,
|
|
6473
|
+
"vcs.ref.head.name": workspace.branch
|
|
6474
|
+
});
|
|
6475
|
+
return workspace;
|
|
5626
6476
|
});
|
|
5627
6477
|
} catch (error) {
|
|
5628
6478
|
const remote = ctx.assignment.repository?.remote ?? null;
|
|
@@ -5656,6 +6506,7 @@ class Turn {
|
|
|
5656
6506
|
if (this.endReason)
|
|
5657
6507
|
throw new TurnFailure("stopped");
|
|
5658
6508
|
this.mapperOptions.cwd = this.cwd;
|
|
6509
|
+
this.otel.cwd = this.cwd;
|
|
5659
6510
|
const remote = this.workspace?.repository?.remote ?? null;
|
|
5660
6511
|
if (remote) {
|
|
5661
6512
|
this.mapperOptions.pullRequests = new PrDetector(remote, this.workspace?.repositoryConfig?.pr_url_patterns ?? [], ctx.pullRequests ?? []);
|
|
@@ -5684,13 +6535,17 @@ class Turn {
|
|
|
5684
6535
|
if (coAuthor && this.workspace?.repository) {
|
|
5685
6536
|
Object.assign(launch.env, coAuthorEnvironment(launch.env, installCoAuthorHooks(ctx.stateDir), coAuthor));
|
|
5686
6537
|
}
|
|
6538
|
+
this.withTelemetry(launch);
|
|
5687
6539
|
this.log.note(`spawn ${launch.command.join(" ")} in ${this.cwd}`);
|
|
5688
6540
|
try {
|
|
5689
6541
|
this.conn = AcpConnection.spawn({
|
|
5690
6542
|
command: launch.command,
|
|
5691
6543
|
cwd: this.cwd,
|
|
5692
6544
|
env: launch.env,
|
|
5693
|
-
stderr: (line) =>
|
|
6545
|
+
stderr: (line) => {
|
|
6546
|
+
this.log.stderr(line);
|
|
6547
|
+
this.otel.stderr(line);
|
|
6548
|
+
},
|
|
5694
6549
|
noise: (line) => this.noise(line),
|
|
5695
6550
|
trace: (line, message) => {
|
|
5696
6551
|
if (line.startsWith("<-")) {
|
|
@@ -5698,37 +6553,45 @@ class Turn {
|
|
|
5698
6553
|
this.noiseInARow = 0;
|
|
5699
6554
|
}
|
|
5700
6555
|
this.log.rpc(line, message);
|
|
6556
|
+
this.otel.frame(line, message);
|
|
5701
6557
|
},
|
|
5702
6558
|
handlers: {
|
|
5703
6559
|
update: (n) => {
|
|
5704
6560
|
if (this.acpSessionId && n.sessionId !== this.acpSessionId)
|
|
5705
6561
|
return;
|
|
6562
|
+
this.otel.update(n.update);
|
|
5706
6563
|
this.emitAll(this.mapper.update(n.update));
|
|
5707
6564
|
},
|
|
5708
6565
|
requestPermission: (params) => this.permission(params),
|
|
5709
6566
|
readTextFile: async (params) => {
|
|
6567
|
+
const done = this.otel.fs("read", params.path);
|
|
5710
6568
|
try {
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
};
|
|
6569
|
+
const content = this.fs.read(params.path, params.line, params.limit);
|
|
6570
|
+
done({ bytes: Buffer.byteLength(content) });
|
|
6571
|
+
return { content };
|
|
5714
6572
|
} catch (error) {
|
|
6573
|
+
done({ refused: errorMessage(error) });
|
|
5715
6574
|
throw this.fsError(error);
|
|
5716
6575
|
}
|
|
5717
6576
|
},
|
|
5718
6577
|
writeTextFile: async (params) => {
|
|
6578
|
+
const done = this.otel.fs("write", params.path);
|
|
5719
6579
|
if (this.readOnly) {
|
|
5720
6580
|
this.log.note(`refused a write (read-only): ${params.path}`);
|
|
6581
|
+
done({ refused: "read-only" });
|
|
5721
6582
|
throw new AcpError(RESOURCE_REFUSED, READ_ONLY_MESSAGE, {
|
|
5722
6583
|
path: params.path
|
|
5723
6584
|
});
|
|
5724
6585
|
}
|
|
5725
6586
|
try {
|
|
5726
6587
|
this.fs.write(params.path, params.content);
|
|
6588
|
+
done({ bytes: Buffer.byteLength(params.content) });
|
|
5727
6589
|
} catch (error) {
|
|
6590
|
+
done({ refused: errorMessage(error) });
|
|
5728
6591
|
throw this.fsError(error);
|
|
5729
6592
|
}
|
|
5730
6593
|
},
|
|
5731
|
-
createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId))),
|
|
6594
|
+
createElicitation: (params) => this.otel.around("elicitation.wait", {}, () => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId)))),
|
|
5732
6595
|
unknown: (method) => this.log.note(`unhandled ${method}`)
|
|
5733
6596
|
}
|
|
5734
6597
|
});
|
|
@@ -5739,7 +6602,20 @@ class Turn {
|
|
|
5739
6602
|
const conn = this.conn;
|
|
5740
6603
|
ctx.setPid(conn.pid);
|
|
5741
6604
|
ctx.setState("starting");
|
|
5742
|
-
|
|
6605
|
+
this.otel.spawned({
|
|
6606
|
+
"process.pid": conn.pid,
|
|
6607
|
+
"hypotenuse.agent.id": this.agentId,
|
|
6608
|
+
"hypotenuse.agent.preset": launch.preset ?? "custom",
|
|
6609
|
+
"hypotenuse.telemetry.native": this.receiver !== null
|
|
6610
|
+
}, launch.command.join(" "));
|
|
6611
|
+
const init = await this.otel.around("acp.initialize", { "rpc.method": "initialize" }, () => this.guard(conn.initialize(package_default.version), launch.startupTimeoutMs, `${this.agentId} did not answer initialize within ${launch.startupTimeoutMs / 1000}s (\`${launch.command.join(" ")}\`).`, true));
|
|
6612
|
+
this.otel.initialized({
|
|
6613
|
+
"gen_ai.agent.name": init.agentInfo?.name ?? this.agentId,
|
|
6614
|
+
"hypotenuse.agent.version": init.agentInfo?.version ?? null,
|
|
6615
|
+
"hypotenuse.acp.protocol_version": init.protocolVersion,
|
|
6616
|
+
"hypotenuse.acp.load_session": Boolean(init.agentCapabilities?.loadSession),
|
|
6617
|
+
"hypotenuse.acp.auth_methods": (init.authMethods ?? []).length
|
|
6618
|
+
});
|
|
5743
6619
|
this.agentTitle = init.agentInfo?.title ?? init.agentInfo?.name ?? this.agentId;
|
|
5744
6620
|
this.agentVersion = init.agentInfo?.version ?? null;
|
|
5745
6621
|
ctx.log.debug(`${this.agentId} initialized: ${init.agentInfo?.name ?? "?"} ${init.agentInfo?.version ?? ""}`, { session: assignment.agentSessionId });
|
|
@@ -5795,7 +6671,7 @@ class Turn {
|
|
|
5795
6671
|
if (!method)
|
|
5796
6672
|
throw new AuthRequired(this.loginNeed(), "not logged in");
|
|
5797
6673
|
this.log.note(`authenticate ${method}`);
|
|
5798
|
-
await this.guard(this.conn.authenticate(method), 0, "");
|
|
6674
|
+
await this.otel.around("acp.authenticate", { "hypotenuse.acp.auth_method": method }, () => this.guard(this.conn.authenticate(method), 0, ""));
|
|
5799
6675
|
authenticated = true;
|
|
5800
6676
|
}
|
|
5801
6677
|
}
|
|
@@ -5823,12 +6699,12 @@ class Turn {
|
|
|
5823
6699
|
if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
|
|
5824
6700
|
try {
|
|
5825
6701
|
if (caps.sessionCapabilities?.resume) {
|
|
5826
|
-
const answer = await this.guard(conn.resumeSession({
|
|
6702
|
+
const answer = await this.otel.around("acp.session.resume", {}, () => this.guard(conn.resumeSession({
|
|
5827
6703
|
sessionId: previousAcpSessionId,
|
|
5828
6704
|
cwd: this.cwd,
|
|
5829
6705
|
mcpServers,
|
|
5830
6706
|
...directories
|
|
5831
|
-
}), 0, "");
|
|
6707
|
+
}), 0, ""));
|
|
5832
6708
|
this.acpSessionId = previousAcpSessionId;
|
|
5833
6709
|
this.resumed = true;
|
|
5834
6710
|
await this.applyMode(answer);
|
|
@@ -5838,12 +6714,12 @@ class Turn {
|
|
|
5838
6714
|
this.acpSessionId = previousAcpSessionId;
|
|
5839
6715
|
this.mapper.replaying = true;
|
|
5840
6716
|
try {
|
|
5841
|
-
const answer = await this.guard(conn.loadSession({
|
|
6717
|
+
const answer = await this.otel.around("acp.session.load", {}, () => this.guard(conn.loadSession({
|
|
5842
6718
|
sessionId: previousAcpSessionId,
|
|
5843
6719
|
cwd: this.cwd,
|
|
5844
6720
|
mcpServers,
|
|
5845
6721
|
...directories
|
|
5846
|
-
}), 0, "");
|
|
6722
|
+
}), 0, ""));
|
|
5847
6723
|
this.resumed = true;
|
|
5848
6724
|
await this.applyMode(answer);
|
|
5849
6725
|
return;
|
|
@@ -5859,7 +6735,7 @@ class Turn {
|
|
|
5859
6735
|
this.ctx.log.info(`could not resume the agent's session; starting a new one with the history in the prompt: ${errorMessage(error)}`, { session: assignment.agentSessionId });
|
|
5860
6736
|
}
|
|
5861
6737
|
}
|
|
5862
|
-
const created = await this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, "");
|
|
6738
|
+
const created = await this.otel.around("acp.session.new", { "hypotenuse.mcp.servers": mcpServers.length }, () => this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, ""));
|
|
5863
6739
|
this.acpSessionId = created.sessionId;
|
|
5864
6740
|
await this.applyMode(created);
|
|
5865
6741
|
}
|
|
@@ -5931,6 +6807,17 @@ class Turn {
|
|
|
5931
6807
|
}, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
|
|
5932
6808
|
this.promptStartedAt = this.now();
|
|
5933
6809
|
this.lastTraffic = this.now();
|
|
6810
|
+
const promptText = blocks.map((b) => b.text ?? "").join(`
|
|
6811
|
+
|
|
6812
|
+
`);
|
|
6813
|
+
this.otel.promptStarted({
|
|
6814
|
+
"hypotenuse.prompt.blocks": blocks.length,
|
|
6815
|
+
"hypotenuse.prompt.bytes": Buffer.byteLength(promptText),
|
|
6816
|
+
"hypotenuse.prompt.history": ctx.assignment.history.length,
|
|
6817
|
+
"hypotenuse.prompt.guidance": ctx.assignment.guidance.length,
|
|
6818
|
+
"hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
|
|
6819
|
+
"hypotenuse.acp.resumed": this.resumed
|
|
6820
|
+
}, promptText);
|
|
5934
6821
|
const request = conn.prompt(sessionId, blocks);
|
|
5935
6822
|
this.promptSettled = request.then(() => {}, () => {});
|
|
5936
6823
|
const answer = await this.guard(request, 0, "").catch((error) => {
|
|
@@ -5938,6 +6825,12 @@ class Turn {
|
|
|
5938
6825
|
throw error;
|
|
5939
6826
|
throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
|
|
5940
6827
|
});
|
|
6828
|
+
const usage = answer.usage;
|
|
6829
|
+
this.otel.promptEnded({
|
|
6830
|
+
"gen_ai.response.finish_reasons": answer.stopReason,
|
|
6831
|
+
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
6832
|
+
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
6833
|
+
}, answer.stopReason === "end_turn" || answer.stopReason === "cancelled" ? null : `stopReason: ${answer.stopReason}`);
|
|
5941
6834
|
if (this.endReason)
|
|
5942
6835
|
return await this.stopped();
|
|
5943
6836
|
this.emit({ type: "status", state: "finishing" });
|
|
@@ -6022,6 +6915,33 @@ class Turn {
|
|
|
6022
6915
|
await within(conn.exited, 2000, null);
|
|
6023
6916
|
}
|
|
6024
6917
|
async shutdown() {
|
|
6918
|
+
await this.closeAgent();
|
|
6919
|
+
if (this.conn)
|
|
6920
|
+
this.otel.exited(await within(this.conn.exited, 500, null));
|
|
6921
|
+
await this.receiver?.close();
|
|
6922
|
+
this.receiver = null;
|
|
6923
|
+
}
|
|
6924
|
+
withTelemetry(launch) {
|
|
6925
|
+
const turn = this.ctx.telemetry;
|
|
6926
|
+
if (!turn)
|
|
6927
|
+
return;
|
|
6928
|
+
const traceparent = this.otel.traceparent;
|
|
6929
|
+
if (traceparent)
|
|
6930
|
+
launch.env.TRACEPARENT = traceparent;
|
|
6931
|
+
if (!turn.agentNative)
|
|
6932
|
+
return;
|
|
6933
|
+
this.receiver = new NativeReceiver(turn);
|
|
6934
|
+
const endpoint = this.receiver.start();
|
|
6935
|
+
turn.addSecret(this.receiver.nonce);
|
|
6936
|
+
dropInheritedOtel(launch.env);
|
|
6937
|
+
const native = nativeTelemetryLaunch(launch.preset, {
|
|
6938
|
+
endpoint,
|
|
6939
|
+
level: turn.level
|
|
6940
|
+
});
|
|
6941
|
+
Object.assign(launch.env, native.env);
|
|
6942
|
+
launch.command = [...launch.command, ...native.args];
|
|
6943
|
+
}
|
|
6944
|
+
async closeAgent() {
|
|
6025
6945
|
const conn = this.conn;
|
|
6026
6946
|
if (!conn)
|
|
6027
6947
|
return;
|
|
@@ -6074,13 +6994,26 @@ class Turn {
|
|
|
6074
6994
|
async permission(params) {
|
|
6075
6995
|
const policy = this.role?.permission_policy ?? "allow";
|
|
6076
6996
|
const decision = decide(policy, params.toolCall.kind);
|
|
6997
|
+
const waited = this.otel.permission(params.toolCall, typeof policy === "string" ? policy : JSON.stringify(policy), params.options);
|
|
6077
6998
|
if (decision !== "ask") {
|
|
6078
6999
|
const optionId = automaticOption(params.options, decision);
|
|
6079
7000
|
this.log.note(`permission ${params.toolCall.toolCallId}: ${decision} \u2192 ${optionId ?? "cancelled"}`);
|
|
7001
|
+
waited({
|
|
7002
|
+
outcome: optionId ? "selected" : "cancelled",
|
|
7003
|
+
optionId: optionId ?? undefined,
|
|
7004
|
+
decidedBy: "policy"
|
|
7005
|
+
});
|
|
6080
7006
|
return optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } };
|
|
6081
7007
|
}
|
|
6082
7008
|
this.emitAll(this.mapper.flushThought(true));
|
|
6083
|
-
|
|
7009
|
+
const answer = await this.questions.ask(permissionQuestion(params, this.where, this.cwd));
|
|
7010
|
+
const outcome = answer.outcome;
|
|
7011
|
+
waited({
|
|
7012
|
+
outcome: outcome?.outcome ?? "cancelled",
|
|
7013
|
+
optionId: outcome?.optionId,
|
|
7014
|
+
decidedBy: "person"
|
|
7015
|
+
});
|
|
7016
|
+
return answer;
|
|
6084
7017
|
}
|
|
6085
7018
|
fsError(error) {
|
|
6086
7019
|
if (error instanceof OutsideWorktree) {
|
|
@@ -6282,6 +7215,9 @@ class DaemonApi {
|
|
|
6282
7215
|
"User-Agent": `hypotenuse/${package_default.version} (${platform()})`
|
|
6283
7216
|
};
|
|
6284
7217
|
}
|
|
7218
|
+
traced(traceparent) {
|
|
7219
|
+
return traceparent ? { ...this.headers, [TRACEPARENT_HEADER]: traceparent } : this.headers;
|
|
7220
|
+
}
|
|
6285
7221
|
async heartbeat(body) {
|
|
6286
7222
|
const answer = await this.client.request("POST", "/daemon/heartbeat", {
|
|
6287
7223
|
body,
|
|
@@ -6309,26 +7245,41 @@ class DaemonApi {
|
|
|
6309
7245
|
});
|
|
6310
7246
|
return ClaimResponse.parse(answer).assignment;
|
|
6311
7247
|
}
|
|
6312
|
-
async events(body) {
|
|
7248
|
+
async events(body, traceparent) {
|
|
6313
7249
|
const answer = await this.client.request("POST", "/daemon/events", {
|
|
6314
7250
|
body,
|
|
6315
|
-
headers: this.
|
|
7251
|
+
headers: this.traced(traceparent),
|
|
6316
7252
|
timeoutMs: REQUEST_TIMEOUT_MS
|
|
6317
7253
|
});
|
|
6318
7254
|
return EventsResponse.parse(answer);
|
|
6319
7255
|
}
|
|
6320
|
-
async complete(body) {
|
|
7256
|
+
async complete(body, traceparent) {
|
|
6321
7257
|
const answer = await this.client.request("POST", "/daemon/complete", {
|
|
6322
7258
|
body,
|
|
6323
|
-
headers: this.
|
|
7259
|
+
headers: this.traced(traceparent),
|
|
6324
7260
|
timeoutMs: REQUEST_TIMEOUT_MS
|
|
6325
7261
|
});
|
|
6326
7262
|
return CompleteResponse.parse(answer);
|
|
6327
7263
|
}
|
|
6328
|
-
async
|
|
7264
|
+
async telemetry(batch) {
|
|
7265
|
+
const answer = await this.client.request("POST", "/daemon/telemetry", {
|
|
7266
|
+
rawBody: batch.gzipped,
|
|
7267
|
+
headers: {
|
|
7268
|
+
...this.headers,
|
|
7269
|
+
"Content-Type": "application/json",
|
|
7270
|
+
"Content-Encoding": "gzip",
|
|
7271
|
+
[TELEMETRY_SIGNAL_HEADER]: batch.signal,
|
|
7272
|
+
[TELEMETRY_WORK_ITEM_HEADER]: String(batch.workItemId),
|
|
7273
|
+
[TRACEPARENT_HEADER]: batch.traceparent
|
|
7274
|
+
},
|
|
7275
|
+
timeoutMs: REQUEST_TIMEOUT_MS
|
|
7276
|
+
});
|
|
7277
|
+
return TelemetryResponse.parse(answer);
|
|
7278
|
+
}
|
|
7279
|
+
async tool(route, body, traceparent) {
|
|
6329
7280
|
const answer = await this.client.request("POST", route, {
|
|
6330
7281
|
body,
|
|
6331
|
-
headers: this.
|
|
7282
|
+
headers: this.traced(traceparent),
|
|
6332
7283
|
timeoutMs: TOOL_REQUEST_TIMEOUT_MS
|
|
6333
7284
|
});
|
|
6334
7285
|
return ToolResult.parse(answer).result;
|
|
@@ -6953,11 +7904,575 @@ class StateDb {
|
|
|
6953
7904
|
|
|
6954
7905
|
// src/daemon/supervisor.ts
|
|
6955
7906
|
import { hostname as hostname3 } from "os";
|
|
6956
|
-
import { isAbsolute as
|
|
7907
|
+
import { isAbsolute as isAbsolute7 } from "path";
|
|
7908
|
+
|
|
7909
|
+
// src/telemetry/collector.ts
|
|
7910
|
+
var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
|
|
7911
|
+
var TOKEN_BOUNDS = [
|
|
7912
|
+
100,
|
|
7913
|
+
1000,
|
|
7914
|
+
1e4,
|
|
7915
|
+
50000,
|
|
7916
|
+
1e5,
|
|
7917
|
+
250000,
|
|
7918
|
+
500000,
|
|
7919
|
+
1e6
|
|
7920
|
+
];
|
|
7921
|
+
|
|
7922
|
+
class SpanHandle {
|
|
7923
|
+
turn;
|
|
7924
|
+
name;
|
|
7925
|
+
parentSpanId;
|
|
7926
|
+
kind;
|
|
7927
|
+
spanId;
|
|
7928
|
+
startMs;
|
|
7929
|
+
attrs = {};
|
|
7930
|
+
events = [];
|
|
7931
|
+
ended = false;
|
|
7932
|
+
error = null;
|
|
7933
|
+
constructor(turn, name, parentSpanId, kind = "internal", attrs = {}, spanId = newSpanId()) {
|
|
7934
|
+
this.turn = turn;
|
|
7935
|
+
this.name = name;
|
|
7936
|
+
this.parentSpanId = parentSpanId;
|
|
7937
|
+
this.kind = kind;
|
|
7938
|
+
this.spanId = spanId;
|
|
7939
|
+
this.startMs = turn.now();
|
|
7940
|
+
Object.assign(this.attrs, attrs);
|
|
7941
|
+
}
|
|
7942
|
+
set(attrs) {
|
|
7943
|
+
Object.assign(this.attrs, attrs);
|
|
7944
|
+
return this;
|
|
7945
|
+
}
|
|
7946
|
+
content(key, text) {
|
|
7947
|
+
Object.assign(this.attrs, this.turn.contentAttrs(key, text));
|
|
7948
|
+
return this;
|
|
7949
|
+
}
|
|
7950
|
+
event(name, attrs = {}) {
|
|
7951
|
+
if (!this.ended)
|
|
7952
|
+
this.events?.push({
|
|
7953
|
+
name,
|
|
7954
|
+
timeUnixNano: unixNano(this.turn.now()),
|
|
7955
|
+
attributes: attributes(attrs)
|
|
7956
|
+
});
|
|
7957
|
+
return this;
|
|
7958
|
+
}
|
|
7959
|
+
fail(message) {
|
|
7960
|
+
this.error = message.slice(0, 1024);
|
|
7961
|
+
return this;
|
|
7962
|
+
}
|
|
7963
|
+
get traceparent() {
|
|
7964
|
+
return this.turn.traceparentFor(this.spanId);
|
|
7965
|
+
}
|
|
7966
|
+
get isEnded() {
|
|
7967
|
+
return this.ended;
|
|
7968
|
+
}
|
|
7969
|
+
end(attrs = {}) {
|
|
7970
|
+
if (this.ended)
|
|
7971
|
+
return;
|
|
7972
|
+
this.ended = true;
|
|
7973
|
+
Object.assign(this.attrs, attrs);
|
|
7974
|
+
this.turn.record({
|
|
7975
|
+
traceId: this.turn.traceId,
|
|
7976
|
+
spanId: this.spanId,
|
|
7977
|
+
parentSpanId: this.parentSpanId,
|
|
7978
|
+
name: this.name,
|
|
7979
|
+
kind: SPAN_KIND[this.kind],
|
|
7980
|
+
startTimeUnixNano: unixNano(this.startMs),
|
|
7981
|
+
endTimeUnixNano: unixNano(this.turn.now()),
|
|
7982
|
+
attributes: attributes(this.attrs),
|
|
7983
|
+
events: this.events,
|
|
7984
|
+
status: this.error ? { code: STATUS_CODE.error, message: this.error } : { code: STATUS_CODE.unset }
|
|
7985
|
+
});
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
|
|
7989
|
+
class TurnTelemetry {
|
|
7990
|
+
options;
|
|
7991
|
+
traceId;
|
|
7992
|
+
rootSpanId;
|
|
7993
|
+
level;
|
|
7994
|
+
sampled;
|
|
7995
|
+
agentNative;
|
|
7996
|
+
now;
|
|
7997
|
+
turnSpan;
|
|
7998
|
+
resource;
|
|
7999
|
+
finished = false;
|
|
8000
|
+
extraSecrets = [];
|
|
8001
|
+
promptSpan = null;
|
|
8002
|
+
constructor(options) {
|
|
8003
|
+
this.options = options;
|
|
8004
|
+
const ctx = parseTraceparent(options.claim.traceparent);
|
|
8005
|
+
this.traceId = ctx?.traceId ?? "0".repeat(32);
|
|
8006
|
+
this.rootSpanId = ctx?.spanId ?? "0".repeat(16);
|
|
8007
|
+
this.level = options.claim.content;
|
|
8008
|
+
this.sampled = options.claim.sampled && ctx !== null;
|
|
8009
|
+
this.agentNative = options.claim.agentNative;
|
|
8010
|
+
this.now = options.now ?? Date.now;
|
|
8011
|
+
const a = options.assignment;
|
|
8012
|
+
this.resource = attributes({
|
|
8013
|
+
"service.name": "hypotenuse-daemon",
|
|
8014
|
+
"service.namespace": "hypotenuse",
|
|
8015
|
+
"service.version": package_default.version,
|
|
8016
|
+
"hypotenuse.source": "daemon",
|
|
8017
|
+
...this.turnAttributes(),
|
|
8018
|
+
"hypotenuse.repository": a.repository?.remote ?? null
|
|
8019
|
+
});
|
|
8020
|
+
this.turnSpan = new SpanHandle(this, "daemon.turn", this.rootSpanId, "internal", {
|
|
8021
|
+
...this.turnAttributes(),
|
|
8022
|
+
"hypotenuse.work_item.kind": a.kind,
|
|
8023
|
+
"hypotenuse.work_item.fresh": a.fresh,
|
|
8024
|
+
"hypotenuse.telemetry.content": this.level,
|
|
8025
|
+
"hypotenuse.telemetry.agent_native": this.agentNative
|
|
8026
|
+
});
|
|
8027
|
+
}
|
|
8028
|
+
turnAttributes() {
|
|
8029
|
+
const a = this.options.assignment;
|
|
8030
|
+
return {
|
|
8031
|
+
"hypotenuse.session.id": a.agentSessionId,
|
|
8032
|
+
"hypotenuse.work_item.id": a.workItemId,
|
|
8033
|
+
"hypotenuse.role": a.role,
|
|
8034
|
+
"hypotenuse.runner.name": this.options.runner.name,
|
|
8035
|
+
"hypotenuse.agent.id": this.options.agentId,
|
|
8036
|
+
"hypotenuse.agent.preset": this.options.preset ?? "custom",
|
|
8037
|
+
"hypotenuse.repository": a.repository?.remote ?? null,
|
|
8038
|
+
"linear.issue.identifier": a.issue.identifier,
|
|
8039
|
+
"linear.agent_session.id": a.externalSessionId,
|
|
8040
|
+
"linear.team.key": a.issue.teamKey
|
|
8041
|
+
};
|
|
8042
|
+
}
|
|
8043
|
+
addSecret(value) {
|
|
8044
|
+
this.extraSecrets.push(value);
|
|
8045
|
+
}
|
|
8046
|
+
get resourceAttributes() {
|
|
8047
|
+
return this.resource;
|
|
8048
|
+
}
|
|
8049
|
+
collects(level) {
|
|
8050
|
+
return levelRank(this.level) >= levelRank(level);
|
|
8051
|
+
}
|
|
8052
|
+
traceparentFor(spanId = this.turnSpan.spanId) {
|
|
8053
|
+
return formatTraceparent({
|
|
8054
|
+
traceId: this.traceId,
|
|
8055
|
+
spanId,
|
|
8056
|
+
sampled: this.sampled
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
span(name, attrs = {}, parent = null, kind = "internal", spanId) {
|
|
8060
|
+
return new SpanHandle(this, name, (parent ?? this.turnSpan).spanId, kind, attrs, spanId);
|
|
8061
|
+
}
|
|
8062
|
+
contentAttrs(key, text) {
|
|
8063
|
+
if (text === null || text === undefined)
|
|
8064
|
+
return {};
|
|
8065
|
+
const out = {
|
|
8066
|
+
[`hypotenuse.${key}.bytes`]: Buffer.byteLength(text)
|
|
8067
|
+
};
|
|
8068
|
+
if (!this.collects("content"))
|
|
8069
|
+
return out;
|
|
8070
|
+
const { text: kept, originalBytes } = capContent(text);
|
|
8071
|
+
out[`${CONTENT_ATTRIBUTE_PREFIX}${key}`] = kept;
|
|
8072
|
+
if (originalBytes !== null)
|
|
8073
|
+
out[`${CONTENT_ATTRIBUTE_PREFIX}${key}${TRUNCATED_SUFFIX}`] = originalBytes;
|
|
8074
|
+
return out;
|
|
8075
|
+
}
|
|
8076
|
+
record(span) {
|
|
8077
|
+
if (!this.sampled || this.finished)
|
|
8078
|
+
return;
|
|
8079
|
+
this.enqueue("traces", {
|
|
8080
|
+
resourceSpans: [
|
|
8081
|
+
{
|
|
8082
|
+
resource: { attributes: this.resource },
|
|
8083
|
+
scopeSpans: [{ scope: SCOPE, spans: [span] }]
|
|
8084
|
+
}
|
|
8085
|
+
]
|
|
8086
|
+
});
|
|
8087
|
+
}
|
|
8088
|
+
log(record) {
|
|
8089
|
+
const level = record.level ?? "content";
|
|
8090
|
+
if (!this.sampled || this.finished || !this.collects(level))
|
|
8091
|
+
return;
|
|
8092
|
+
const { text, originalBytes } = capContent(record.body);
|
|
8093
|
+
this.enqueue("logs", {
|
|
8094
|
+
resourceLogs: [
|
|
8095
|
+
{
|
|
8096
|
+
resource: { attributes: this.resource },
|
|
8097
|
+
scopeLogs: [
|
|
8098
|
+
{
|
|
8099
|
+
scope: SCOPE,
|
|
8100
|
+
logRecords: [
|
|
8101
|
+
{
|
|
8102
|
+
timeUnixNano: unixNano(this.now()),
|
|
8103
|
+
observedTimeUnixNano: unixNano(this.now()),
|
|
8104
|
+
severityNumber: SEVERITY[record.severity ?? "info"],
|
|
8105
|
+
severityText: (record.severity ?? "info").toUpperCase(),
|
|
8106
|
+
body: { stringValue: text },
|
|
8107
|
+
...record.eventName ? { eventName: record.eventName } : {},
|
|
8108
|
+
attributes: attributes({
|
|
8109
|
+
...record.attrs ?? {},
|
|
8110
|
+
[LEVEL_ATTRIBUTE]: level,
|
|
8111
|
+
...originalBytes !== null ? {
|
|
8112
|
+
[`hypotenuse.body${TRUNCATED_SUFFIX}`]: originalBytes
|
|
8113
|
+
} : {}
|
|
8114
|
+
}),
|
|
8115
|
+
traceId: this.traceId,
|
|
8116
|
+
spanId: (record.span ?? this.turnSpan).spanId
|
|
8117
|
+
}
|
|
8118
|
+
]
|
|
8119
|
+
}
|
|
8120
|
+
]
|
|
8121
|
+
}
|
|
8122
|
+
]
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
frame(direction, message, span) {
|
|
8126
|
+
if (!this.collects("full"))
|
|
8127
|
+
return;
|
|
8128
|
+
const m = message;
|
|
8129
|
+
this.log({
|
|
8130
|
+
body: typeof message === "string" ? message : JSON.stringify(message),
|
|
8131
|
+
level: "full",
|
|
8132
|
+
eventName: "acp.frame",
|
|
8133
|
+
attrs: {
|
|
8134
|
+
"hypotenuse.acp.direction": direction.startsWith("<-") ? "in" : "out",
|
|
8135
|
+
"rpc.method": typeof m?.method === "string" ? m.method : null,
|
|
8136
|
+
"rpc.jsonrpc.request_id": typeof m?.id === "string" || typeof m?.id === "number" ? String(m.id) : null
|
|
8137
|
+
},
|
|
8138
|
+
span
|
|
8139
|
+
});
|
|
8140
|
+
}
|
|
8141
|
+
usage(usage) {
|
|
8142
|
+
if (this.finished)
|
|
8143
|
+
return;
|
|
8144
|
+
const points = [
|
|
8145
|
+
["input", usage.inputTokens],
|
|
8146
|
+
["output", usage.outputTokens]
|
|
8147
|
+
].flatMap(([type, n]) => typeof n === "number" ? [
|
|
8148
|
+
{
|
|
8149
|
+
attributes: attributes({
|
|
8150
|
+
"gen_ai.token.type": type,
|
|
8151
|
+
"gen_ai.agent.name": this.options.agentId
|
|
8152
|
+
}),
|
|
8153
|
+
timeUnixNano: unixNano(this.now()),
|
|
8154
|
+
count: "1",
|
|
8155
|
+
sum: n,
|
|
8156
|
+
bucketCounts: [
|
|
8157
|
+
...TOKEN_BOUNDS.map((b, i) => n <= b && (i === 0 || n > TOKEN_BOUNDS[i - 1]) ? "1" : "0"),
|
|
8158
|
+
n > TOKEN_BOUNDS.at(-1) ? "1" : "0"
|
|
8159
|
+
],
|
|
8160
|
+
explicitBounds: TOKEN_BOUNDS,
|
|
8161
|
+
min: n,
|
|
8162
|
+
max: n
|
|
8163
|
+
}
|
|
8164
|
+
] : []);
|
|
8165
|
+
if (points.length === 0)
|
|
8166
|
+
return;
|
|
8167
|
+
this.metric({
|
|
8168
|
+
name: "gen_ai.client.token.usage",
|
|
8169
|
+
unit: "{token}",
|
|
8170
|
+
histogram: {
|
|
8171
|
+
aggregationTemporality: TEMPORALITY.delta,
|
|
8172
|
+
dataPoints: points
|
|
8173
|
+
}
|
|
8174
|
+
});
|
|
8175
|
+
}
|
|
8176
|
+
slots(slots) {
|
|
8177
|
+
const at = unixNano(this.now());
|
|
8178
|
+
this.metric({
|
|
8179
|
+
name: "hypotenuse.runner.slots",
|
|
8180
|
+
unit: "{slot}",
|
|
8181
|
+
gauge: {
|
|
8182
|
+
dataPoints: [
|
|
8183
|
+
{
|
|
8184
|
+
attributes: attributes({ "hypotenuse.slot.state": "capacity" }),
|
|
8185
|
+
timeUnixNano: at,
|
|
8186
|
+
asInt: String(slots.capacity)
|
|
8187
|
+
},
|
|
8188
|
+
{
|
|
8189
|
+
attributes: attributes({ "hypotenuse.slot.state": "in_use" }),
|
|
8190
|
+
timeUnixNano: at,
|
|
8191
|
+
asInt: String(slots.inUse)
|
|
8192
|
+
}
|
|
8193
|
+
]
|
|
8194
|
+
}
|
|
8195
|
+
});
|
|
8196
|
+
}
|
|
8197
|
+
metric(metric) {
|
|
8198
|
+
this.enqueue("metrics", {
|
|
8199
|
+
resourceMetrics: [
|
|
8200
|
+
{
|
|
8201
|
+
resource: { attributes: this.resource },
|
|
8202
|
+
scopeMetrics: [{ scope: SCOPE, metrics: [metric] }]
|
|
8203
|
+
}
|
|
8204
|
+
]
|
|
8205
|
+
});
|
|
8206
|
+
}
|
|
8207
|
+
enqueue(signal, body) {
|
|
8208
|
+
const refused = this.options.queue.isRefused(this.options.assignment.workItemId);
|
|
8209
|
+
this.options.queue.add({
|
|
8210
|
+
workItemId: this.options.assignment.workItemId,
|
|
8211
|
+
traceparent: this.traceparentFor(),
|
|
8212
|
+
signal,
|
|
8213
|
+
body: refused ? body : redactOtlp(body, {
|
|
8214
|
+
values: [...this.options.secrets(), ...this.extraSecrets]
|
|
8215
|
+
})
|
|
8216
|
+
}, this.resource);
|
|
8217
|
+
}
|
|
8218
|
+
finish(outcome, error) {
|
|
8219
|
+
if (this.finished)
|
|
8220
|
+
return;
|
|
8221
|
+
if (error)
|
|
8222
|
+
this.turnSpan.fail(error);
|
|
8223
|
+
this.turnSpan.end({ "hypotenuse.outcome": outcome });
|
|
8224
|
+
this.finished = true;
|
|
8225
|
+
}
|
|
8226
|
+
}
|
|
8227
|
+
|
|
8228
|
+
// src/telemetry/queue.ts
|
|
8229
|
+
var FLUSH_INTERVAL_MS = 2000;
|
|
8230
|
+
var FLUSH_BYTES = 1024 * 1024;
|
|
8231
|
+
|
|
8232
|
+
class TelemetryQueue {
|
|
8233
|
+
sender;
|
|
8234
|
+
capacityBytes;
|
|
8235
|
+
log;
|
|
8236
|
+
now;
|
|
8237
|
+
batches = [];
|
|
8238
|
+
bytes = 0;
|
|
8239
|
+
timer = null;
|
|
8240
|
+
flushing = null;
|
|
8241
|
+
backoffUntil = 0;
|
|
8242
|
+
failures = 0;
|
|
8243
|
+
refused = new Set;
|
|
8244
|
+
losses = new Map;
|
|
8245
|
+
droppedTotal = new Map;
|
|
8246
|
+
constructor(sender, capacityBytes, log = () => {}, now = Date.now) {
|
|
8247
|
+
this.sender = sender;
|
|
8248
|
+
this.capacityBytes = capacityBytes;
|
|
8249
|
+
this.log = log;
|
|
8250
|
+
this.now = now;
|
|
8251
|
+
}
|
|
8252
|
+
start() {
|
|
8253
|
+
if (this.timer)
|
|
8254
|
+
return;
|
|
8255
|
+
this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
8256
|
+
this.timer.unref?.();
|
|
8257
|
+
}
|
|
8258
|
+
stop() {
|
|
8259
|
+
if (this.timer)
|
|
8260
|
+
clearInterval(this.timer);
|
|
8261
|
+
this.timer = null;
|
|
8262
|
+
}
|
|
8263
|
+
get pendingBytes() {
|
|
8264
|
+
return this.bytes;
|
|
8265
|
+
}
|
|
8266
|
+
isRefused(workItemId) {
|
|
8267
|
+
return this.refused.has(workItemId);
|
|
8268
|
+
}
|
|
8269
|
+
add(batch, resource = []) {
|
|
8270
|
+
if (this.refused.has(batch.workItemId)) {
|
|
8271
|
+
this.countLoss(batch, resource);
|
|
8272
|
+
return;
|
|
8273
|
+
}
|
|
8274
|
+
const bytes = JSON.stringify(batch.body).length;
|
|
8275
|
+
this.batches.push({ ...batch, bytes });
|
|
8276
|
+
this.bytes += bytes;
|
|
8277
|
+
if (!this.losses.has(batch.workItemId))
|
|
8278
|
+
this.losses.set(batch.workItemId, {
|
|
8279
|
+
spans: 0,
|
|
8280
|
+
logs: 0,
|
|
8281
|
+
resource,
|
|
8282
|
+
traceparent: batch.traceparent
|
|
8283
|
+
});
|
|
8284
|
+
this.evict();
|
|
8285
|
+
if (this.bytes >= FLUSH_BYTES)
|
|
8286
|
+
this.flush();
|
|
8287
|
+
}
|
|
8288
|
+
countLoss(batch, resource = []) {
|
|
8289
|
+
const loss = this.losses.get(batch.workItemId) ?? {
|
|
8290
|
+
spans: 0,
|
|
8291
|
+
logs: 0,
|
|
8292
|
+
resource,
|
|
8293
|
+
traceparent: batch.traceparent
|
|
8294
|
+
};
|
|
8295
|
+
const n = countRecords(batch.signal, batch.body);
|
|
8296
|
+
if (batch.signal === "traces")
|
|
8297
|
+
loss.spans += n;
|
|
8298
|
+
else if (batch.signal === "logs")
|
|
8299
|
+
loss.logs += n;
|
|
8300
|
+
this.losses.set(batch.workItemId, loss);
|
|
8301
|
+
this.droppedTotal.set(batch.workItemId, (this.droppedTotal.get(batch.workItemId) ?? 0) + n);
|
|
8302
|
+
}
|
|
8303
|
+
evict() {
|
|
8304
|
+
for (const signal of ["logs", "traces"]) {
|
|
8305
|
+
while (this.bytes > this.capacityBytes) {
|
|
8306
|
+
const index = this.batches.findIndex((b) => b.signal === signal);
|
|
8307
|
+
if (index === -1)
|
|
8308
|
+
break;
|
|
8309
|
+
const [gone] = this.batches.splice(index, 1);
|
|
8310
|
+
if (!gone)
|
|
8311
|
+
break;
|
|
8312
|
+
this.bytes -= gone.bytes;
|
|
8313
|
+
this.countLoss(gone);
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
reportLosses() {
|
|
8318
|
+
for (const [workItemId, loss] of this.losses) {
|
|
8319
|
+
if (this.refused.has(workItemId))
|
|
8320
|
+
continue;
|
|
8321
|
+
if (loss.spans === 0 && loss.logs === 0)
|
|
8322
|
+
continue;
|
|
8323
|
+
const at = unixNano(this.now());
|
|
8324
|
+
const point = (kind, n) => ({
|
|
8325
|
+
attributes: attributes({ "hypotenuse.telemetry.kind": kind }),
|
|
8326
|
+
timeUnixNano: at,
|
|
8327
|
+
asInt: String(n)
|
|
8328
|
+
});
|
|
8329
|
+
const metrics = {
|
|
8330
|
+
resourceMetrics: [
|
|
8331
|
+
{
|
|
8332
|
+
resource: { attributes: loss.resource },
|
|
8333
|
+
scopeMetrics: [
|
|
8334
|
+
{
|
|
8335
|
+
scope: { name: "hypotenuse.daemon" },
|
|
8336
|
+
metrics: [
|
|
8337
|
+
{
|
|
8338
|
+
name: "hypotenuse.telemetry.dropped",
|
|
8339
|
+
unit: "{record}",
|
|
8340
|
+
sum: {
|
|
8341
|
+
aggregationTemporality: TEMPORALITY.delta,
|
|
8342
|
+
isMonotonic: true,
|
|
8343
|
+
dataPoints: [
|
|
8344
|
+
...loss.spans ? [point("span", loss.spans)] : [],
|
|
8345
|
+
...loss.logs ? [point("log", loss.logs)] : []
|
|
8346
|
+
]
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
]
|
|
8350
|
+
}
|
|
8351
|
+
]
|
|
8352
|
+
}
|
|
8353
|
+
]
|
|
8354
|
+
};
|
|
8355
|
+
loss.spans = 0;
|
|
8356
|
+
loss.logs = 0;
|
|
8357
|
+
const bytes = JSON.stringify(metrics).length;
|
|
8358
|
+
this.batches.push({
|
|
8359
|
+
workItemId,
|
|
8360
|
+
traceparent: loss.traceparent,
|
|
8361
|
+
signal: "metrics",
|
|
8362
|
+
body: metrics,
|
|
8363
|
+
bytes
|
|
8364
|
+
});
|
|
8365
|
+
this.bytes += bytes;
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
flush() {
|
|
8369
|
+
this.flushing ??= this.flushOnce().finally(() => {
|
|
8370
|
+
this.flushing = null;
|
|
8371
|
+
});
|
|
8372
|
+
return this.flushing;
|
|
8373
|
+
}
|
|
8374
|
+
async flushOnce() {
|
|
8375
|
+
if (this.now() < this.backoffUntil)
|
|
8376
|
+
return;
|
|
8377
|
+
this.reportLosses();
|
|
8378
|
+
if (this.batches.length === 0)
|
|
8379
|
+
return;
|
|
8380
|
+
const taken = this.batches;
|
|
8381
|
+
this.batches = [];
|
|
8382
|
+
this.bytes = 0;
|
|
8383
|
+
const groups = new Map;
|
|
8384
|
+
for (const b of taken) {
|
|
8385
|
+
const key = `${b.workItemId}|${b.signal}`;
|
|
8386
|
+
groups.set(key, [...groups.get(key) ?? [], b]);
|
|
8387
|
+
}
|
|
8388
|
+
const retry = [];
|
|
8389
|
+
let failed = false;
|
|
8390
|
+
for (const group of groups.values()) {
|
|
8391
|
+
const first = group[0];
|
|
8392
|
+
if (this.refused.has(first.workItemId)) {
|
|
8393
|
+
for (const b of group)
|
|
8394
|
+
this.countLoss(b);
|
|
8395
|
+
continue;
|
|
8396
|
+
}
|
|
8397
|
+
if (failed) {
|
|
8398
|
+
retry.push(...group);
|
|
8399
|
+
continue;
|
|
8400
|
+
}
|
|
8401
|
+
const body = mergeRequests(first.signal, group.map((b) => b.body));
|
|
8402
|
+
try {
|
|
8403
|
+
await this.sender.telemetry({
|
|
8404
|
+
workItemId: first.workItemId,
|
|
8405
|
+
traceparent: first.traceparent,
|
|
8406
|
+
signal: first.signal,
|
|
8407
|
+
gzipped: Bun.gzipSync(new TextEncoder().encode(JSON.stringify(body)))
|
|
8408
|
+
});
|
|
8409
|
+
this.failures = 0;
|
|
8410
|
+
} catch (error) {
|
|
8411
|
+
if (error instanceof ApiError && (error.status === 409 || error.status === 410 || error.status === 400)) {
|
|
8412
|
+
this.refused.add(first.workItemId);
|
|
8413
|
+
for (const b of group)
|
|
8414
|
+
this.countLoss(b);
|
|
8415
|
+
this.log(`telemetry for work item ${first.workItemId} refused (${error.status}): ${error.message}`);
|
|
8416
|
+
continue;
|
|
8417
|
+
}
|
|
8418
|
+
failed = true;
|
|
8419
|
+
retry.push(...group);
|
|
8420
|
+
this.failures++;
|
|
8421
|
+
const wait = error instanceof ApiError && error.retryAfterMs ? error.retryAfterMs : Math.min(1000 * 2 ** (this.failures - 1), 60000);
|
|
8422
|
+
this.backoffUntil = this.now() + wait;
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
this.batches = [...retry, ...this.batches];
|
|
8426
|
+
this.bytes = this.batches.reduce((n, b) => n + b.bytes, 0);
|
|
8427
|
+
this.evict();
|
|
8428
|
+
}
|
|
8429
|
+
async drain(timeoutMs) {
|
|
8430
|
+
const deadline = this.now() + timeoutMs;
|
|
8431
|
+
for (;; ) {
|
|
8432
|
+
this.backoffUntil = Math.min(this.backoffUntil, this.now());
|
|
8433
|
+
await this.flush();
|
|
8434
|
+
if (this.batches.length === 0)
|
|
8435
|
+
return true;
|
|
8436
|
+
if (this.now() >= deadline)
|
|
8437
|
+
return false;
|
|
8438
|
+
await Bun.sleep(Math.min(500, Math.max(0, deadline - this.now())));
|
|
8439
|
+
}
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
function countRecords(signal, body) {
|
|
8443
|
+
if (signal === "traces")
|
|
8444
|
+
return (body.resourceSpans ?? []).reduce((n, rs) => n + (rs.scopeSpans ?? []).reduce((m, s) => m + (s.spans?.length ?? 0), 0), 0);
|
|
8445
|
+
if (signal === "logs")
|
|
8446
|
+
return (body.resourceLogs ?? []).reduce((n, rl) => n + (rl.scopeLogs ?? []).reduce((m, s) => m + (s.logRecords?.length ?? 0), 0), 0);
|
|
8447
|
+
return 0;
|
|
8448
|
+
}
|
|
8449
|
+
|
|
8450
|
+
// src/telemetry/redact.ts
|
|
8451
|
+
function resolvedConfigSecrets(config, env) {
|
|
8452
|
+
const out = [];
|
|
8453
|
+
const take = (table) => {
|
|
8454
|
+
for (const value of Object.values(table ?? {})) {
|
|
8455
|
+
if (!value.includes("$"))
|
|
8456
|
+
continue;
|
|
8457
|
+
const resolved = expandVariables(value, env);
|
|
8458
|
+
if (resolved)
|
|
8459
|
+
out.push(resolved);
|
|
8460
|
+
}
|
|
8461
|
+
};
|
|
8462
|
+
for (const agent of Object.values(config.agents))
|
|
8463
|
+
take(agent.env);
|
|
8464
|
+
for (const role of [config.roles.manager, config.roles.worker]) {
|
|
8465
|
+
for (const server of role?.extra_mcp_servers ?? []) {
|
|
8466
|
+
take(server.env);
|
|
8467
|
+
take(server.headers);
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
return out;
|
|
8471
|
+
}
|
|
6957
8472
|
|
|
6958
8473
|
// src/worktree/cleanup.ts
|
|
6959
8474
|
import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
|
|
6960
|
-
import { join as join9, relative as
|
|
8475
|
+
import { join as join9, relative as relative4, resolve as resolve3 } from "path";
|
|
6961
8476
|
|
|
6962
8477
|
// src/worktree/merge-state.ts
|
|
6963
8478
|
var MERGE_STATE_TTL_MS = 10 * 60000;
|
|
@@ -7136,7 +8651,7 @@ class WorktreeCleaner {
|
|
|
7136
8651
|
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
7137
8652
|
if (!path || !branch || known.has(path))
|
|
7138
8653
|
continue;
|
|
7139
|
-
const rel =
|
|
8654
|
+
const rel = relative4(root, path);
|
|
7140
8655
|
if (rel.startsWith("..") || rel === "")
|
|
7141
8656
|
continue;
|
|
7142
8657
|
const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
|
|
@@ -7361,6 +8876,11 @@ function buildCapabilities(config, machine, repositories) {
|
|
|
7361
8876
|
teams: config.subscriptions.teams,
|
|
7362
8877
|
projects: config.subscriptions.projects,
|
|
7363
8878
|
labels: config.subscriptions.labels
|
|
8879
|
+
},
|
|
8880
|
+
telemetry: {
|
|
8881
|
+
enabled: config.telemetry.enabled,
|
|
8882
|
+
maxContent: config.telemetry.max_content,
|
|
8883
|
+
agentNative: config.telemetry.agent_native
|
|
7364
8884
|
}
|
|
7365
8885
|
};
|
|
7366
8886
|
}
|
|
@@ -7434,6 +8954,7 @@ class EventSink {
|
|
|
7434
8954
|
backoff = new Backoff;
|
|
7435
8955
|
batchLimit = new Map;
|
|
7436
8956
|
closed = false;
|
|
8957
|
+
traceparents = new Map;
|
|
7437
8958
|
constructor(state, api, log, hooks) {
|
|
7438
8959
|
this.state = state;
|
|
7439
8960
|
this.api = api;
|
|
@@ -7460,6 +8981,12 @@ class EventSink {
|
|
|
7460
8981
|
this.kick();
|
|
7461
8982
|
return { eventId, seq };
|
|
7462
8983
|
}
|
|
8984
|
+
trace(workItemId, traceparent) {
|
|
8985
|
+
if (traceparent)
|
|
8986
|
+
this.traceparents.set(workItemId, traceparent);
|
|
8987
|
+
else
|
|
8988
|
+
this.traceparents.delete(workItemId);
|
|
8989
|
+
}
|
|
7463
8990
|
complete(completion) {
|
|
7464
8991
|
this.state.queueCompletion(completion.request.workItemId, completion);
|
|
7465
8992
|
this.kick();
|
|
@@ -7574,7 +9101,7 @@ class EventSink {
|
|
|
7574
9101
|
const answer = await this.api.events({
|
|
7575
9102
|
workItemId,
|
|
7576
9103
|
events: batch.map((row) => row.payload)
|
|
7577
|
-
});
|
|
9104
|
+
}, this.traceparents.get(workItemId));
|
|
7578
9105
|
this.state.deleteEvents(ids);
|
|
7579
9106
|
this.log.debug("events sent", {
|
|
7580
9107
|
workItem: workItemId,
|
|
@@ -7632,7 +9159,8 @@ class EventSink {
|
|
|
7632
9159
|
return;
|
|
7633
9160
|
const { request } = row.payload;
|
|
7634
9161
|
try {
|
|
7635
|
-
const answer = await this.api.complete(request);
|
|
9162
|
+
const answer = await this.api.complete(request, this.traceparents.get(workItemId));
|
|
9163
|
+
this.traceparents.delete(workItemId);
|
|
7636
9164
|
this.log.info(`reported work item ${request.outcome}`, {
|
|
7637
9165
|
workItem: workItemId,
|
|
7638
9166
|
alreadyTerminal: answer.alreadyTerminal
|
|
@@ -7702,7 +9230,7 @@ class ReadCache {
|
|
|
7702
9230
|
}
|
|
7703
9231
|
|
|
7704
9232
|
// src/mcp/tools.ts
|
|
7705
|
-
import { z as
|
|
9233
|
+
import { z as z11 } from "zod";
|
|
7706
9234
|
var CACHED_TOOLS = new Set([
|
|
7707
9235
|
"linear_get_issue",
|
|
7708
9236
|
"linear_list_comments",
|
|
@@ -7710,7 +9238,7 @@ var CACHED_TOOLS = new Set([
|
|
|
7710
9238
|
"linear_list_labels"
|
|
7711
9239
|
]);
|
|
7712
9240
|
function describeTool(tool) {
|
|
7713
|
-
const { $schema: _schema, ...schema } =
|
|
9241
|
+
const { $schema: _schema, ...schema } = z11.toJSONSchema(tool.input);
|
|
7714
9242
|
return {
|
|
7715
9243
|
name: tool.name,
|
|
7716
9244
|
description: tool.description,
|
|
@@ -7789,11 +9317,14 @@ class McpBroker {
|
|
|
7789
9317
|
if (!checked.ok)
|
|
7790
9318
|
return { text: checked.message, isError: true };
|
|
7791
9319
|
const { tool, input } = checked;
|
|
9320
|
+
const span = turn.trace?.(tool.name, input) ?? null;
|
|
7792
9321
|
const cached = CACHED_TOOLS.has(tool.name);
|
|
7793
9322
|
if (cached) {
|
|
7794
9323
|
const hit = this.cache.get(turn.workItemId, tool.name, input);
|
|
7795
|
-
if (hit)
|
|
9324
|
+
if (hit) {
|
|
9325
|
+
span?.end({ isError: false, cached: true, text: hit.text });
|
|
7796
9326
|
return hit;
|
|
9327
|
+
}
|
|
7797
9328
|
}
|
|
7798
9329
|
if (tool.kind === "write")
|
|
7799
9330
|
this.cache.invalidate(turn.workItemId);
|
|
@@ -7808,15 +9339,17 @@ class McpBroker {
|
|
|
7808
9339
|
const result = await this.transport.tool(tool.route, {
|
|
7809
9340
|
...input,
|
|
7810
9341
|
agentSessionId: turn.agentSessionId
|
|
7811
|
-
});
|
|
9342
|
+
}, span?.traceparent);
|
|
7812
9343
|
answer = { text: result.text, data: result.data };
|
|
7813
9344
|
} catch (error) {
|
|
7814
9345
|
if (asking)
|
|
7815
9346
|
turn.askingUser(false);
|
|
7816
9347
|
answer = { text: toolErrorText(error), isError: true };
|
|
7817
9348
|
this.log(`${tool.name} failed: ${answer.text}`);
|
|
9349
|
+
span?.end({ isError: true, cached: false, text: answer.text });
|
|
7818
9350
|
return answer;
|
|
7819
9351
|
}
|
|
9352
|
+
span?.end({ isError: false, cached: false, text: answer.text });
|
|
7820
9353
|
if (cached)
|
|
7821
9354
|
this.cache.set(turn.workItemId, tool.name, input, answer);
|
|
7822
9355
|
if (tool.name === "linear_report_pr" && answer.data?.linked)
|
|
@@ -8133,6 +9666,8 @@ class Supervisor {
|
|
|
8133
9666
|
sweepTimer = null;
|
|
8134
9667
|
diskWasLow = false;
|
|
8135
9668
|
broker;
|
|
9669
|
+
telemetryQueue;
|
|
9670
|
+
secrets = [];
|
|
8136
9671
|
constructor(options) {
|
|
8137
9672
|
this.options = options;
|
|
8138
9673
|
this.config = options.config;
|
|
@@ -8154,6 +9689,8 @@ class Supervisor {
|
|
|
8154
9689
|
}
|
|
8155
9690
|
});
|
|
8156
9691
|
this.broker = new McpBroker(options.api, (line) => options.log.info(line));
|
|
9692
|
+
this.telemetryQueue = new TelemetryQueue(options.api, this.config.telemetry.buffer_mb * 1024 * 1024, (line) => options.log.info(line));
|
|
9693
|
+
this.secrets = this.baseSecrets();
|
|
8157
9694
|
this.sink = new EventSink(options.state, options.api, options.log, {
|
|
8158
9695
|
onCancelRequested: (id) => this.stop(id, "stop"),
|
|
8159
9696
|
onNotYours: (id) => this.silence(id),
|
|
@@ -8183,10 +9720,42 @@ class Supervisor {
|
|
|
8183
9720
|
}
|
|
8184
9721
|
this.link.start();
|
|
8185
9722
|
this.sink.kick(true);
|
|
9723
|
+
this.telemetryQueue.start();
|
|
8186
9724
|
this.sweepTimer = setInterval(() => void this.cleanup({}), this.options.sweepIntervalMs ?? SWEEP_INTERVAL_MS);
|
|
8187
9725
|
this.sweepTimer.unref?.();
|
|
8188
9726
|
return interrupted;
|
|
8189
9727
|
}
|
|
9728
|
+
baseSecrets() {
|
|
9729
|
+
return [
|
|
9730
|
+
...this.options.secrets ?? [],
|
|
9731
|
+
...resolvedConfigSecrets(this.config, this.options.env)
|
|
9732
|
+
];
|
|
9733
|
+
}
|
|
9734
|
+
turnTelemetry(assignment, agentId, slot) {
|
|
9735
|
+
const claim = assignment.telemetry;
|
|
9736
|
+
if (!claim || !this.config.telemetry.enabled)
|
|
9737
|
+
return null;
|
|
9738
|
+
const preset = this.config.agents[agentId]?.preset ?? (isPreset(agentId) ? agentId : null);
|
|
9739
|
+
const max = this.config.telemetry.max_content;
|
|
9740
|
+
const telemetry = new TurnTelemetry({
|
|
9741
|
+
claim: {
|
|
9742
|
+
...claim,
|
|
9743
|
+
content: minLevel(claim.content, max),
|
|
9744
|
+
agentNative: claim.agentNative && this.config.telemetry.agent_native
|
|
9745
|
+
},
|
|
9746
|
+
assignment,
|
|
9747
|
+
runner: { name: this.config.runner.name },
|
|
9748
|
+
agentId,
|
|
9749
|
+
preset,
|
|
9750
|
+
queue: this.telemetryQueue,
|
|
9751
|
+
secrets: () => slot.nonce ? [...this.secrets, slot.nonce] : this.secrets
|
|
9752
|
+
});
|
|
9753
|
+
telemetry.slots({
|
|
9754
|
+
capacity: this.config.runner.max_parallel_jobs,
|
|
9755
|
+
inUse: this.slots.size
|
|
9756
|
+
});
|
|
9757
|
+
return telemetry;
|
|
9758
|
+
}
|
|
8190
9759
|
worktreeInUse(wt) {
|
|
8191
9760
|
if (wt.state === "active")
|
|
8192
9761
|
return true;
|
|
@@ -8392,6 +9961,9 @@ class Supervisor {
|
|
|
8392
9961
|
};
|
|
8393
9962
|
this.slots.set(assignment.workItemId, slot);
|
|
8394
9963
|
const mcpCommand = this.options.mcpCommand === undefined ? selfCommand() : this.options.mcpCommand;
|
|
9964
|
+
const telemetry = this.turnTelemetry(assignment, agentId, slot);
|
|
9965
|
+
if (telemetry)
|
|
9966
|
+
this.sink.trace(assignment.workItemId, telemetry.traceparentFor());
|
|
8395
9967
|
if (mcpCommand) {
|
|
8396
9968
|
slot.nonce = this.broker.register({
|
|
8397
9969
|
agentSessionId: assignment.agentSessionId,
|
|
@@ -8405,7 +9977,25 @@ class Supervisor {
|
|
|
8405
9977
|
reportedPullRequest: (url) => {
|
|
8406
9978
|
this.options.state.addPullRequest(slot.agentSessionId, url);
|
|
8407
9979
|
slot.onReportedPullRequest?.(url);
|
|
8408
|
-
}
|
|
9980
|
+
},
|
|
9981
|
+
trace: telemetry ? (tool, input) => {
|
|
9982
|
+
const span = telemetry.span("mcp.call", {
|
|
9983
|
+
"gen_ai.operation.name": "execute_tool",
|
|
9984
|
+
"gen_ai.tool.name": tool,
|
|
9985
|
+
"hypotenuse.mcp.server": "hypotenuse"
|
|
9986
|
+
}, telemetry.promptSpan, "client").content("mcp.arguments", JSON.stringify(input));
|
|
9987
|
+
return {
|
|
9988
|
+
traceparent: span.traceparent,
|
|
9989
|
+
end: (result) => {
|
|
9990
|
+
if (result.isError)
|
|
9991
|
+
span.fail(result.text.slice(0, 500));
|
|
9992
|
+
span.content("mcp.result", result.text).end({
|
|
9993
|
+
"hypotenuse.mcp.cached": result.cached,
|
|
9994
|
+
"hypotenuse.mcp.result.bytes": Buffer.byteLength(result.text)
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
};
|
|
9998
|
+
} : undefined
|
|
8409
9999
|
});
|
|
8410
10000
|
}
|
|
8411
10001
|
log.info(`claimed ${assignment.kind} for ${assignment.issue.identifier ?? "an issue"}`, {
|
|
@@ -8473,7 +10063,8 @@ class Supervisor {
|
|
|
8473
10063
|
},
|
|
8474
10064
|
onReportedPullRequest: (handler) => {
|
|
8475
10065
|
slot.onReportedPullRequest = handler;
|
|
8476
|
-
}
|
|
10066
|
+
},
|
|
10067
|
+
telemetry
|
|
8477
10068
|
};
|
|
8478
10069
|
slot.done = this.runTurn(slot, ctx);
|
|
8479
10070
|
}
|
|
@@ -8527,8 +10118,18 @@ class Supervisor {
|
|
|
8527
10118
|
});
|
|
8528
10119
|
this.link.beatNow();
|
|
8529
10120
|
const worktreeId = this.options.state.session(slot.agentSessionId)?.worktreeId;
|
|
8530
|
-
|
|
8531
|
-
|
|
10121
|
+
const telemetry = ctx.telemetry ?? null;
|
|
10122
|
+
if (worktreeId) {
|
|
10123
|
+
const span = telemetry?.span("worktree.cleanup", {
|
|
10124
|
+
"hypotenuse.worktree.id": worktreeId
|
|
10125
|
+
});
|
|
10126
|
+
const report = await this.cleanup({ only: [worktreeId] });
|
|
10127
|
+
span?.end({
|
|
10128
|
+
"hypotenuse.worktree.removed": (report?.removed.length ?? 0) > 0,
|
|
10129
|
+
"hypotenuse.worktree.kept": (report?.kept.length ?? 0) > 0
|
|
10130
|
+
});
|
|
10131
|
+
}
|
|
10132
|
+
telemetry?.finish(slot.silent ? "taken_away" : result.outcome, result.error);
|
|
8532
10133
|
}
|
|
8533
10134
|
stop(workItemId, reason) {
|
|
8534
10135
|
const slot = this.slots.get(workItemId);
|
|
@@ -8587,6 +10188,7 @@ class Supervisor {
|
|
|
8587
10188
|
await this.waitIdle(this.config.sessions.stop_grace + 5000);
|
|
8588
10189
|
}
|
|
8589
10190
|
const flushed = await this.sink.drain(5000);
|
|
10191
|
+
await this.telemetryQueue.drain(5000);
|
|
8590
10192
|
this.link.beatNow();
|
|
8591
10193
|
return { drained: this.slots.size === 0 && flushed, cancelled };
|
|
8592
10194
|
}
|
|
@@ -8648,6 +10250,8 @@ class Supervisor {
|
|
|
8648
10250
|
}
|
|
8649
10251
|
this.config = merged;
|
|
8650
10252
|
this.options.log.level = merged.log.level;
|
|
10253
|
+
this.telemetryQueue.capacityBytes = merged.telemetry.buffer_mb * 1024 * 1024;
|
|
10254
|
+
this.secrets = this.baseSecrets();
|
|
8651
10255
|
this.repositories = daemonRepositories(merged, this.options.machine);
|
|
8652
10256
|
this.capabilities = this.buildCapabilities();
|
|
8653
10257
|
this.options.log.info("config reloaded", {
|
|
@@ -8742,6 +10346,7 @@ class Supervisor {
|
|
|
8742
10346
|
this.sweepTimer = null;
|
|
8743
10347
|
this.link.stop();
|
|
8744
10348
|
this.sink.close();
|
|
10349
|
+
this.telemetryQueue.stop();
|
|
8745
10350
|
}
|
|
8746
10351
|
}
|
|
8747
10352
|
function changedKeys(a, b, prefix = "") {
|
|
@@ -8756,7 +10361,7 @@ function changedKeys(a, b, prefix = "") {
|
|
|
8756
10361
|
return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
|
|
8757
10362
|
}
|
|
8758
10363
|
function resolveCommand(command, env) {
|
|
8759
|
-
if (
|
|
10364
|
+
if (isAbsolute7(command))
|
|
8760
10365
|
return command;
|
|
8761
10366
|
return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
|
|
8762
10367
|
}
|
|
@@ -8854,6 +10459,7 @@ async function runDaemon(options) {
|
|
|
8854
10459
|
let state;
|
|
8855
10460
|
let origin;
|
|
8856
10461
|
let runner;
|
|
10462
|
+
let credentials;
|
|
8857
10463
|
try {
|
|
8858
10464
|
const read = readDaemonConfig(options, env);
|
|
8859
10465
|
if (!read.config)
|
|
@@ -8862,7 +10468,6 @@ async function runDaemon(options) {
|
|
|
8862
10468
|
paths = daemonPaths(read.path, options.stateDir, config.log.file, env);
|
|
8863
10469
|
log = new DaemonLog(paths.log, config.log.level, !options.detachedChild);
|
|
8864
10470
|
const credentialsFile = credentialsPath(env);
|
|
8865
|
-
let credentials;
|
|
8866
10471
|
try {
|
|
8867
10472
|
credentials = loadCredentials(credentialsFile);
|
|
8868
10473
|
} catch (error) {
|
|
@@ -8938,7 +10543,11 @@ async function runDaemon(options) {
|
|
|
8938
10543
|
stateDir: paths.stateDir,
|
|
8939
10544
|
env,
|
|
8940
10545
|
readConfig: () => readDaemonConfig(options, env),
|
|
8941
|
-
requestExit
|
|
10546
|
+
requestExit,
|
|
10547
|
+
secrets: [
|
|
10548
|
+
runner.token,
|
|
10549
|
+
...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
|
|
10550
|
+
]
|
|
8942
10551
|
});
|
|
8943
10552
|
control = new ControlServer(paths.socket, {
|
|
8944
10553
|
status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
|
|
@@ -10337,6 +11946,20 @@ async function runDoctor(ctx, options) {
|
|
|
10337
11946
|
if (options.agent && !resolveAgents(config).some((a) => a.id === options.agent)) {
|
|
10338
11947
|
list.fail("agents", `no agent "${options.agent}" in the config`);
|
|
10339
11948
|
}
|
|
11949
|
+
const t = config.telemetry;
|
|
11950
|
+
if (!t.enabled)
|
|
11951
|
+
list.ok("telemetry", "off: this runner sends no telemetry (`[telemetry] enabled = false`)");
|
|
11952
|
+
else {
|
|
11953
|
+
list.ok("telemetry", `sends up to \`${t.max_content}\` for the project's exporters; buffer ${t.buffer_mb} MB`);
|
|
11954
|
+
const agents = resolveAgents(config).filter((a) => a.roles.length > 0);
|
|
11955
|
+
if (!t.agent_native)
|
|
11956
|
+
list.ok("telemetry", "agents' own telemetry is not captured");
|
|
11957
|
+
else
|
|
11958
|
+
for (const agent of agents) {
|
|
11959
|
+
const support = nativeTelemetrySupport(agent.preset);
|
|
11960
|
+
list.ok("telemetry", support === "native" ? `${agent.id}: contributes its own telemetry (model requests, tokens, cost)` : support === "standard-otel" ? `${agent.id}: contributes its own telemetry if it reads the standard OTEL_* variables` : `${agent.id}: a custom command \u2014 it gets OTEL_EXPORTER_OTLP_ENDPOINT and HYPOTENUSE_OTLP_ENDPOINT`);
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
10340
11963
|
for (const repo of daemonRepositories(config, machine)) {
|
|
10341
11964
|
if (options.offline) {
|
|
10342
11965
|
list.ok("repositories", `${repo.name}: ${repo.remote} (${repo.baseBranch})`);
|
|
@@ -12003,6 +13626,161 @@ function registerTagCommands(program) {
|
|
|
12003
13626
|
}
|
|
12004
13627
|
}
|
|
12005
13628
|
|
|
13629
|
+
// src/commands/telemetry.ts
|
|
13630
|
+
function collect2(value, previous = []) {
|
|
13631
|
+
return [...previous, value];
|
|
13632
|
+
}
|
|
13633
|
+
function parsePairs(pairs, what) {
|
|
13634
|
+
const out = {};
|
|
13635
|
+
for (const pair of pairs) {
|
|
13636
|
+
const at = pair.indexOf("=");
|
|
13637
|
+
if (at <= 0)
|
|
13638
|
+
throw new Error(`${what} must be KEY=VALUE, got "${pair}"`);
|
|
13639
|
+
out[pair.slice(0, at).trim()] = pair.slice(at + 1);
|
|
13640
|
+
}
|
|
13641
|
+
return out;
|
|
13642
|
+
}
|
|
13643
|
+
function exporterHealth(e) {
|
|
13644
|
+
if (!e.enabled)
|
|
13645
|
+
return "paused";
|
|
13646
|
+
if (e.lastErrorAt && (!e.lastExportAt || e.lastErrorAt > e.lastExportAt))
|
|
13647
|
+
return `error ${when(e.lastErrorAt)}: ${e.lastError ?? ""}`;
|
|
13648
|
+
return e.lastExportAt ? `ok ${when(e.lastExportAt)}` : "nothing yet";
|
|
13649
|
+
}
|
|
13650
|
+
function describeExporter(e) {
|
|
13651
|
+
const attrs = Object.entries(e.resourceAttributes).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
13652
|
+
return [
|
|
13653
|
+
`${e.name} (id ${e.id})${e.enabled ? "" : " paused"}`,
|
|
13654
|
+
`endpoint ${e.endpoint}`,
|
|
13655
|
+
`protocol ${e.protocol}`,
|
|
13656
|
+
`content ${e.content}`,
|
|
13657
|
+
`signals ${e.signals.join(", ")}`,
|
|
13658
|
+
`sampled ${Math.round(e.traceSampleRatio * 100)}% of turns`,
|
|
13659
|
+
`agent's own ${e.agentNative ? "captured" : "not captured"}`,
|
|
13660
|
+
`headers ${e.headerNames.length ? e.headerNames.join(", ") : "none"} (values are never shown)`,
|
|
13661
|
+
`attributes ${attrs || "none"}`,
|
|
13662
|
+
`last export ${when(e.lastExportAt)}`,
|
|
13663
|
+
`last error ${e.lastError ? `${when(e.lastErrorAt)}: ${e.lastError}` : "-"}`
|
|
13664
|
+
].join(`
|
|
13665
|
+
`);
|
|
13666
|
+
}
|
|
13667
|
+
function exporterBody(opts) {
|
|
13668
|
+
const body = {};
|
|
13669
|
+
if (opts.name !== undefined)
|
|
13670
|
+
body.name = opts.name;
|
|
13671
|
+
if (opts.endpoint !== undefined)
|
|
13672
|
+
body.endpoint = opts.endpoint;
|
|
13673
|
+
if (opts.protocol !== undefined)
|
|
13674
|
+
body.protocol = opts.protocol;
|
|
13675
|
+
if (opts.clearHeaders)
|
|
13676
|
+
body.headers = {};
|
|
13677
|
+
else if (opts.header?.length)
|
|
13678
|
+
body.headers = parsePairs(opts.header, "--header");
|
|
13679
|
+
if (opts.signals !== undefined)
|
|
13680
|
+
body.signals = opts.signals.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13681
|
+
if (opts.content !== undefined)
|
|
13682
|
+
body.content = opts.content;
|
|
13683
|
+
if (opts.agentNative !== undefined)
|
|
13684
|
+
body.agentNative = opts.agentNative;
|
|
13685
|
+
if (opts.sample !== undefined)
|
|
13686
|
+
body.traceSampleRatio = Number(opts.sample);
|
|
13687
|
+
if (opts.attribute?.length)
|
|
13688
|
+
body.resourceAttributes = parsePairs(opts.attribute, "--attribute");
|
|
13689
|
+
if (opts.enabled !== undefined)
|
|
13690
|
+
body.enabled = opts.enabled;
|
|
13691
|
+
return body;
|
|
13692
|
+
}
|
|
13693
|
+
function exporterOptions(command, adding) {
|
|
13694
|
+
if (adding)
|
|
13695
|
+
command.requiredOption("--name <name>", "what it is called, unique in the project").requiredOption("--endpoint <url>", "the collector's OTLP/HTTP base URL (https; /v1/<signal> is appended)");
|
|
13696
|
+
else
|
|
13697
|
+
command.option("--name <name>", "a new name").option("--endpoint <url>", "a new endpoint");
|
|
13698
|
+
return command.option("--protocol <protocol>", "http/protobuf (default) or http/json").option("--header <KEY=VALUE>", "a header sent with every export, e.g. an API key; repeat for more (replaces all; never printed)", collect2).option("--content <level>", "metadata (default), content, or full \u2014 full sends prompts and source code").option("--signals <list>", "comma-separated: traces,metrics,logs").option("--sample <ratio>", "0\u20131: the share of turns whose traces are kept").option("--attribute <KEY=VALUE>", "an extra resource attribute; repeat for more (replaces all)", collect2).option("--agent-native", "capture the coding agent's own telemetry").option("--no-agent-native", "do not capture the agent's own telemetry");
|
|
13699
|
+
}
|
|
13700
|
+
function registerTelemetryCommands(program) {
|
|
13701
|
+
const telemetry = program.command("telemetry").description("Where the selected project's OpenTelemetry goes");
|
|
13702
|
+
const exporter = telemetry.command("exporter").description("The project's OpenTelemetry collectors");
|
|
13703
|
+
withPaging(exporter.command("list").description("List exporters")).action(async function() {
|
|
13704
|
+
await runCommand(this, async (ctx) => {
|
|
13705
|
+
const projectId = await ctx.projectId();
|
|
13706
|
+
const result = await ctx.signedInClient().request("GET", "/telemetry-exporters", {
|
|
13707
|
+
query: { projectId, ...pageQuery(this) }
|
|
13708
|
+
});
|
|
13709
|
+
emit(ctx, result, () => table(result.telemetryExporters.map((e) => ({
|
|
13710
|
+
id: e.id,
|
|
13711
|
+
name: e.name,
|
|
13712
|
+
endpoint: e.endpoint,
|
|
13713
|
+
content: e.content,
|
|
13714
|
+
signals: e.signals.join(","),
|
|
13715
|
+
sampled: `${Math.round(e.traceSampleRatio * 100)}%`,
|
|
13716
|
+
headers: e.headerNames.join(",") || "-",
|
|
13717
|
+
health: exporterHealth(e)
|
|
13718
|
+
})), [
|
|
13719
|
+
{ key: "id" },
|
|
13720
|
+
{ key: "name", maxWidth: 30 },
|
|
13721
|
+
{ key: "endpoint", maxWidth: 50 },
|
|
13722
|
+
{ key: "content" },
|
|
13723
|
+
{ key: "signals" },
|
|
13724
|
+
{ key: "sampled" },
|
|
13725
|
+
{ key: "headers", maxWidth: 30 },
|
|
13726
|
+
{ key: "health", maxWidth: 60 }
|
|
13727
|
+
]) + pageFooter(result.pagination));
|
|
13728
|
+
});
|
|
13729
|
+
});
|
|
13730
|
+
exporter.command("view <id>").description("Show one exporter (header names only)").action(async function(id) {
|
|
13731
|
+
await runCommand(this, async (ctx) => {
|
|
13732
|
+
const projectId = await ctx.projectId();
|
|
13733
|
+
const result = await ctx.signedInClient().request("GET", `/telemetry-exporter/${id}`, { query: { projectId } });
|
|
13734
|
+
emit(ctx, result, () => describeExporter(result.telemetryExporter));
|
|
13735
|
+
});
|
|
13736
|
+
});
|
|
13737
|
+
exporterOptions(exporter.command("add").description("Export the project's telemetry to a collector (admin)"), true).option("--disabled", "create it paused").action(async function() {
|
|
13738
|
+
await runCommand(this, async (ctx) => {
|
|
13739
|
+
const opts = this.opts();
|
|
13740
|
+
const projectId = await ctx.projectId();
|
|
13741
|
+
const result = await ctx.signedInClient().request("PUT", "/telemetry-exporter", {
|
|
13742
|
+
body: {
|
|
13743
|
+
projectId,
|
|
13744
|
+
...exporterBody(opts),
|
|
13745
|
+
...opts.disabled ? { enabled: false } : {}
|
|
13746
|
+
}
|
|
13747
|
+
});
|
|
13748
|
+
emit(ctx, result, () => `Added exporter ${result.telemetryExporter.name} (id ${result.telemetryExporter.id}) at ${result.telemetryExporter.content}; \`hypotenuse telemetry exporter test ${result.telemetryExporter.id}\` sends a probe`);
|
|
13749
|
+
});
|
|
13750
|
+
});
|
|
13751
|
+
exporterOptions(exporter.command("edit <id>").description("Change an exporter; headers are kept unless given (admin)"), false).option("--clear-headers", "send no headers from now on").option("--enable", "resume exporting").option("--disable", "pause exporting").action(async function(id) {
|
|
13752
|
+
await runCommand(this, async (ctx) => {
|
|
13753
|
+
const opts = this.opts();
|
|
13754
|
+
const projectId = await ctx.projectId();
|
|
13755
|
+
const body = exporterBody({
|
|
13756
|
+
...opts,
|
|
13757
|
+
enabled: opts.enable ? true : opts.disable ? false : undefined
|
|
13758
|
+
});
|
|
13759
|
+
const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}`, { body: { projectId, ...body } });
|
|
13760
|
+
emit(ctx, result, () => describeExporter(result.telemetryExporter));
|
|
13761
|
+
});
|
|
13762
|
+
});
|
|
13763
|
+
exporter.command("remove <id>").description("Stop exporting to a collector and forget its headers (admin)").action(async function(id) {
|
|
13764
|
+
await runCommand(this, async (ctx) => {
|
|
13765
|
+
const projectId = await ctx.projectId();
|
|
13766
|
+
const result = await ctx.signedInClient().request("DELETE", `/telemetry-exporter/${id}`, {
|
|
13767
|
+
body: { projectId }
|
|
13768
|
+
});
|
|
13769
|
+
emit(ctx, result, () => `Removed exporter ${id}`);
|
|
13770
|
+
});
|
|
13771
|
+
});
|
|
13772
|
+
exporter.command("test <id>").description("Send the collector a probe span, log, and metric, and show its answers (admin)").action(async function(id) {
|
|
13773
|
+
await runCommand(this, async (ctx) => {
|
|
13774
|
+
const projectId = await ctx.projectId();
|
|
13775
|
+
const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}/test`, { body: { projectId } });
|
|
13776
|
+
emit(ctx, result, () => result.results.map((r) => `${glyph(r.ok ? "complete" : "error")} ${r.signal.padEnd(8)} ${r.status || "-"}${r.error ? ` ${r.error}` : ""}`).join(`
|
|
13777
|
+
`));
|
|
13778
|
+
if (!result.ok)
|
|
13779
|
+
process.exitCode = 1;
|
|
13780
|
+
});
|
|
13781
|
+
});
|
|
13782
|
+
}
|
|
13783
|
+
|
|
12006
13784
|
// src/commands/upgrade.ts
|
|
12007
13785
|
var UPGRADE_DRAIN_MS = 10 * 60000;
|
|
12008
13786
|
async function restartRunningDaemon(socketPath, call = callDaemon) {
|
|
@@ -12272,6 +14050,7 @@ function buildProgram() {
|
|
|
12272
14050
|
registerEventCommands(program);
|
|
12273
14051
|
registerRunnerCommands(program);
|
|
12274
14052
|
registerRoutingCommands(program);
|
|
14053
|
+
registerTelemetryCommands(program);
|
|
12275
14054
|
registerIdentityCommands(program);
|
|
12276
14055
|
registerConfigCommands(program);
|
|
12277
14056
|
registerDaemonCommands(program);
|