@arcadeai/hypotenuse 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hypotenuse.js +2165 -397
- package/package.json +2 -1
- package/skills/hypotenuse.md +2 -1
package/dist/hypotenuse.js
CHANGED
|
@@ -343,7 +343,7 @@ import { Command } from "commander";
|
|
|
343
343
|
// package.json
|
|
344
344
|
var package_default = {
|
|
345
345
|
name: "@arcadeai/hypotenuse",
|
|
346
|
-
version: "0.
|
|
346
|
+
version: "0.14.0",
|
|
347
347
|
description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
|
|
348
348
|
type: "module",
|
|
349
349
|
license: "Apache-2.0",
|
|
@@ -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: {} };
|
|
@@ -3602,6 +4036,389 @@ import { join as join10 } from "path";
|
|
|
3602
4036
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
|
|
3603
4037
|
import { join as join8 } from "path";
|
|
3604
4038
|
|
|
4039
|
+
// src/telemetry/acp.ts
|
|
4040
|
+
import { isAbsolute as isAbsolute4, relative as relative3 } from "path";
|
|
4041
|
+
class AcpTelemetry {
|
|
4042
|
+
turn;
|
|
4043
|
+
cwd;
|
|
4044
|
+
tools = new Map;
|
|
4045
|
+
spawn = null;
|
|
4046
|
+
prompt = null;
|
|
4047
|
+
chunk = null;
|
|
4048
|
+
promptSpanId = newSpanId();
|
|
4049
|
+
constructor(turn, cwd) {
|
|
4050
|
+
this.turn = turn;
|
|
4051
|
+
this.cwd = cwd;
|
|
4052
|
+
}
|
|
4053
|
+
get traceparent() {
|
|
4054
|
+
return this.turn?.traceparentFor(this.promptSpanId) ?? null;
|
|
4055
|
+
}
|
|
4056
|
+
rel(path) {
|
|
4057
|
+
if (!isAbsolute4(path))
|
|
4058
|
+
return path;
|
|
4059
|
+
const r = relative3(this.cwd, path);
|
|
4060
|
+
return r.startsWith("..") ? path : r || ".";
|
|
4061
|
+
}
|
|
4062
|
+
begin(name, attrs = {}) {
|
|
4063
|
+
return this.turn?.span(name, attrs, this.prompt) ?? null;
|
|
4064
|
+
}
|
|
4065
|
+
async around(name, attrs, fn) {
|
|
4066
|
+
const span = this.begin(name, attrs);
|
|
4067
|
+
try {
|
|
4068
|
+
return await fn(span);
|
|
4069
|
+
} catch (error) {
|
|
4070
|
+
span?.fail(error instanceof Error ? error.message : String(error));
|
|
4071
|
+
throw error;
|
|
4072
|
+
} finally {
|
|
4073
|
+
span?.end();
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
spawned(attrs, command) {
|
|
4077
|
+
this.spawn = this.begin("agent.spawn", attrs);
|
|
4078
|
+
this.spawn?.content("agent.command", command);
|
|
4079
|
+
}
|
|
4080
|
+
initialized(attrs) {
|
|
4081
|
+
this.spawn?.set(attrs);
|
|
4082
|
+
}
|
|
4083
|
+
exited(exitCode) {
|
|
4084
|
+
this.spawn?.end({ "process.exit.code": exitCode ?? null });
|
|
4085
|
+
}
|
|
4086
|
+
promptStarted(attrs, text) {
|
|
4087
|
+
if (!this.turn)
|
|
4088
|
+
return;
|
|
4089
|
+
this.prompt = this.turn.span("acp.prompt", { "gen_ai.operation.name": "invoke_agent", ...attrs }, null, "client", this.promptSpanId);
|
|
4090
|
+
this.prompt.content("prompt", text);
|
|
4091
|
+
this.turn.promptSpan = this.prompt;
|
|
4092
|
+
}
|
|
4093
|
+
promptEnded(attrs, error) {
|
|
4094
|
+
this.flushChunk();
|
|
4095
|
+
for (const [, span] of this.tools)
|
|
4096
|
+
span.end({ "hypotenuse.tool.finished": false });
|
|
4097
|
+
this.tools.clear();
|
|
4098
|
+
if (error)
|
|
4099
|
+
this.prompt?.fail(error);
|
|
4100
|
+
this.prompt?.end(attrs);
|
|
4101
|
+
if (this.turn)
|
|
4102
|
+
this.turn.promptSpan = null;
|
|
4103
|
+
const input = attrs["gen_ai.usage.input_tokens"];
|
|
4104
|
+
const output = attrs["gen_ai.usage.output_tokens"];
|
|
4105
|
+
if (typeof input === "number" || typeof output === "number")
|
|
4106
|
+
this.turn?.usage({
|
|
4107
|
+
inputTokens: typeof input === "number" ? input : undefined,
|
|
4108
|
+
outputTokens: typeof output === "number" ? output : undefined
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
flushChunk() {
|
|
4112
|
+
const chunk = this.chunk;
|
|
4113
|
+
this.chunk = null;
|
|
4114
|
+
if (!chunk || !this.prompt || !this.turn)
|
|
4115
|
+
return;
|
|
4116
|
+
this.prompt.event(`agent.${chunk.kind}`, this.turn.contentAttrs("text", chunk.text));
|
|
4117
|
+
}
|
|
4118
|
+
update(update) {
|
|
4119
|
+
if (!this.turn || !this.prompt)
|
|
4120
|
+
return;
|
|
4121
|
+
const u = update;
|
|
4122
|
+
const kind = String(u.sessionUpdate);
|
|
4123
|
+
if (kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
|
|
4124
|
+
const content = u.content;
|
|
4125
|
+
if (content?.type !== "text")
|
|
4126
|
+
return;
|
|
4127
|
+
const which = kind === "agent_thought_chunk" ? "thought" : "message";
|
|
4128
|
+
if (this.chunk && this.chunk.kind !== which)
|
|
4129
|
+
this.flushChunk();
|
|
4130
|
+
this.chunk ??= { kind: which, text: "" };
|
|
4131
|
+
this.chunk.text += content.text ?? "";
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
this.flushChunk();
|
|
4135
|
+
switch (kind) {
|
|
4136
|
+
case "tool_call": {
|
|
4137
|
+
const id = String(u.toolCallId);
|
|
4138
|
+
const span = this.turn.span("tool_call", {
|
|
4139
|
+
"gen_ai.operation.name": "execute_tool",
|
|
4140
|
+
"gen_ai.tool.call.id": id,
|
|
4141
|
+
"gen_ai.tool.type": typeof u.kind === "string" ? u.kind : null,
|
|
4142
|
+
"hypotenuse.tool.status": typeof u.status === "string" ? u.status : "pending"
|
|
4143
|
+
}, this.prompt);
|
|
4144
|
+
span.content("tool.title", typeof u.title === "string" ? u.title : null);
|
|
4145
|
+
this.toolDetail(span, u);
|
|
4146
|
+
this.tools.set(id, span);
|
|
4147
|
+
if (u.status === "completed" || u.status === "failed")
|
|
4148
|
+
this.endTool(id, u);
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
case "tool_call_update": {
|
|
4152
|
+
const id = String(u.toolCallId);
|
|
4153
|
+
const span = this.tools.get(id);
|
|
4154
|
+
if (!span)
|
|
4155
|
+
return;
|
|
4156
|
+
if (typeof u.kind === "string")
|
|
4157
|
+
span.set({ "gen_ai.tool.type": u.kind });
|
|
4158
|
+
if (typeof u.title === "string")
|
|
4159
|
+
span.content("tool.title", u.title);
|
|
4160
|
+
this.toolDetail(span, u);
|
|
4161
|
+
if (u.status === "completed" || u.status === "failed")
|
|
4162
|
+
this.endTool(id, u);
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
case "plan": {
|
|
4166
|
+
const entries = u.entries ?? [];
|
|
4167
|
+
this.prompt.event("plan", {
|
|
4168
|
+
"hypotenuse.plan.entries": entries.length,
|
|
4169
|
+
"hypotenuse.plan.completed": entries.filter((e) => e.status === "completed").length,
|
|
4170
|
+
...this.turn.contentAttrs("plan", JSON.stringify(entries))
|
|
4171
|
+
});
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
case "usage_update": {
|
|
4175
|
+
const cost = u.cost;
|
|
4176
|
+
this.prompt.event("usage", {
|
|
4177
|
+
"hypotenuse.context.used": typeof u.used === "number" ? u.used : null,
|
|
4178
|
+
"hypotenuse.context.size": typeof u.size === "number" ? u.size : null,
|
|
4179
|
+
"hypotenuse.usage.cost": cost?.amount ?? null,
|
|
4180
|
+
"hypotenuse.usage.currency": cost?.currency ?? null
|
|
4181
|
+
});
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
case "current_mode_update":
|
|
4185
|
+
this.prompt.event("mode", {
|
|
4186
|
+
"hypotenuse.mode": typeof u.currentModeId === "string" ? u.currentModeId : null
|
|
4187
|
+
});
|
|
4188
|
+
return;
|
|
4189
|
+
default:
|
|
4190
|
+
this.prompt.event("session_update", { "hypotenuse.update.kind": kind });
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
toolDetail(span, u) {
|
|
4194
|
+
const locations = Array.isArray(u.locations) ? u.locations : null;
|
|
4195
|
+
if (locations?.length)
|
|
4196
|
+
span.set({
|
|
4197
|
+
"hypotenuse.tool.locations": locations.map((l) => l.path ? this.rel(l.path) : "").filter(Boolean).join(",")
|
|
4198
|
+
});
|
|
4199
|
+
if (u.rawInput !== undefined)
|
|
4200
|
+
span.content("tool.input", JSON.stringify(u.rawInput));
|
|
4201
|
+
if (u.rawOutput !== undefined)
|
|
4202
|
+
span.content("tool.output", JSON.stringify(u.rawOutput));
|
|
4203
|
+
else if (Array.isArray(u.content) && u.content.length)
|
|
4204
|
+
span.content("tool.output", JSON.stringify(u.content));
|
|
4205
|
+
}
|
|
4206
|
+
endTool(id, u) {
|
|
4207
|
+
const span = this.tools.get(id);
|
|
4208
|
+
this.tools.delete(id);
|
|
4209
|
+
if (!span)
|
|
4210
|
+
return;
|
|
4211
|
+
if (u.status === "failed")
|
|
4212
|
+
span.fail("the tool call failed");
|
|
4213
|
+
span.end({ "hypotenuse.tool.status": String(u.status) });
|
|
4214
|
+
}
|
|
4215
|
+
permission(toolCall, policy, options) {
|
|
4216
|
+
const parent = this.tools.get(toolCall.toolCallId) ?? this.prompt;
|
|
4217
|
+
const span = this.turn?.span("permission.wait", {
|
|
4218
|
+
"gen_ai.tool.call.id": toolCall.toolCallId,
|
|
4219
|
+
"gen_ai.tool.type": toolCall.kind ?? null,
|
|
4220
|
+
"hypotenuse.permission.policy": policy,
|
|
4221
|
+
"hypotenuse.permission.options": options.map((o) => o.kind ?? o.optionId).join(",")
|
|
4222
|
+
}, parent) ?? null;
|
|
4223
|
+
span?.content("tool.title", toolCall.title ?? null);
|
|
4224
|
+
return (answer) => span?.end({
|
|
4225
|
+
"hypotenuse.permission.outcome": answer.outcome,
|
|
4226
|
+
"hypotenuse.permission.option": answer.optionId ?? null,
|
|
4227
|
+
"hypotenuse.permission.decided_by": answer.decidedBy
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
fs(op, path) {
|
|
4231
|
+
const span = this.begin(`fs.${op}`, { "file.path": this.rel(path) });
|
|
4232
|
+
return (result) => {
|
|
4233
|
+
if (result.refused)
|
|
4234
|
+
span?.fail(result.refused);
|
|
4235
|
+
span?.end({
|
|
4236
|
+
"hypotenuse.fs.bytes": result.bytes ?? null,
|
|
4237
|
+
"hypotenuse.fs.refused": result.refused ?? null
|
|
4238
|
+
});
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
frame(line, message) {
|
|
4242
|
+
this.turn?.frame(line, message, this.prompt);
|
|
4243
|
+
}
|
|
4244
|
+
stderr(line) {
|
|
4245
|
+
this.turn?.log({
|
|
4246
|
+
body: line,
|
|
4247
|
+
level: "content",
|
|
4248
|
+
eventName: "agent.stderr",
|
|
4249
|
+
severity: "warn",
|
|
4250
|
+
span: this.spawn
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// src/telemetry/native.ts
|
|
4256
|
+
var NATIVE_TELEMETRY_PRESETS = {
|
|
4257
|
+
claude: "native",
|
|
4258
|
+
codex: "native",
|
|
4259
|
+
gemini: "native",
|
|
4260
|
+
qwen: "native",
|
|
4261
|
+
goose: "standard-otel",
|
|
4262
|
+
opencode: "standard-otel"
|
|
4263
|
+
};
|
|
4264
|
+
function nativeTelemetrySupport(preset) {
|
|
4265
|
+
return preset && NATIVE_TELEMETRY_PRESETS[preset] || "unknown";
|
|
4266
|
+
}
|
|
4267
|
+
function dropInheritedOtel(env) {
|
|
4268
|
+
for (const key of Object.keys(env))
|
|
4269
|
+
if (key.startsWith("OTEL_"))
|
|
4270
|
+
delete env[key];
|
|
4271
|
+
}
|
|
4272
|
+
function nativeTelemetryLaunch(preset, target) {
|
|
4273
|
+
const full = target.level === "full";
|
|
4274
|
+
const env = {
|
|
4275
|
+
HYPOTENUSE_OTLP_ENDPOINT: target.endpoint,
|
|
4276
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: target.endpoint,
|
|
4277
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
4278
|
+
OTEL_TRACES_EXPORTER: "otlp",
|
|
4279
|
+
OTEL_METRICS_EXPORTER: "otlp",
|
|
4280
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
4281
|
+
OTEL_METRIC_EXPORT_INTERVAL: "5000",
|
|
4282
|
+
OTEL_LOGS_EXPORT_INTERVAL: "2000",
|
|
4283
|
+
OTEL_BSP_SCHEDULE_DELAY: "1000"
|
|
4284
|
+
};
|
|
4285
|
+
const args = [];
|
|
4286
|
+
switch (preset) {
|
|
4287
|
+
case "claude":
|
|
4288
|
+
env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
|
4289
|
+
env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA = "1";
|
|
4290
|
+
if (full) {
|
|
4291
|
+
env.OTEL_LOG_USER_PROMPTS = "1";
|
|
4292
|
+
env.OTEL_LOG_TOOL_DETAILS = "1";
|
|
4293
|
+
}
|
|
4294
|
+
break;
|
|
4295
|
+
case "gemini":
|
|
4296
|
+
case "qwen": {
|
|
4297
|
+
const prefix = preset === "gemini" ? "GEMINI" : "QWEN";
|
|
4298
|
+
env[`${prefix}_TELEMETRY_ENABLED`] = "true";
|
|
4299
|
+
env[`${prefix}_TELEMETRY_TARGET`] = "local";
|
|
4300
|
+
env[`${prefix}_TELEMETRY_OTLP_ENDPOINT`] = target.endpoint;
|
|
4301
|
+
env[`${prefix}_TELEMETRY_OTLP_PROTOCOL`] = "http";
|
|
4302
|
+
env[`${prefix}_TELEMETRY_LOG_PROMPTS`] = full ? "true" : "false";
|
|
4303
|
+
break;
|
|
4304
|
+
}
|
|
4305
|
+
case "codex":
|
|
4306
|
+
args.push("-c", `otel.exporter={otlp-http={endpoint="${target.endpoint}",protocol="json"}}`, "-c", `otel.log_user_prompt=${full ? "true" : "false"}`);
|
|
4307
|
+
break;
|
|
4308
|
+
}
|
|
4309
|
+
return { env, args };
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
// src/telemetry/receiver.ts
|
|
4313
|
+
var MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
4314
|
+
var DRAIN_MS = 1500;
|
|
4315
|
+
|
|
4316
|
+
class NativeReceiver {
|
|
4317
|
+
turn;
|
|
4318
|
+
nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(18))).toString("base64url");
|
|
4319
|
+
server = null;
|
|
4320
|
+
received = {
|
|
4321
|
+
traces: 0,
|
|
4322
|
+
metrics: 0,
|
|
4323
|
+
logs: 0
|
|
4324
|
+
};
|
|
4325
|
+
lastReceivedAt = 0;
|
|
4326
|
+
constructor(turn) {
|
|
4327
|
+
this.turn = turn;
|
|
4328
|
+
}
|
|
4329
|
+
start() {
|
|
4330
|
+
this.server = Bun.serve({
|
|
4331
|
+
hostname: "127.0.0.1",
|
|
4332
|
+
port: 0,
|
|
4333
|
+
maxRequestBodySize: MAX_BODY_BYTES,
|
|
4334
|
+
fetch: (req) => this.handle(req)
|
|
4335
|
+
});
|
|
4336
|
+
return this.endpoint;
|
|
4337
|
+
}
|
|
4338
|
+
get endpoint() {
|
|
4339
|
+
return `http://127.0.0.1:${this.server?.port ?? 0}/${this.nonce}`;
|
|
4340
|
+
}
|
|
4341
|
+
async handle(req) {
|
|
4342
|
+
const url = new URL(req.url);
|
|
4343
|
+
const prefix = `/${this.nonce}/`;
|
|
4344
|
+
const signal = signalOfPath(url.pathname);
|
|
4345
|
+
if (req.method !== "POST" || !url.pathname.startsWith(prefix) || !signal)
|
|
4346
|
+
return new Response(null, { status: 404 });
|
|
4347
|
+
const contentType = req.headers.get("content-type");
|
|
4348
|
+
try {
|
|
4349
|
+
let bytes = new Uint8Array(await req.arrayBuffer());
|
|
4350
|
+
if ((req.headers.get("content-encoding") ?? "").includes("gzip"))
|
|
4351
|
+
bytes = new Uint8Array(Bun.gunzipSync(bytes));
|
|
4352
|
+
if (bytes.byteLength > MAX_BODY_BYTES)
|
|
4353
|
+
throw new Error("too large");
|
|
4354
|
+
this.accept(signal, decodeOtlpBody(signal, bytes, contentType));
|
|
4355
|
+
} catch {
|
|
4356
|
+
return new Response(null, { status: 400 });
|
|
4357
|
+
}
|
|
4358
|
+
const json = (contentType ?? "").includes("json");
|
|
4359
|
+
return new Response(json ? "{}" : new Uint8Array(0), {
|
|
4360
|
+
status: 200,
|
|
4361
|
+
headers: {
|
|
4362
|
+
"Content-Type": json ? "application/json" : "application/x-protobuf"
|
|
4363
|
+
}
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
accept(signal, request) {
|
|
4367
|
+
this.received[signal]++;
|
|
4368
|
+
this.lastReceivedAt = Date.now();
|
|
4369
|
+
const turn = this.turn;
|
|
4370
|
+
const level = {
|
|
4371
|
+
key: LEVEL_ATTRIBUTE,
|
|
4372
|
+
value: { stringValue: turn.level }
|
|
4373
|
+
};
|
|
4374
|
+
const tag = (attrs) => [
|
|
4375
|
+
...(attrs ?? []).filter((a) => a.key !== LEVEL_ATTRIBUTE),
|
|
4376
|
+
level
|
|
4377
|
+
];
|
|
4378
|
+
if (signal === "traces") {
|
|
4379
|
+
if (!turn.sampled)
|
|
4380
|
+
return;
|
|
4381
|
+
for (const rs of request.resourceSpans ?? [])
|
|
4382
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
4383
|
+
ss.spans = (ss.spans ?? []).map((s) => {
|
|
4384
|
+
const root = !s.parentSpanId || /^0+$/.test(s.parentSpanId);
|
|
4385
|
+
return {
|
|
4386
|
+
...s,
|
|
4387
|
+
traceId: turn.traceId,
|
|
4388
|
+
...root ? { parentSpanId: turn.turnSpan.spanId } : {},
|
|
4389
|
+
attributes: tag(s.attributes)
|
|
4390
|
+
};
|
|
4391
|
+
});
|
|
4392
|
+
}
|
|
4393
|
+
} else if (signal === "logs") {
|
|
4394
|
+
if (!turn.sampled)
|
|
4395
|
+
return;
|
|
4396
|
+
for (const rl of request.resourceLogs ?? [])
|
|
4397
|
+
for (const sl of rl.scopeLogs ?? [])
|
|
4398
|
+
sl.logRecords = (sl.logRecords ?? []).map((l) => ({
|
|
4399
|
+
...l,
|
|
4400
|
+
traceId: turn.traceId,
|
|
4401
|
+
...l.traceId && l.traceId !== turn.traceId ? { spanId: turn.turnSpan.spanId } : {},
|
|
4402
|
+
attributes: tag(l.attributes)
|
|
4403
|
+
}));
|
|
4404
|
+
}
|
|
4405
|
+
stampResource(signal, request, [
|
|
4406
|
+
...turn.resourceAttributes.filter((a) => a.key !== "hypotenuse.source" && !a.key.startsWith("service.")),
|
|
4407
|
+
...attributes({ "hypotenuse.source": "agent_native" })
|
|
4408
|
+
]);
|
|
4409
|
+
turn.enqueue(signal, request);
|
|
4410
|
+
}
|
|
4411
|
+
async close(drainMs = DRAIN_MS) {
|
|
4412
|
+
if (!this.server)
|
|
4413
|
+
return;
|
|
4414
|
+
const quietFor = Date.now() - this.lastReceivedAt;
|
|
4415
|
+
if (quietFor < drainMs)
|
|
4416
|
+
await Bun.sleep(drainMs - quietFor);
|
|
4417
|
+
this.server.stop(true);
|
|
4418
|
+
this.server = null;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
|
|
3605
4422
|
// src/worktree/co-author.ts
|
|
3606
4423
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3607
4424
|
import { join as join5 } from "path";
|
|
@@ -4620,7 +5437,7 @@ import {
|
|
|
4620
5437
|
rmSync as rmSync3,
|
|
4621
5438
|
writeFileSync as writeFileSync4
|
|
4622
5439
|
} from "fs";
|
|
4623
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as
|
|
5440
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
|
|
4624
5441
|
|
|
4625
5442
|
class OutsideWorktree extends Error {
|
|
4626
5443
|
path;
|
|
@@ -4679,7 +5496,7 @@ class ConfinedFs {
|
|
|
4679
5496
|
const root = this.roots[0];
|
|
4680
5497
|
if (!root)
|
|
4681
5498
|
throw new OutsideWorktree(path);
|
|
4682
|
-
return
|
|
5499
|
+
return isAbsolute5(path) ? resolve2(path) : resolve2(root, path);
|
|
4683
5500
|
}
|
|
4684
5501
|
read(path, line, limit) {
|
|
4685
5502
|
const real = this.resolveExisting(path);
|
|
@@ -4973,7 +5790,7 @@ function callText(call) {
|
|
|
4973
5790
|
}
|
|
4974
5791
|
|
|
4975
5792
|
// src/acp/mcp-servers.ts
|
|
4976
|
-
import { isAbsolute as
|
|
5793
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
4977
5794
|
var HYPOTENUSE_SERVER_NAME = "hypotenuse";
|
|
4978
5795
|
var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
4979
5796
|
|
|
@@ -5023,7 +5840,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
|
|
|
5023
5840
|
const [command, ...args] = s.command;
|
|
5024
5841
|
servers.push({
|
|
5025
5842
|
name: s.name,
|
|
5026
|
-
command:
|
|
5843
|
+
command: isAbsolute6(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
|
|
5027
5844
|
args,
|
|
5028
5845
|
env: pairs(s.name, s.env, env)
|
|
5029
5846
|
});
|
|
@@ -5473,6 +6290,8 @@ class Turn {
|
|
|
5473
6290
|
where;
|
|
5474
6291
|
readOnly;
|
|
5475
6292
|
agentNote;
|
|
6293
|
+
otel;
|
|
6294
|
+
receiver = null;
|
|
5476
6295
|
constructor(ctx, options) {
|
|
5477
6296
|
this.ctx = ctx;
|
|
5478
6297
|
this.options = options;
|
|
@@ -5498,6 +6317,7 @@ class Turn {
|
|
|
5498
6317
|
}
|
|
5499
6318
|
};
|
|
5500
6319
|
this.mapper = new TurnMapper(this.mapperOptions);
|
|
6320
|
+
this.otel = new AcpTelemetry(ctx.telemetry ?? null, this.cwd);
|
|
5501
6321
|
this.questions = new QuestionQueue({
|
|
5502
6322
|
post: (event) => this.emit(event),
|
|
5503
6323
|
waiting: (waiting) => {
|
|
@@ -5609,6 +6429,7 @@ class Turn {
|
|
|
5609
6429
|
throw error;
|
|
5610
6430
|
} finally {
|
|
5611
6431
|
clearInterval(timer);
|
|
6432
|
+
this.otel.promptEnded({ "hypotenuse.end_reason": this.endReason }, this.endReason && this.endReason !== "stop" ? this.endReason : null);
|
|
5612
6433
|
await this.shutdown();
|
|
5613
6434
|
await this.workspace?.release().catch((error) => ctx.log.warn(`releasing the workspace failed: ${errorMessage(error)}`, {
|
|
5614
6435
|
session: ctx.assignment.agentSessionId
|
|
@@ -5619,10 +6440,28 @@ class Turn {
|
|
|
5619
6440
|
const { ctx } = this;
|
|
5620
6441
|
const { assignment } = ctx;
|
|
5621
6442
|
if (ctx.prepareWorkspace) {
|
|
6443
|
+
const prepare = ctx.prepareWorkspace;
|
|
5622
6444
|
try {
|
|
5623
|
-
this.workspace = await
|
|
5624
|
-
|
|
5625
|
-
|
|
6445
|
+
this.workspace = await this.otel.around("worktree.prepare", {
|
|
6446
|
+
"hypotenuse.repository": assignment.repository?.remote ?? null,
|
|
6447
|
+
"hypotenuse.work_item.fresh": assignment.fresh
|
|
6448
|
+
}, async (span) => {
|
|
6449
|
+
const workspace = await prepare({
|
|
6450
|
+
output: (line) => {
|
|
6451
|
+
this.log.note(line);
|
|
6452
|
+
this.otel.turn?.log({
|
|
6453
|
+
body: line,
|
|
6454
|
+
eventName: "worktree.output",
|
|
6455
|
+
span
|
|
6456
|
+
});
|
|
6457
|
+
},
|
|
6458
|
+
env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
|
|
6459
|
+
});
|
|
6460
|
+
span?.set({
|
|
6461
|
+
"hypotenuse.worktree.mode": workspace.mode,
|
|
6462
|
+
"vcs.ref.head.name": workspace.branch
|
|
6463
|
+
});
|
|
6464
|
+
return workspace;
|
|
5626
6465
|
});
|
|
5627
6466
|
} catch (error) {
|
|
5628
6467
|
const remote = ctx.assignment.repository?.remote ?? null;
|
|
@@ -5656,6 +6495,7 @@ class Turn {
|
|
|
5656
6495
|
if (this.endReason)
|
|
5657
6496
|
throw new TurnFailure("stopped");
|
|
5658
6497
|
this.mapperOptions.cwd = this.cwd;
|
|
6498
|
+
this.otel.cwd = this.cwd;
|
|
5659
6499
|
const remote = this.workspace?.repository?.remote ?? null;
|
|
5660
6500
|
if (remote) {
|
|
5661
6501
|
this.mapperOptions.pullRequests = new PrDetector(remote, this.workspace?.repositoryConfig?.pr_url_patterns ?? [], ctx.pullRequests ?? []);
|
|
@@ -5684,13 +6524,17 @@ class Turn {
|
|
|
5684
6524
|
if (coAuthor && this.workspace?.repository) {
|
|
5685
6525
|
Object.assign(launch.env, coAuthorEnvironment(launch.env, installCoAuthorHooks(ctx.stateDir), coAuthor));
|
|
5686
6526
|
}
|
|
6527
|
+
this.withTelemetry(launch);
|
|
5687
6528
|
this.log.note(`spawn ${launch.command.join(" ")} in ${this.cwd}`);
|
|
5688
6529
|
try {
|
|
5689
6530
|
this.conn = AcpConnection.spawn({
|
|
5690
6531
|
command: launch.command,
|
|
5691
6532
|
cwd: this.cwd,
|
|
5692
6533
|
env: launch.env,
|
|
5693
|
-
stderr: (line) =>
|
|
6534
|
+
stderr: (line) => {
|
|
6535
|
+
this.log.stderr(line);
|
|
6536
|
+
this.otel.stderr(line);
|
|
6537
|
+
},
|
|
5694
6538
|
noise: (line) => this.noise(line),
|
|
5695
6539
|
trace: (line, message) => {
|
|
5696
6540
|
if (line.startsWith("<-")) {
|
|
@@ -5698,37 +6542,45 @@ class Turn {
|
|
|
5698
6542
|
this.noiseInARow = 0;
|
|
5699
6543
|
}
|
|
5700
6544
|
this.log.rpc(line, message);
|
|
6545
|
+
this.otel.frame(line, message);
|
|
5701
6546
|
},
|
|
5702
6547
|
handlers: {
|
|
5703
6548
|
update: (n) => {
|
|
5704
6549
|
if (this.acpSessionId && n.sessionId !== this.acpSessionId)
|
|
5705
6550
|
return;
|
|
6551
|
+
this.otel.update(n.update);
|
|
5706
6552
|
this.emitAll(this.mapper.update(n.update));
|
|
5707
6553
|
},
|
|
5708
6554
|
requestPermission: (params) => this.permission(params),
|
|
5709
6555
|
readTextFile: async (params) => {
|
|
6556
|
+
const done = this.otel.fs("read", params.path);
|
|
5710
6557
|
try {
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
};
|
|
6558
|
+
const content = this.fs.read(params.path, params.line, params.limit);
|
|
6559
|
+
done({ bytes: Buffer.byteLength(content) });
|
|
6560
|
+
return { content };
|
|
5714
6561
|
} catch (error) {
|
|
6562
|
+
done({ refused: errorMessage(error) });
|
|
5715
6563
|
throw this.fsError(error);
|
|
5716
6564
|
}
|
|
5717
6565
|
},
|
|
5718
6566
|
writeTextFile: async (params) => {
|
|
6567
|
+
const done = this.otel.fs("write", params.path);
|
|
5719
6568
|
if (this.readOnly) {
|
|
5720
6569
|
this.log.note(`refused a write (read-only): ${params.path}`);
|
|
6570
|
+
done({ refused: "read-only" });
|
|
5721
6571
|
throw new AcpError(RESOURCE_REFUSED, READ_ONLY_MESSAGE, {
|
|
5722
6572
|
path: params.path
|
|
5723
6573
|
});
|
|
5724
6574
|
}
|
|
5725
6575
|
try {
|
|
5726
6576
|
this.fs.write(params.path, params.content);
|
|
6577
|
+
done({ bytes: Buffer.byteLength(params.content) });
|
|
5727
6578
|
} catch (error) {
|
|
6579
|
+
done({ refused: errorMessage(error) });
|
|
5728
6580
|
throw this.fsError(error);
|
|
5729
6581
|
}
|
|
5730
6582
|
},
|
|
5731
|
-
createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId))),
|
|
6583
|
+
createElicitation: (params) => this.otel.around("elicitation.wait", {}, () => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId)))),
|
|
5732
6584
|
unknown: (method) => this.log.note(`unhandled ${method}`)
|
|
5733
6585
|
}
|
|
5734
6586
|
});
|
|
@@ -5739,7 +6591,20 @@ class Turn {
|
|
|
5739
6591
|
const conn = this.conn;
|
|
5740
6592
|
ctx.setPid(conn.pid);
|
|
5741
6593
|
ctx.setState("starting");
|
|
5742
|
-
|
|
6594
|
+
this.otel.spawned({
|
|
6595
|
+
"process.pid": conn.pid,
|
|
6596
|
+
"hypotenuse.agent.id": this.agentId,
|
|
6597
|
+
"hypotenuse.agent.preset": launch.preset ?? "custom",
|
|
6598
|
+
"hypotenuse.telemetry.native": this.receiver !== null
|
|
6599
|
+
}, launch.command.join(" "));
|
|
6600
|
+
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));
|
|
6601
|
+
this.otel.initialized({
|
|
6602
|
+
"gen_ai.agent.name": init.agentInfo?.name ?? this.agentId,
|
|
6603
|
+
"hypotenuse.agent.version": init.agentInfo?.version ?? null,
|
|
6604
|
+
"hypotenuse.acp.protocol_version": init.protocolVersion,
|
|
6605
|
+
"hypotenuse.acp.load_session": Boolean(init.agentCapabilities?.loadSession),
|
|
6606
|
+
"hypotenuse.acp.auth_methods": (init.authMethods ?? []).length
|
|
6607
|
+
});
|
|
5743
6608
|
this.agentTitle = init.agentInfo?.title ?? init.agentInfo?.name ?? this.agentId;
|
|
5744
6609
|
this.agentVersion = init.agentInfo?.version ?? null;
|
|
5745
6610
|
ctx.log.debug(`${this.agentId} initialized: ${init.agentInfo?.name ?? "?"} ${init.agentInfo?.version ?? ""}`, { session: assignment.agentSessionId });
|
|
@@ -5795,7 +6660,7 @@ class Turn {
|
|
|
5795
6660
|
if (!method)
|
|
5796
6661
|
throw new AuthRequired(this.loginNeed(), "not logged in");
|
|
5797
6662
|
this.log.note(`authenticate ${method}`);
|
|
5798
|
-
await this.guard(this.conn.authenticate(method), 0, "");
|
|
6663
|
+
await this.otel.around("acp.authenticate", { "hypotenuse.acp.auth_method": method }, () => this.guard(this.conn.authenticate(method), 0, ""));
|
|
5799
6664
|
authenticated = true;
|
|
5800
6665
|
}
|
|
5801
6666
|
}
|
|
@@ -5823,12 +6688,12 @@ class Turn {
|
|
|
5823
6688
|
if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
|
|
5824
6689
|
try {
|
|
5825
6690
|
if (caps.sessionCapabilities?.resume) {
|
|
5826
|
-
const answer = await this.guard(conn.resumeSession({
|
|
6691
|
+
const answer = await this.otel.around("acp.session.resume", {}, () => this.guard(conn.resumeSession({
|
|
5827
6692
|
sessionId: previousAcpSessionId,
|
|
5828
6693
|
cwd: this.cwd,
|
|
5829
6694
|
mcpServers,
|
|
5830
6695
|
...directories
|
|
5831
|
-
}), 0, "");
|
|
6696
|
+
}), 0, ""));
|
|
5832
6697
|
this.acpSessionId = previousAcpSessionId;
|
|
5833
6698
|
this.resumed = true;
|
|
5834
6699
|
await this.applyMode(answer);
|
|
@@ -5838,12 +6703,12 @@ class Turn {
|
|
|
5838
6703
|
this.acpSessionId = previousAcpSessionId;
|
|
5839
6704
|
this.mapper.replaying = true;
|
|
5840
6705
|
try {
|
|
5841
|
-
const answer = await this.guard(conn.loadSession({
|
|
6706
|
+
const answer = await this.otel.around("acp.session.load", {}, () => this.guard(conn.loadSession({
|
|
5842
6707
|
sessionId: previousAcpSessionId,
|
|
5843
6708
|
cwd: this.cwd,
|
|
5844
6709
|
mcpServers,
|
|
5845
6710
|
...directories
|
|
5846
|
-
}), 0, "");
|
|
6711
|
+
}), 0, ""));
|
|
5847
6712
|
this.resumed = true;
|
|
5848
6713
|
await this.applyMode(answer);
|
|
5849
6714
|
return;
|
|
@@ -5859,7 +6724,7 @@ class Turn {
|
|
|
5859
6724
|
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
6725
|
}
|
|
5861
6726
|
}
|
|
5862
|
-
const created = await this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, "");
|
|
6727
|
+
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
6728
|
this.acpSessionId = created.sessionId;
|
|
5864
6729
|
await this.applyMode(created);
|
|
5865
6730
|
}
|
|
@@ -5931,6 +6796,17 @@ class Turn {
|
|
|
5931
6796
|
}, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
|
|
5932
6797
|
this.promptStartedAt = this.now();
|
|
5933
6798
|
this.lastTraffic = this.now();
|
|
6799
|
+
const promptText = blocks.map((b) => b.text ?? "").join(`
|
|
6800
|
+
|
|
6801
|
+
`);
|
|
6802
|
+
this.otel.promptStarted({
|
|
6803
|
+
"hypotenuse.prompt.blocks": blocks.length,
|
|
6804
|
+
"hypotenuse.prompt.bytes": Buffer.byteLength(promptText),
|
|
6805
|
+
"hypotenuse.prompt.history": ctx.assignment.history.length,
|
|
6806
|
+
"hypotenuse.prompt.guidance": ctx.assignment.guidance.length,
|
|
6807
|
+
"hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
|
|
6808
|
+
"hypotenuse.acp.resumed": this.resumed
|
|
6809
|
+
}, promptText);
|
|
5934
6810
|
const request = conn.prompt(sessionId, blocks);
|
|
5935
6811
|
this.promptSettled = request.then(() => {}, () => {});
|
|
5936
6812
|
const answer = await this.guard(request, 0, "").catch((error) => {
|
|
@@ -5938,6 +6814,12 @@ class Turn {
|
|
|
5938
6814
|
throw error;
|
|
5939
6815
|
throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
|
|
5940
6816
|
});
|
|
6817
|
+
const usage = answer.usage;
|
|
6818
|
+
this.otel.promptEnded({
|
|
6819
|
+
"gen_ai.response.finish_reasons": answer.stopReason,
|
|
6820
|
+
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
6821
|
+
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
6822
|
+
}, answer.stopReason === "end_turn" || answer.stopReason === "cancelled" ? null : `stopReason: ${answer.stopReason}`);
|
|
5941
6823
|
if (this.endReason)
|
|
5942
6824
|
return await this.stopped();
|
|
5943
6825
|
this.emit({ type: "status", state: "finishing" });
|
|
@@ -6022,6 +6904,33 @@ class Turn {
|
|
|
6022
6904
|
await within(conn.exited, 2000, null);
|
|
6023
6905
|
}
|
|
6024
6906
|
async shutdown() {
|
|
6907
|
+
await this.closeAgent();
|
|
6908
|
+
if (this.conn)
|
|
6909
|
+
this.otel.exited(await within(this.conn.exited, 500, null));
|
|
6910
|
+
await this.receiver?.close();
|
|
6911
|
+
this.receiver = null;
|
|
6912
|
+
}
|
|
6913
|
+
withTelemetry(launch) {
|
|
6914
|
+
const turn = this.ctx.telemetry;
|
|
6915
|
+
if (!turn)
|
|
6916
|
+
return;
|
|
6917
|
+
const traceparent = this.otel.traceparent;
|
|
6918
|
+
if (traceparent)
|
|
6919
|
+
launch.env.TRACEPARENT = traceparent;
|
|
6920
|
+
if (!turn.agentNative)
|
|
6921
|
+
return;
|
|
6922
|
+
this.receiver = new NativeReceiver(turn);
|
|
6923
|
+
const endpoint = this.receiver.start();
|
|
6924
|
+
turn.addSecret(this.receiver.nonce);
|
|
6925
|
+
dropInheritedOtel(launch.env);
|
|
6926
|
+
const native = nativeTelemetryLaunch(launch.preset, {
|
|
6927
|
+
endpoint,
|
|
6928
|
+
level: turn.level
|
|
6929
|
+
});
|
|
6930
|
+
Object.assign(launch.env, native.env);
|
|
6931
|
+
launch.command = [...launch.command, ...native.args];
|
|
6932
|
+
}
|
|
6933
|
+
async closeAgent() {
|
|
6025
6934
|
const conn = this.conn;
|
|
6026
6935
|
if (!conn)
|
|
6027
6936
|
return;
|
|
@@ -6074,13 +6983,26 @@ class Turn {
|
|
|
6074
6983
|
async permission(params) {
|
|
6075
6984
|
const policy = this.role?.permission_policy ?? "allow";
|
|
6076
6985
|
const decision = decide(policy, params.toolCall.kind);
|
|
6986
|
+
const waited = this.otel.permission(params.toolCall, typeof policy === "string" ? policy : JSON.stringify(policy), params.options);
|
|
6077
6987
|
if (decision !== "ask") {
|
|
6078
6988
|
const optionId = automaticOption(params.options, decision);
|
|
6079
6989
|
this.log.note(`permission ${params.toolCall.toolCallId}: ${decision} \u2192 ${optionId ?? "cancelled"}`);
|
|
6990
|
+
waited({
|
|
6991
|
+
outcome: optionId ? "selected" : "cancelled",
|
|
6992
|
+
optionId: optionId ?? undefined,
|
|
6993
|
+
decidedBy: "policy"
|
|
6994
|
+
});
|
|
6080
6995
|
return optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } };
|
|
6081
6996
|
}
|
|
6082
6997
|
this.emitAll(this.mapper.flushThought(true));
|
|
6083
|
-
|
|
6998
|
+
const answer = await this.questions.ask(permissionQuestion(params, this.where, this.cwd));
|
|
6999
|
+
const outcome = answer.outcome;
|
|
7000
|
+
waited({
|
|
7001
|
+
outcome: outcome?.outcome ?? "cancelled",
|
|
7002
|
+
optionId: outcome?.optionId,
|
|
7003
|
+
decidedBy: "person"
|
|
7004
|
+
});
|
|
7005
|
+
return answer;
|
|
6084
7006
|
}
|
|
6085
7007
|
fsError(error) {
|
|
6086
7008
|
if (error instanceof OutsideWorktree) {
|
|
@@ -6282,6 +7204,9 @@ class DaemonApi {
|
|
|
6282
7204
|
"User-Agent": `hypotenuse/${package_default.version} (${platform()})`
|
|
6283
7205
|
};
|
|
6284
7206
|
}
|
|
7207
|
+
traced(traceparent) {
|
|
7208
|
+
return traceparent ? { ...this.headers, [TRACEPARENT_HEADER]: traceparent } : this.headers;
|
|
7209
|
+
}
|
|
6285
7210
|
async heartbeat(body) {
|
|
6286
7211
|
const answer = await this.client.request("POST", "/daemon/heartbeat", {
|
|
6287
7212
|
body,
|
|
@@ -6309,26 +7234,41 @@ class DaemonApi {
|
|
|
6309
7234
|
});
|
|
6310
7235
|
return ClaimResponse.parse(answer).assignment;
|
|
6311
7236
|
}
|
|
6312
|
-
async events(body) {
|
|
7237
|
+
async events(body, traceparent) {
|
|
6313
7238
|
const answer = await this.client.request("POST", "/daemon/events", {
|
|
6314
7239
|
body,
|
|
6315
|
-
headers: this.
|
|
7240
|
+
headers: this.traced(traceparent),
|
|
6316
7241
|
timeoutMs: REQUEST_TIMEOUT_MS
|
|
6317
7242
|
});
|
|
6318
7243
|
return EventsResponse.parse(answer);
|
|
6319
7244
|
}
|
|
6320
|
-
async complete(body) {
|
|
7245
|
+
async complete(body, traceparent) {
|
|
6321
7246
|
const answer = await this.client.request("POST", "/daemon/complete", {
|
|
6322
7247
|
body,
|
|
6323
|
-
headers: this.
|
|
7248
|
+
headers: this.traced(traceparent),
|
|
6324
7249
|
timeoutMs: REQUEST_TIMEOUT_MS
|
|
6325
7250
|
});
|
|
6326
7251
|
return CompleteResponse.parse(answer);
|
|
6327
7252
|
}
|
|
6328
|
-
async
|
|
7253
|
+
async telemetry(batch) {
|
|
7254
|
+
const answer = await this.client.request("POST", "/daemon/telemetry", {
|
|
7255
|
+
rawBody: batch.gzipped,
|
|
7256
|
+
headers: {
|
|
7257
|
+
...this.headers,
|
|
7258
|
+
"Content-Type": "application/json",
|
|
7259
|
+
"Content-Encoding": "gzip",
|
|
7260
|
+
[TELEMETRY_SIGNAL_HEADER]: batch.signal,
|
|
7261
|
+
[TELEMETRY_WORK_ITEM_HEADER]: String(batch.workItemId),
|
|
7262
|
+
[TRACEPARENT_HEADER]: batch.traceparent
|
|
7263
|
+
},
|
|
7264
|
+
timeoutMs: REQUEST_TIMEOUT_MS
|
|
7265
|
+
});
|
|
7266
|
+
return TelemetryResponse.parse(answer);
|
|
7267
|
+
}
|
|
7268
|
+
async tool(route, body, traceparent) {
|
|
6329
7269
|
const answer = await this.client.request("POST", route, {
|
|
6330
7270
|
body,
|
|
6331
|
-
headers: this.
|
|
7271
|
+
headers: this.traced(traceparent),
|
|
6332
7272
|
timeoutMs: TOOL_REQUEST_TIMEOUT_MS
|
|
6333
7273
|
});
|
|
6334
7274
|
return ToolResult.parse(answer).result;
|
|
@@ -6953,11 +7893,575 @@ class StateDb {
|
|
|
6953
7893
|
|
|
6954
7894
|
// src/daemon/supervisor.ts
|
|
6955
7895
|
import { hostname as hostname3 } from "os";
|
|
6956
|
-
import { isAbsolute as
|
|
7896
|
+
import { isAbsolute as isAbsolute7 } from "path";
|
|
7897
|
+
|
|
7898
|
+
// src/telemetry/collector.ts
|
|
7899
|
+
var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
|
|
7900
|
+
var TOKEN_BOUNDS = [
|
|
7901
|
+
100,
|
|
7902
|
+
1000,
|
|
7903
|
+
1e4,
|
|
7904
|
+
50000,
|
|
7905
|
+
1e5,
|
|
7906
|
+
250000,
|
|
7907
|
+
500000,
|
|
7908
|
+
1e6
|
|
7909
|
+
];
|
|
7910
|
+
|
|
7911
|
+
class SpanHandle {
|
|
7912
|
+
turn;
|
|
7913
|
+
name;
|
|
7914
|
+
parentSpanId;
|
|
7915
|
+
kind;
|
|
7916
|
+
spanId;
|
|
7917
|
+
startMs;
|
|
7918
|
+
attrs = {};
|
|
7919
|
+
events = [];
|
|
7920
|
+
ended = false;
|
|
7921
|
+
error = null;
|
|
7922
|
+
constructor(turn, name, parentSpanId, kind = "internal", attrs = {}, spanId = newSpanId()) {
|
|
7923
|
+
this.turn = turn;
|
|
7924
|
+
this.name = name;
|
|
7925
|
+
this.parentSpanId = parentSpanId;
|
|
7926
|
+
this.kind = kind;
|
|
7927
|
+
this.spanId = spanId;
|
|
7928
|
+
this.startMs = turn.now();
|
|
7929
|
+
Object.assign(this.attrs, attrs);
|
|
7930
|
+
}
|
|
7931
|
+
set(attrs) {
|
|
7932
|
+
Object.assign(this.attrs, attrs);
|
|
7933
|
+
return this;
|
|
7934
|
+
}
|
|
7935
|
+
content(key, text) {
|
|
7936
|
+
Object.assign(this.attrs, this.turn.contentAttrs(key, text));
|
|
7937
|
+
return this;
|
|
7938
|
+
}
|
|
7939
|
+
event(name, attrs = {}) {
|
|
7940
|
+
if (!this.ended)
|
|
7941
|
+
this.events?.push({
|
|
7942
|
+
name,
|
|
7943
|
+
timeUnixNano: unixNano(this.turn.now()),
|
|
7944
|
+
attributes: attributes(attrs)
|
|
7945
|
+
});
|
|
7946
|
+
return this;
|
|
7947
|
+
}
|
|
7948
|
+
fail(message) {
|
|
7949
|
+
this.error = message.slice(0, 1024);
|
|
7950
|
+
return this;
|
|
7951
|
+
}
|
|
7952
|
+
get traceparent() {
|
|
7953
|
+
return this.turn.traceparentFor(this.spanId);
|
|
7954
|
+
}
|
|
7955
|
+
get isEnded() {
|
|
7956
|
+
return this.ended;
|
|
7957
|
+
}
|
|
7958
|
+
end(attrs = {}) {
|
|
7959
|
+
if (this.ended)
|
|
7960
|
+
return;
|
|
7961
|
+
this.ended = true;
|
|
7962
|
+
Object.assign(this.attrs, attrs);
|
|
7963
|
+
this.turn.record({
|
|
7964
|
+
traceId: this.turn.traceId,
|
|
7965
|
+
spanId: this.spanId,
|
|
7966
|
+
parentSpanId: this.parentSpanId,
|
|
7967
|
+
name: this.name,
|
|
7968
|
+
kind: SPAN_KIND[this.kind],
|
|
7969
|
+
startTimeUnixNano: unixNano(this.startMs),
|
|
7970
|
+
endTimeUnixNano: unixNano(this.turn.now()),
|
|
7971
|
+
attributes: attributes(this.attrs),
|
|
7972
|
+
events: this.events,
|
|
7973
|
+
status: this.error ? { code: STATUS_CODE.error, message: this.error } : { code: STATUS_CODE.unset }
|
|
7974
|
+
});
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
|
|
7978
|
+
class TurnTelemetry {
|
|
7979
|
+
options;
|
|
7980
|
+
traceId;
|
|
7981
|
+
rootSpanId;
|
|
7982
|
+
level;
|
|
7983
|
+
sampled;
|
|
7984
|
+
agentNative;
|
|
7985
|
+
now;
|
|
7986
|
+
turnSpan;
|
|
7987
|
+
resource;
|
|
7988
|
+
finished = false;
|
|
7989
|
+
extraSecrets = [];
|
|
7990
|
+
promptSpan = null;
|
|
7991
|
+
constructor(options) {
|
|
7992
|
+
this.options = options;
|
|
7993
|
+
const ctx = parseTraceparent(options.claim.traceparent);
|
|
7994
|
+
this.traceId = ctx?.traceId ?? "0".repeat(32);
|
|
7995
|
+
this.rootSpanId = ctx?.spanId ?? "0".repeat(16);
|
|
7996
|
+
this.level = options.claim.content;
|
|
7997
|
+
this.sampled = options.claim.sampled && ctx !== null;
|
|
7998
|
+
this.agentNative = options.claim.agentNative;
|
|
7999
|
+
this.now = options.now ?? Date.now;
|
|
8000
|
+
const a = options.assignment;
|
|
8001
|
+
this.resource = attributes({
|
|
8002
|
+
"service.name": "hypotenuse-daemon",
|
|
8003
|
+
"service.namespace": "hypotenuse",
|
|
8004
|
+
"service.version": package_default.version,
|
|
8005
|
+
"hypotenuse.source": "daemon",
|
|
8006
|
+
...this.turnAttributes(),
|
|
8007
|
+
"hypotenuse.repository": a.repository?.remote ?? null
|
|
8008
|
+
});
|
|
8009
|
+
this.turnSpan = new SpanHandle(this, "daemon.turn", this.rootSpanId, "internal", {
|
|
8010
|
+
...this.turnAttributes(),
|
|
8011
|
+
"hypotenuse.work_item.kind": a.kind,
|
|
8012
|
+
"hypotenuse.work_item.fresh": a.fresh,
|
|
8013
|
+
"hypotenuse.telemetry.content": this.level,
|
|
8014
|
+
"hypotenuse.telemetry.agent_native": this.agentNative
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
8017
|
+
turnAttributes() {
|
|
8018
|
+
const a = this.options.assignment;
|
|
8019
|
+
return {
|
|
8020
|
+
"hypotenuse.session.id": a.agentSessionId,
|
|
8021
|
+
"hypotenuse.work_item.id": a.workItemId,
|
|
8022
|
+
"hypotenuse.role": a.role,
|
|
8023
|
+
"hypotenuse.runner.name": this.options.runner.name,
|
|
8024
|
+
"hypotenuse.agent.id": this.options.agentId,
|
|
8025
|
+
"hypotenuse.agent.preset": this.options.preset ?? "custom",
|
|
8026
|
+
"hypotenuse.repository": a.repository?.remote ?? null,
|
|
8027
|
+
"linear.issue.identifier": a.issue.identifier,
|
|
8028
|
+
"linear.agent_session.id": a.externalSessionId,
|
|
8029
|
+
"linear.team.key": a.issue.teamKey
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
addSecret(value) {
|
|
8033
|
+
this.extraSecrets.push(value);
|
|
8034
|
+
}
|
|
8035
|
+
get resourceAttributes() {
|
|
8036
|
+
return this.resource;
|
|
8037
|
+
}
|
|
8038
|
+
collects(level) {
|
|
8039
|
+
return levelRank(this.level) >= levelRank(level);
|
|
8040
|
+
}
|
|
8041
|
+
traceparentFor(spanId = this.turnSpan.spanId) {
|
|
8042
|
+
return formatTraceparent({
|
|
8043
|
+
traceId: this.traceId,
|
|
8044
|
+
spanId,
|
|
8045
|
+
sampled: this.sampled
|
|
8046
|
+
});
|
|
8047
|
+
}
|
|
8048
|
+
span(name, attrs = {}, parent = null, kind = "internal", spanId) {
|
|
8049
|
+
return new SpanHandle(this, name, (parent ?? this.turnSpan).spanId, kind, attrs, spanId);
|
|
8050
|
+
}
|
|
8051
|
+
contentAttrs(key, text) {
|
|
8052
|
+
if (text === null || text === undefined)
|
|
8053
|
+
return {};
|
|
8054
|
+
const out = {
|
|
8055
|
+
[`hypotenuse.${key}.bytes`]: Buffer.byteLength(text)
|
|
8056
|
+
};
|
|
8057
|
+
if (!this.collects("content"))
|
|
8058
|
+
return out;
|
|
8059
|
+
const { text: kept, originalBytes } = capContent(text);
|
|
8060
|
+
out[`${CONTENT_ATTRIBUTE_PREFIX}${key}`] = kept;
|
|
8061
|
+
if (originalBytes !== null)
|
|
8062
|
+
out[`${CONTENT_ATTRIBUTE_PREFIX}${key}${TRUNCATED_SUFFIX}`] = originalBytes;
|
|
8063
|
+
return out;
|
|
8064
|
+
}
|
|
8065
|
+
record(span) {
|
|
8066
|
+
if (!this.sampled || this.finished)
|
|
8067
|
+
return;
|
|
8068
|
+
this.enqueue("traces", {
|
|
8069
|
+
resourceSpans: [
|
|
8070
|
+
{
|
|
8071
|
+
resource: { attributes: this.resource },
|
|
8072
|
+
scopeSpans: [{ scope: SCOPE, spans: [span] }]
|
|
8073
|
+
}
|
|
8074
|
+
]
|
|
8075
|
+
});
|
|
8076
|
+
}
|
|
8077
|
+
log(record) {
|
|
8078
|
+
const level = record.level ?? "content";
|
|
8079
|
+
if (!this.sampled || this.finished || !this.collects(level))
|
|
8080
|
+
return;
|
|
8081
|
+
const { text, originalBytes } = capContent(record.body);
|
|
8082
|
+
this.enqueue("logs", {
|
|
8083
|
+
resourceLogs: [
|
|
8084
|
+
{
|
|
8085
|
+
resource: { attributes: this.resource },
|
|
8086
|
+
scopeLogs: [
|
|
8087
|
+
{
|
|
8088
|
+
scope: SCOPE,
|
|
8089
|
+
logRecords: [
|
|
8090
|
+
{
|
|
8091
|
+
timeUnixNano: unixNano(this.now()),
|
|
8092
|
+
observedTimeUnixNano: unixNano(this.now()),
|
|
8093
|
+
severityNumber: SEVERITY[record.severity ?? "info"],
|
|
8094
|
+
severityText: (record.severity ?? "info").toUpperCase(),
|
|
8095
|
+
body: { stringValue: text },
|
|
8096
|
+
...record.eventName ? { eventName: record.eventName } : {},
|
|
8097
|
+
attributes: attributes({
|
|
8098
|
+
...record.attrs ?? {},
|
|
8099
|
+
[LEVEL_ATTRIBUTE]: level,
|
|
8100
|
+
...originalBytes !== null ? {
|
|
8101
|
+
[`hypotenuse.body${TRUNCATED_SUFFIX}`]: originalBytes
|
|
8102
|
+
} : {}
|
|
8103
|
+
}),
|
|
8104
|
+
traceId: this.traceId,
|
|
8105
|
+
spanId: (record.span ?? this.turnSpan).spanId
|
|
8106
|
+
}
|
|
8107
|
+
]
|
|
8108
|
+
}
|
|
8109
|
+
]
|
|
8110
|
+
}
|
|
8111
|
+
]
|
|
8112
|
+
});
|
|
8113
|
+
}
|
|
8114
|
+
frame(direction, message, span) {
|
|
8115
|
+
if (!this.collects("full"))
|
|
8116
|
+
return;
|
|
8117
|
+
const m = message;
|
|
8118
|
+
this.log({
|
|
8119
|
+
body: typeof message === "string" ? message : JSON.stringify(message),
|
|
8120
|
+
level: "full",
|
|
8121
|
+
eventName: "acp.frame",
|
|
8122
|
+
attrs: {
|
|
8123
|
+
"hypotenuse.acp.direction": direction.startsWith("<-") ? "in" : "out",
|
|
8124
|
+
"rpc.method": typeof m?.method === "string" ? m.method : null,
|
|
8125
|
+
"rpc.jsonrpc.request_id": typeof m?.id === "string" || typeof m?.id === "number" ? String(m.id) : null
|
|
8126
|
+
},
|
|
8127
|
+
span
|
|
8128
|
+
});
|
|
8129
|
+
}
|
|
8130
|
+
usage(usage) {
|
|
8131
|
+
if (this.finished)
|
|
8132
|
+
return;
|
|
8133
|
+
const points = [
|
|
8134
|
+
["input", usage.inputTokens],
|
|
8135
|
+
["output", usage.outputTokens]
|
|
8136
|
+
].flatMap(([type, n]) => typeof n === "number" ? [
|
|
8137
|
+
{
|
|
8138
|
+
attributes: attributes({
|
|
8139
|
+
"gen_ai.token.type": type,
|
|
8140
|
+
"gen_ai.agent.name": this.options.agentId
|
|
8141
|
+
}),
|
|
8142
|
+
timeUnixNano: unixNano(this.now()),
|
|
8143
|
+
count: "1",
|
|
8144
|
+
sum: n,
|
|
8145
|
+
bucketCounts: [
|
|
8146
|
+
...TOKEN_BOUNDS.map((b, i) => n <= b && (i === 0 || n > TOKEN_BOUNDS[i - 1]) ? "1" : "0"),
|
|
8147
|
+
n > TOKEN_BOUNDS.at(-1) ? "1" : "0"
|
|
8148
|
+
],
|
|
8149
|
+
explicitBounds: TOKEN_BOUNDS,
|
|
8150
|
+
min: n,
|
|
8151
|
+
max: n
|
|
8152
|
+
}
|
|
8153
|
+
] : []);
|
|
8154
|
+
if (points.length === 0)
|
|
8155
|
+
return;
|
|
8156
|
+
this.metric({
|
|
8157
|
+
name: "gen_ai.client.token.usage",
|
|
8158
|
+
unit: "{token}",
|
|
8159
|
+
histogram: {
|
|
8160
|
+
aggregationTemporality: TEMPORALITY.delta,
|
|
8161
|
+
dataPoints: points
|
|
8162
|
+
}
|
|
8163
|
+
});
|
|
8164
|
+
}
|
|
8165
|
+
slots(slots) {
|
|
8166
|
+
const at = unixNano(this.now());
|
|
8167
|
+
this.metric({
|
|
8168
|
+
name: "hypotenuse.runner.slots",
|
|
8169
|
+
unit: "{slot}",
|
|
8170
|
+
gauge: {
|
|
8171
|
+
dataPoints: [
|
|
8172
|
+
{
|
|
8173
|
+
attributes: attributes({ "hypotenuse.slot.state": "capacity" }),
|
|
8174
|
+
timeUnixNano: at,
|
|
8175
|
+
asInt: String(slots.capacity)
|
|
8176
|
+
},
|
|
8177
|
+
{
|
|
8178
|
+
attributes: attributes({ "hypotenuse.slot.state": "in_use" }),
|
|
8179
|
+
timeUnixNano: at,
|
|
8180
|
+
asInt: String(slots.inUse)
|
|
8181
|
+
}
|
|
8182
|
+
]
|
|
8183
|
+
}
|
|
8184
|
+
});
|
|
8185
|
+
}
|
|
8186
|
+
metric(metric) {
|
|
8187
|
+
this.enqueue("metrics", {
|
|
8188
|
+
resourceMetrics: [
|
|
8189
|
+
{
|
|
8190
|
+
resource: { attributes: this.resource },
|
|
8191
|
+
scopeMetrics: [{ scope: SCOPE, metrics: [metric] }]
|
|
8192
|
+
}
|
|
8193
|
+
]
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8196
|
+
enqueue(signal, body) {
|
|
8197
|
+
const refused = this.options.queue.isRefused(this.options.assignment.workItemId);
|
|
8198
|
+
this.options.queue.add({
|
|
8199
|
+
workItemId: this.options.assignment.workItemId,
|
|
8200
|
+
traceparent: this.traceparentFor(),
|
|
8201
|
+
signal,
|
|
8202
|
+
body: refused ? body : redactOtlp(body, {
|
|
8203
|
+
values: [...this.options.secrets(), ...this.extraSecrets]
|
|
8204
|
+
})
|
|
8205
|
+
}, this.resource);
|
|
8206
|
+
}
|
|
8207
|
+
finish(outcome, error) {
|
|
8208
|
+
if (this.finished)
|
|
8209
|
+
return;
|
|
8210
|
+
if (error)
|
|
8211
|
+
this.turnSpan.fail(error);
|
|
8212
|
+
this.turnSpan.end({ "hypotenuse.outcome": outcome });
|
|
8213
|
+
this.finished = true;
|
|
8214
|
+
}
|
|
8215
|
+
}
|
|
8216
|
+
|
|
8217
|
+
// src/telemetry/queue.ts
|
|
8218
|
+
var FLUSH_INTERVAL_MS = 2000;
|
|
8219
|
+
var FLUSH_BYTES = 1024 * 1024;
|
|
8220
|
+
|
|
8221
|
+
class TelemetryQueue {
|
|
8222
|
+
sender;
|
|
8223
|
+
capacityBytes;
|
|
8224
|
+
log;
|
|
8225
|
+
now;
|
|
8226
|
+
batches = [];
|
|
8227
|
+
bytes = 0;
|
|
8228
|
+
timer = null;
|
|
8229
|
+
flushing = null;
|
|
8230
|
+
backoffUntil = 0;
|
|
8231
|
+
failures = 0;
|
|
8232
|
+
refused = new Set;
|
|
8233
|
+
losses = new Map;
|
|
8234
|
+
droppedTotal = new Map;
|
|
8235
|
+
constructor(sender, capacityBytes, log = () => {}, now = Date.now) {
|
|
8236
|
+
this.sender = sender;
|
|
8237
|
+
this.capacityBytes = capacityBytes;
|
|
8238
|
+
this.log = log;
|
|
8239
|
+
this.now = now;
|
|
8240
|
+
}
|
|
8241
|
+
start() {
|
|
8242
|
+
if (this.timer)
|
|
8243
|
+
return;
|
|
8244
|
+
this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
8245
|
+
this.timer.unref?.();
|
|
8246
|
+
}
|
|
8247
|
+
stop() {
|
|
8248
|
+
if (this.timer)
|
|
8249
|
+
clearInterval(this.timer);
|
|
8250
|
+
this.timer = null;
|
|
8251
|
+
}
|
|
8252
|
+
get pendingBytes() {
|
|
8253
|
+
return this.bytes;
|
|
8254
|
+
}
|
|
8255
|
+
isRefused(workItemId) {
|
|
8256
|
+
return this.refused.has(workItemId);
|
|
8257
|
+
}
|
|
8258
|
+
add(batch, resource = []) {
|
|
8259
|
+
if (this.refused.has(batch.workItemId)) {
|
|
8260
|
+
this.countLoss(batch, resource);
|
|
8261
|
+
return;
|
|
8262
|
+
}
|
|
8263
|
+
const bytes = JSON.stringify(batch.body).length;
|
|
8264
|
+
this.batches.push({ ...batch, bytes });
|
|
8265
|
+
this.bytes += bytes;
|
|
8266
|
+
if (!this.losses.has(batch.workItemId))
|
|
8267
|
+
this.losses.set(batch.workItemId, {
|
|
8268
|
+
spans: 0,
|
|
8269
|
+
logs: 0,
|
|
8270
|
+
resource,
|
|
8271
|
+
traceparent: batch.traceparent
|
|
8272
|
+
});
|
|
8273
|
+
this.evict();
|
|
8274
|
+
if (this.bytes >= FLUSH_BYTES)
|
|
8275
|
+
this.flush();
|
|
8276
|
+
}
|
|
8277
|
+
countLoss(batch, resource = []) {
|
|
8278
|
+
const loss = this.losses.get(batch.workItemId) ?? {
|
|
8279
|
+
spans: 0,
|
|
8280
|
+
logs: 0,
|
|
8281
|
+
resource,
|
|
8282
|
+
traceparent: batch.traceparent
|
|
8283
|
+
};
|
|
8284
|
+
const n = countRecords(batch.signal, batch.body);
|
|
8285
|
+
if (batch.signal === "traces")
|
|
8286
|
+
loss.spans += n;
|
|
8287
|
+
else if (batch.signal === "logs")
|
|
8288
|
+
loss.logs += n;
|
|
8289
|
+
this.losses.set(batch.workItemId, loss);
|
|
8290
|
+
this.droppedTotal.set(batch.workItemId, (this.droppedTotal.get(batch.workItemId) ?? 0) + n);
|
|
8291
|
+
}
|
|
8292
|
+
evict() {
|
|
8293
|
+
for (const signal of ["logs", "traces"]) {
|
|
8294
|
+
while (this.bytes > this.capacityBytes) {
|
|
8295
|
+
const index = this.batches.findIndex((b) => b.signal === signal);
|
|
8296
|
+
if (index === -1)
|
|
8297
|
+
break;
|
|
8298
|
+
const [gone] = this.batches.splice(index, 1);
|
|
8299
|
+
if (!gone)
|
|
8300
|
+
break;
|
|
8301
|
+
this.bytes -= gone.bytes;
|
|
8302
|
+
this.countLoss(gone);
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
reportLosses() {
|
|
8307
|
+
for (const [workItemId, loss] of this.losses) {
|
|
8308
|
+
if (this.refused.has(workItemId))
|
|
8309
|
+
continue;
|
|
8310
|
+
if (loss.spans === 0 && loss.logs === 0)
|
|
8311
|
+
continue;
|
|
8312
|
+
const at = unixNano(this.now());
|
|
8313
|
+
const point = (kind, n) => ({
|
|
8314
|
+
attributes: attributes({ "hypotenuse.telemetry.kind": kind }),
|
|
8315
|
+
timeUnixNano: at,
|
|
8316
|
+
asInt: String(n)
|
|
8317
|
+
});
|
|
8318
|
+
const metrics = {
|
|
8319
|
+
resourceMetrics: [
|
|
8320
|
+
{
|
|
8321
|
+
resource: { attributes: loss.resource },
|
|
8322
|
+
scopeMetrics: [
|
|
8323
|
+
{
|
|
8324
|
+
scope: { name: "hypotenuse.daemon" },
|
|
8325
|
+
metrics: [
|
|
8326
|
+
{
|
|
8327
|
+
name: "hypotenuse.telemetry.dropped",
|
|
8328
|
+
unit: "{record}",
|
|
8329
|
+
sum: {
|
|
8330
|
+
aggregationTemporality: TEMPORALITY.delta,
|
|
8331
|
+
isMonotonic: true,
|
|
8332
|
+
dataPoints: [
|
|
8333
|
+
...loss.spans ? [point("span", loss.spans)] : [],
|
|
8334
|
+
...loss.logs ? [point("log", loss.logs)] : []
|
|
8335
|
+
]
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
]
|
|
8339
|
+
}
|
|
8340
|
+
]
|
|
8341
|
+
}
|
|
8342
|
+
]
|
|
8343
|
+
};
|
|
8344
|
+
loss.spans = 0;
|
|
8345
|
+
loss.logs = 0;
|
|
8346
|
+
const bytes = JSON.stringify(metrics).length;
|
|
8347
|
+
this.batches.push({
|
|
8348
|
+
workItemId,
|
|
8349
|
+
traceparent: loss.traceparent,
|
|
8350
|
+
signal: "metrics",
|
|
8351
|
+
body: metrics,
|
|
8352
|
+
bytes
|
|
8353
|
+
});
|
|
8354
|
+
this.bytes += bytes;
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
flush() {
|
|
8358
|
+
this.flushing ??= this.flushOnce().finally(() => {
|
|
8359
|
+
this.flushing = null;
|
|
8360
|
+
});
|
|
8361
|
+
return this.flushing;
|
|
8362
|
+
}
|
|
8363
|
+
async flushOnce() {
|
|
8364
|
+
if (this.now() < this.backoffUntil)
|
|
8365
|
+
return;
|
|
8366
|
+
this.reportLosses();
|
|
8367
|
+
if (this.batches.length === 0)
|
|
8368
|
+
return;
|
|
8369
|
+
const taken = this.batches;
|
|
8370
|
+
this.batches = [];
|
|
8371
|
+
this.bytes = 0;
|
|
8372
|
+
const groups = new Map;
|
|
8373
|
+
for (const b of taken) {
|
|
8374
|
+
const key = `${b.workItemId}|${b.signal}`;
|
|
8375
|
+
groups.set(key, [...groups.get(key) ?? [], b]);
|
|
8376
|
+
}
|
|
8377
|
+
const retry = [];
|
|
8378
|
+
let failed = false;
|
|
8379
|
+
for (const group of groups.values()) {
|
|
8380
|
+
const first = group[0];
|
|
8381
|
+
if (this.refused.has(first.workItemId)) {
|
|
8382
|
+
for (const b of group)
|
|
8383
|
+
this.countLoss(b);
|
|
8384
|
+
continue;
|
|
8385
|
+
}
|
|
8386
|
+
if (failed) {
|
|
8387
|
+
retry.push(...group);
|
|
8388
|
+
continue;
|
|
8389
|
+
}
|
|
8390
|
+
const body = mergeRequests(first.signal, group.map((b) => b.body));
|
|
8391
|
+
try {
|
|
8392
|
+
await this.sender.telemetry({
|
|
8393
|
+
workItemId: first.workItemId,
|
|
8394
|
+
traceparent: first.traceparent,
|
|
8395
|
+
signal: first.signal,
|
|
8396
|
+
gzipped: Bun.gzipSync(new TextEncoder().encode(JSON.stringify(body)))
|
|
8397
|
+
});
|
|
8398
|
+
this.failures = 0;
|
|
8399
|
+
} catch (error) {
|
|
8400
|
+
if (error instanceof ApiError && (error.status === 409 || error.status === 410 || error.status === 400)) {
|
|
8401
|
+
this.refused.add(first.workItemId);
|
|
8402
|
+
for (const b of group)
|
|
8403
|
+
this.countLoss(b);
|
|
8404
|
+
this.log(`telemetry for work item ${first.workItemId} refused (${error.status}): ${error.message}`);
|
|
8405
|
+
continue;
|
|
8406
|
+
}
|
|
8407
|
+
failed = true;
|
|
8408
|
+
retry.push(...group);
|
|
8409
|
+
this.failures++;
|
|
8410
|
+
const wait = error instanceof ApiError && error.retryAfterMs ? error.retryAfterMs : Math.min(1000 * 2 ** (this.failures - 1), 60000);
|
|
8411
|
+
this.backoffUntil = this.now() + wait;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
this.batches = [...retry, ...this.batches];
|
|
8415
|
+
this.bytes = this.batches.reduce((n, b) => n + b.bytes, 0);
|
|
8416
|
+
this.evict();
|
|
8417
|
+
}
|
|
8418
|
+
async drain(timeoutMs) {
|
|
8419
|
+
const deadline = this.now() + timeoutMs;
|
|
8420
|
+
for (;; ) {
|
|
8421
|
+
this.backoffUntil = Math.min(this.backoffUntil, this.now());
|
|
8422
|
+
await this.flush();
|
|
8423
|
+
if (this.batches.length === 0)
|
|
8424
|
+
return true;
|
|
8425
|
+
if (this.now() >= deadline)
|
|
8426
|
+
return false;
|
|
8427
|
+
await Bun.sleep(Math.min(500, Math.max(0, deadline - this.now())));
|
|
8428
|
+
}
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8431
|
+
function countRecords(signal, body) {
|
|
8432
|
+
if (signal === "traces")
|
|
8433
|
+
return (body.resourceSpans ?? []).reduce((n, rs) => n + (rs.scopeSpans ?? []).reduce((m, s) => m + (s.spans?.length ?? 0), 0), 0);
|
|
8434
|
+
if (signal === "logs")
|
|
8435
|
+
return (body.resourceLogs ?? []).reduce((n, rl) => n + (rl.scopeLogs ?? []).reduce((m, s) => m + (s.logRecords?.length ?? 0), 0), 0);
|
|
8436
|
+
return 0;
|
|
8437
|
+
}
|
|
8438
|
+
|
|
8439
|
+
// src/telemetry/redact.ts
|
|
8440
|
+
function resolvedConfigSecrets(config, env) {
|
|
8441
|
+
const out = [];
|
|
8442
|
+
const take = (table) => {
|
|
8443
|
+
for (const value of Object.values(table ?? {})) {
|
|
8444
|
+
if (!value.includes("$"))
|
|
8445
|
+
continue;
|
|
8446
|
+
const resolved = expandVariables(value, env);
|
|
8447
|
+
if (resolved)
|
|
8448
|
+
out.push(resolved);
|
|
8449
|
+
}
|
|
8450
|
+
};
|
|
8451
|
+
for (const agent of Object.values(config.agents))
|
|
8452
|
+
take(agent.env);
|
|
8453
|
+
for (const role of [config.roles.manager, config.roles.worker]) {
|
|
8454
|
+
for (const server of role?.extra_mcp_servers ?? []) {
|
|
8455
|
+
take(server.env);
|
|
8456
|
+
take(server.headers);
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
return out;
|
|
8460
|
+
}
|
|
6957
8461
|
|
|
6958
8462
|
// src/worktree/cleanup.ts
|
|
6959
8463
|
import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
|
|
6960
|
-
import { join as join9, relative as
|
|
8464
|
+
import { join as join9, relative as relative4, resolve as resolve3 } from "path";
|
|
6961
8465
|
|
|
6962
8466
|
// src/worktree/merge-state.ts
|
|
6963
8467
|
var MERGE_STATE_TTL_MS = 10 * 60000;
|
|
@@ -7136,7 +8640,7 @@ class WorktreeCleaner {
|
|
|
7136
8640
|
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
7137
8641
|
if (!path || !branch || known.has(path))
|
|
7138
8642
|
continue;
|
|
7139
|
-
const rel =
|
|
8643
|
+
const rel = relative4(root, path);
|
|
7140
8644
|
if (rel.startsWith("..") || rel === "")
|
|
7141
8645
|
continue;
|
|
7142
8646
|
const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
|
|
@@ -7361,6 +8865,11 @@ function buildCapabilities(config, machine, repositories) {
|
|
|
7361
8865
|
teams: config.subscriptions.teams,
|
|
7362
8866
|
projects: config.subscriptions.projects,
|
|
7363
8867
|
labels: config.subscriptions.labels
|
|
8868
|
+
},
|
|
8869
|
+
telemetry: {
|
|
8870
|
+
enabled: config.telemetry.enabled,
|
|
8871
|
+
maxContent: config.telemetry.max_content,
|
|
8872
|
+
agentNative: config.telemetry.agent_native
|
|
7364
8873
|
}
|
|
7365
8874
|
};
|
|
7366
8875
|
}
|
|
@@ -7434,6 +8943,7 @@ class EventSink {
|
|
|
7434
8943
|
backoff = new Backoff;
|
|
7435
8944
|
batchLimit = new Map;
|
|
7436
8945
|
closed = false;
|
|
8946
|
+
traceparents = new Map;
|
|
7437
8947
|
constructor(state, api, log, hooks) {
|
|
7438
8948
|
this.state = state;
|
|
7439
8949
|
this.api = api;
|
|
@@ -7460,6 +8970,12 @@ class EventSink {
|
|
|
7460
8970
|
this.kick();
|
|
7461
8971
|
return { eventId, seq };
|
|
7462
8972
|
}
|
|
8973
|
+
trace(workItemId, traceparent) {
|
|
8974
|
+
if (traceparent)
|
|
8975
|
+
this.traceparents.set(workItemId, traceparent);
|
|
8976
|
+
else
|
|
8977
|
+
this.traceparents.delete(workItemId);
|
|
8978
|
+
}
|
|
7463
8979
|
complete(completion) {
|
|
7464
8980
|
this.state.queueCompletion(completion.request.workItemId, completion);
|
|
7465
8981
|
this.kick();
|
|
@@ -7574,7 +9090,7 @@ class EventSink {
|
|
|
7574
9090
|
const answer = await this.api.events({
|
|
7575
9091
|
workItemId,
|
|
7576
9092
|
events: batch.map((row) => row.payload)
|
|
7577
|
-
});
|
|
9093
|
+
}, this.traceparents.get(workItemId));
|
|
7578
9094
|
this.state.deleteEvents(ids);
|
|
7579
9095
|
this.log.debug("events sent", {
|
|
7580
9096
|
workItem: workItemId,
|
|
@@ -7632,7 +9148,8 @@ class EventSink {
|
|
|
7632
9148
|
return;
|
|
7633
9149
|
const { request } = row.payload;
|
|
7634
9150
|
try {
|
|
7635
|
-
const answer = await this.api.complete(request);
|
|
9151
|
+
const answer = await this.api.complete(request, this.traceparents.get(workItemId));
|
|
9152
|
+
this.traceparents.delete(workItemId);
|
|
7636
9153
|
this.log.info(`reported work item ${request.outcome}`, {
|
|
7637
9154
|
workItem: workItemId,
|
|
7638
9155
|
alreadyTerminal: answer.alreadyTerminal
|
|
@@ -7702,7 +9219,7 @@ class ReadCache {
|
|
|
7702
9219
|
}
|
|
7703
9220
|
|
|
7704
9221
|
// src/mcp/tools.ts
|
|
7705
|
-
import { z as
|
|
9222
|
+
import { z as z11 } from "zod";
|
|
7706
9223
|
var CACHED_TOOLS = new Set([
|
|
7707
9224
|
"linear_get_issue",
|
|
7708
9225
|
"linear_list_comments",
|
|
@@ -7710,7 +9227,7 @@ var CACHED_TOOLS = new Set([
|
|
|
7710
9227
|
"linear_list_labels"
|
|
7711
9228
|
]);
|
|
7712
9229
|
function describeTool(tool) {
|
|
7713
|
-
const { $schema: _schema, ...schema } =
|
|
9230
|
+
const { $schema: _schema, ...schema } = z11.toJSONSchema(tool.input);
|
|
7714
9231
|
return {
|
|
7715
9232
|
name: tool.name,
|
|
7716
9233
|
description: tool.description,
|
|
@@ -7789,11 +9306,14 @@ class McpBroker {
|
|
|
7789
9306
|
if (!checked.ok)
|
|
7790
9307
|
return { text: checked.message, isError: true };
|
|
7791
9308
|
const { tool, input } = checked;
|
|
9309
|
+
const span = turn.trace?.(tool.name, input) ?? null;
|
|
7792
9310
|
const cached = CACHED_TOOLS.has(tool.name);
|
|
7793
9311
|
if (cached) {
|
|
7794
9312
|
const hit = this.cache.get(turn.workItemId, tool.name, input);
|
|
7795
|
-
if (hit)
|
|
9313
|
+
if (hit) {
|
|
9314
|
+
span?.end({ isError: false, cached: true, text: hit.text });
|
|
7796
9315
|
return hit;
|
|
9316
|
+
}
|
|
7797
9317
|
}
|
|
7798
9318
|
if (tool.kind === "write")
|
|
7799
9319
|
this.cache.invalidate(turn.workItemId);
|
|
@@ -7808,15 +9328,17 @@ class McpBroker {
|
|
|
7808
9328
|
const result = await this.transport.tool(tool.route, {
|
|
7809
9329
|
...input,
|
|
7810
9330
|
agentSessionId: turn.agentSessionId
|
|
7811
|
-
});
|
|
9331
|
+
}, span?.traceparent);
|
|
7812
9332
|
answer = { text: result.text, data: result.data };
|
|
7813
9333
|
} catch (error) {
|
|
7814
9334
|
if (asking)
|
|
7815
9335
|
turn.askingUser(false);
|
|
7816
9336
|
answer = { text: toolErrorText(error), isError: true };
|
|
7817
9337
|
this.log(`${tool.name} failed: ${answer.text}`);
|
|
9338
|
+
span?.end({ isError: true, cached: false, text: answer.text });
|
|
7818
9339
|
return answer;
|
|
7819
9340
|
}
|
|
9341
|
+
span?.end({ isError: false, cached: false, text: answer.text });
|
|
7820
9342
|
if (cached)
|
|
7821
9343
|
this.cache.set(turn.workItemId, tool.name, input, answer);
|
|
7822
9344
|
if (tool.name === "linear_report_pr" && answer.data?.linked)
|
|
@@ -8133,6 +9655,8 @@ class Supervisor {
|
|
|
8133
9655
|
sweepTimer = null;
|
|
8134
9656
|
diskWasLow = false;
|
|
8135
9657
|
broker;
|
|
9658
|
+
telemetryQueue;
|
|
9659
|
+
secrets = [];
|
|
8136
9660
|
constructor(options) {
|
|
8137
9661
|
this.options = options;
|
|
8138
9662
|
this.config = options.config;
|
|
@@ -8154,6 +9678,8 @@ class Supervisor {
|
|
|
8154
9678
|
}
|
|
8155
9679
|
});
|
|
8156
9680
|
this.broker = new McpBroker(options.api, (line) => options.log.info(line));
|
|
9681
|
+
this.telemetryQueue = new TelemetryQueue(options.api, this.config.telemetry.buffer_mb * 1024 * 1024, (line) => options.log.info(line));
|
|
9682
|
+
this.secrets = this.baseSecrets();
|
|
8157
9683
|
this.sink = new EventSink(options.state, options.api, options.log, {
|
|
8158
9684
|
onCancelRequested: (id) => this.stop(id, "stop"),
|
|
8159
9685
|
onNotYours: (id) => this.silence(id),
|
|
@@ -8183,10 +9709,42 @@ class Supervisor {
|
|
|
8183
9709
|
}
|
|
8184
9710
|
this.link.start();
|
|
8185
9711
|
this.sink.kick(true);
|
|
9712
|
+
this.telemetryQueue.start();
|
|
8186
9713
|
this.sweepTimer = setInterval(() => void this.cleanup({}), this.options.sweepIntervalMs ?? SWEEP_INTERVAL_MS);
|
|
8187
9714
|
this.sweepTimer.unref?.();
|
|
8188
9715
|
return interrupted;
|
|
8189
9716
|
}
|
|
9717
|
+
baseSecrets() {
|
|
9718
|
+
return [
|
|
9719
|
+
...this.options.secrets ?? [],
|
|
9720
|
+
...resolvedConfigSecrets(this.config, this.options.env)
|
|
9721
|
+
];
|
|
9722
|
+
}
|
|
9723
|
+
turnTelemetry(assignment, agentId, slot) {
|
|
9724
|
+
const claim = assignment.telemetry;
|
|
9725
|
+
if (!claim || !this.config.telemetry.enabled)
|
|
9726
|
+
return null;
|
|
9727
|
+
const preset = this.config.agents[agentId]?.preset ?? (isPreset(agentId) ? agentId : null);
|
|
9728
|
+
const max = this.config.telemetry.max_content;
|
|
9729
|
+
const telemetry = new TurnTelemetry({
|
|
9730
|
+
claim: {
|
|
9731
|
+
...claim,
|
|
9732
|
+
content: minLevel(claim.content, max),
|
|
9733
|
+
agentNative: claim.agentNative && this.config.telemetry.agent_native
|
|
9734
|
+
},
|
|
9735
|
+
assignment,
|
|
9736
|
+
runner: { name: this.config.runner.name },
|
|
9737
|
+
agentId,
|
|
9738
|
+
preset,
|
|
9739
|
+
queue: this.telemetryQueue,
|
|
9740
|
+
secrets: () => slot.nonce ? [...this.secrets, slot.nonce] : this.secrets
|
|
9741
|
+
});
|
|
9742
|
+
telemetry.slots({
|
|
9743
|
+
capacity: this.config.runner.max_parallel_jobs,
|
|
9744
|
+
inUse: this.slots.size
|
|
9745
|
+
});
|
|
9746
|
+
return telemetry;
|
|
9747
|
+
}
|
|
8190
9748
|
worktreeInUse(wt) {
|
|
8191
9749
|
if (wt.state === "active")
|
|
8192
9750
|
return true;
|
|
@@ -8392,6 +9950,9 @@ class Supervisor {
|
|
|
8392
9950
|
};
|
|
8393
9951
|
this.slots.set(assignment.workItemId, slot);
|
|
8394
9952
|
const mcpCommand = this.options.mcpCommand === undefined ? selfCommand() : this.options.mcpCommand;
|
|
9953
|
+
const telemetry = this.turnTelemetry(assignment, agentId, slot);
|
|
9954
|
+
if (telemetry)
|
|
9955
|
+
this.sink.trace(assignment.workItemId, telemetry.traceparentFor());
|
|
8395
9956
|
if (mcpCommand) {
|
|
8396
9957
|
slot.nonce = this.broker.register({
|
|
8397
9958
|
agentSessionId: assignment.agentSessionId,
|
|
@@ -8405,7 +9966,25 @@ class Supervisor {
|
|
|
8405
9966
|
reportedPullRequest: (url) => {
|
|
8406
9967
|
this.options.state.addPullRequest(slot.agentSessionId, url);
|
|
8407
9968
|
slot.onReportedPullRequest?.(url);
|
|
8408
|
-
}
|
|
9969
|
+
},
|
|
9970
|
+
trace: telemetry ? (tool, input) => {
|
|
9971
|
+
const span = telemetry.span("mcp.call", {
|
|
9972
|
+
"gen_ai.operation.name": "execute_tool",
|
|
9973
|
+
"gen_ai.tool.name": tool,
|
|
9974
|
+
"hypotenuse.mcp.server": "hypotenuse"
|
|
9975
|
+
}, telemetry.promptSpan, "client").content("mcp.arguments", JSON.stringify(input));
|
|
9976
|
+
return {
|
|
9977
|
+
traceparent: span.traceparent,
|
|
9978
|
+
end: (result) => {
|
|
9979
|
+
if (result.isError)
|
|
9980
|
+
span.fail(result.text.slice(0, 500));
|
|
9981
|
+
span.content("mcp.result", result.text).end({
|
|
9982
|
+
"hypotenuse.mcp.cached": result.cached,
|
|
9983
|
+
"hypotenuse.mcp.result.bytes": Buffer.byteLength(result.text)
|
|
9984
|
+
});
|
|
9985
|
+
}
|
|
9986
|
+
};
|
|
9987
|
+
} : undefined
|
|
8409
9988
|
});
|
|
8410
9989
|
}
|
|
8411
9990
|
log.info(`claimed ${assignment.kind} for ${assignment.issue.identifier ?? "an issue"}`, {
|
|
@@ -8473,7 +10052,8 @@ class Supervisor {
|
|
|
8473
10052
|
},
|
|
8474
10053
|
onReportedPullRequest: (handler) => {
|
|
8475
10054
|
slot.onReportedPullRequest = handler;
|
|
8476
|
-
}
|
|
10055
|
+
},
|
|
10056
|
+
telemetry
|
|
8477
10057
|
};
|
|
8478
10058
|
slot.done = this.runTurn(slot, ctx);
|
|
8479
10059
|
}
|
|
@@ -8527,8 +10107,18 @@ class Supervisor {
|
|
|
8527
10107
|
});
|
|
8528
10108
|
this.link.beatNow();
|
|
8529
10109
|
const worktreeId = this.options.state.session(slot.agentSessionId)?.worktreeId;
|
|
8530
|
-
|
|
8531
|
-
|
|
10110
|
+
const telemetry = ctx.telemetry ?? null;
|
|
10111
|
+
if (worktreeId) {
|
|
10112
|
+
const span = telemetry?.span("worktree.cleanup", {
|
|
10113
|
+
"hypotenuse.worktree.id": worktreeId
|
|
10114
|
+
});
|
|
10115
|
+
const report = await this.cleanup({ only: [worktreeId] });
|
|
10116
|
+
span?.end({
|
|
10117
|
+
"hypotenuse.worktree.removed": (report?.removed.length ?? 0) > 0,
|
|
10118
|
+
"hypotenuse.worktree.kept": (report?.kept.length ?? 0) > 0
|
|
10119
|
+
});
|
|
10120
|
+
}
|
|
10121
|
+
telemetry?.finish(slot.silent ? "taken_away" : result.outcome, result.error);
|
|
8532
10122
|
}
|
|
8533
10123
|
stop(workItemId, reason) {
|
|
8534
10124
|
const slot = this.slots.get(workItemId);
|
|
@@ -8587,6 +10177,7 @@ class Supervisor {
|
|
|
8587
10177
|
await this.waitIdle(this.config.sessions.stop_grace + 5000);
|
|
8588
10178
|
}
|
|
8589
10179
|
const flushed = await this.sink.drain(5000);
|
|
10180
|
+
await this.telemetryQueue.drain(5000);
|
|
8590
10181
|
this.link.beatNow();
|
|
8591
10182
|
return { drained: this.slots.size === 0 && flushed, cancelled };
|
|
8592
10183
|
}
|
|
@@ -8648,6 +10239,8 @@ class Supervisor {
|
|
|
8648
10239
|
}
|
|
8649
10240
|
this.config = merged;
|
|
8650
10241
|
this.options.log.level = merged.log.level;
|
|
10242
|
+
this.telemetryQueue.capacityBytes = merged.telemetry.buffer_mb * 1024 * 1024;
|
|
10243
|
+
this.secrets = this.baseSecrets();
|
|
8651
10244
|
this.repositories = daemonRepositories(merged, this.options.machine);
|
|
8652
10245
|
this.capabilities = this.buildCapabilities();
|
|
8653
10246
|
this.options.log.info("config reloaded", {
|
|
@@ -8742,6 +10335,7 @@ class Supervisor {
|
|
|
8742
10335
|
this.sweepTimer = null;
|
|
8743
10336
|
this.link.stop();
|
|
8744
10337
|
this.sink.close();
|
|
10338
|
+
this.telemetryQueue.stop();
|
|
8745
10339
|
}
|
|
8746
10340
|
}
|
|
8747
10341
|
function changedKeys(a, b, prefix = "") {
|
|
@@ -8756,7 +10350,7 @@ function changedKeys(a, b, prefix = "") {
|
|
|
8756
10350
|
return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
|
|
8757
10351
|
}
|
|
8758
10352
|
function resolveCommand(command, env) {
|
|
8759
|
-
if (
|
|
10353
|
+
if (isAbsolute7(command))
|
|
8760
10354
|
return command;
|
|
8761
10355
|
return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
|
|
8762
10356
|
}
|
|
@@ -8854,6 +10448,7 @@ async function runDaemon(options) {
|
|
|
8854
10448
|
let state;
|
|
8855
10449
|
let origin;
|
|
8856
10450
|
let runner;
|
|
10451
|
+
let credentials;
|
|
8857
10452
|
try {
|
|
8858
10453
|
const read = readDaemonConfig(options, env);
|
|
8859
10454
|
if (!read.config)
|
|
@@ -8862,7 +10457,6 @@ async function runDaemon(options) {
|
|
|
8862
10457
|
paths = daemonPaths(read.path, options.stateDir, config.log.file, env);
|
|
8863
10458
|
log = new DaemonLog(paths.log, config.log.level, !options.detachedChild);
|
|
8864
10459
|
const credentialsFile = credentialsPath(env);
|
|
8865
|
-
let credentials;
|
|
8866
10460
|
try {
|
|
8867
10461
|
credentials = loadCredentials(credentialsFile);
|
|
8868
10462
|
} catch (error) {
|
|
@@ -8938,7 +10532,11 @@ async function runDaemon(options) {
|
|
|
8938
10532
|
stateDir: paths.stateDir,
|
|
8939
10533
|
env,
|
|
8940
10534
|
readConfig: () => readDaemonConfig(options, env),
|
|
8941
|
-
requestExit
|
|
10535
|
+
requestExit,
|
|
10536
|
+
secrets: [
|
|
10537
|
+
runner.token,
|
|
10538
|
+
...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
|
|
10539
|
+
]
|
|
8942
10540
|
});
|
|
8943
10541
|
control = new ControlServer(paths.socket, {
|
|
8944
10542
|
status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
|
|
@@ -9765,7 +11363,7 @@ config_version = 1
|
|
|
9765
11363
|
[runner]
|
|
9766
11364
|
# name = "{{hostname}}" # display only
|
|
9767
11365
|
# max_parallel_jobs = {{jobs}} # jobs (turns) at once on this machine
|
|
9768
|
-
roles = ["worker"]
|
|
11366
|
+
roles = ["manager", "worker"] # drop "manager" to only implement issues
|
|
9769
11367
|
|
|
9770
11368
|
[subscriptions]
|
|
9771
11369
|
# teams = ["ENG"] # only these Linear teams (empty: every team)
|
|
@@ -9780,11 +11378,11 @@ agent = "{{agent}}"
|
|
|
9780
11378
|
# prompt_file = "prompts/worker.md" # relative to this file; the built-in template otherwise
|
|
9781
11379
|
# permission_policy = "allow" # or "ask" to approve tool calls from Linear
|
|
9782
11380
|
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
#
|
|
9786
|
-
#
|
|
9787
|
-
# max_parallel = 1
|
|
11381
|
+
[roles.manager]
|
|
11382
|
+
agent = "{{agent}}"
|
|
11383
|
+
# read-only by default: edits, deletes, moves, and commands are refused
|
|
11384
|
+
# permission_policy = { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }
|
|
11385
|
+
# max_parallel = 1 # manager turns at once
|
|
9788
11386
|
|
|
9789
11387
|
{{repositories}}
|
|
9790
11388
|
|
|
@@ -10337,6 +11935,20 @@ async function runDoctor(ctx, options) {
|
|
|
10337
11935
|
if (options.agent && !resolveAgents(config).some((a) => a.id === options.agent)) {
|
|
10338
11936
|
list.fail("agents", `no agent "${options.agent}" in the config`);
|
|
10339
11937
|
}
|
|
11938
|
+
const t = config.telemetry;
|
|
11939
|
+
if (!t.enabled)
|
|
11940
|
+
list.ok("telemetry", "off: this runner sends no telemetry (`[telemetry] enabled = false`)");
|
|
11941
|
+
else {
|
|
11942
|
+
list.ok("telemetry", `sends up to \`${t.max_content}\` for the project's exporters; buffer ${t.buffer_mb} MB`);
|
|
11943
|
+
const agents = resolveAgents(config).filter((a) => a.roles.length > 0);
|
|
11944
|
+
if (!t.agent_native)
|
|
11945
|
+
list.ok("telemetry", "agents' own telemetry is not captured");
|
|
11946
|
+
else
|
|
11947
|
+
for (const agent of agents) {
|
|
11948
|
+
const support = nativeTelemetrySupport(agent.preset);
|
|
11949
|
+
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`);
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
10340
11952
|
for (const repo of daemonRepositories(config, machine)) {
|
|
10341
11953
|
if (options.offline) {
|
|
10342
11954
|
list.ok("repositories", `${repo.name}: ${repo.remote} (${repo.baseBranch})`);
|
|
@@ -12003,6 +13615,161 @@ function registerTagCommands(program) {
|
|
|
12003
13615
|
}
|
|
12004
13616
|
}
|
|
12005
13617
|
|
|
13618
|
+
// src/commands/telemetry.ts
|
|
13619
|
+
function collect2(value, previous = []) {
|
|
13620
|
+
return [...previous, value];
|
|
13621
|
+
}
|
|
13622
|
+
function parsePairs(pairs, what) {
|
|
13623
|
+
const out = {};
|
|
13624
|
+
for (const pair of pairs) {
|
|
13625
|
+
const at = pair.indexOf("=");
|
|
13626
|
+
if (at <= 0)
|
|
13627
|
+
throw new Error(`${what} must be KEY=VALUE, got "${pair}"`);
|
|
13628
|
+
out[pair.slice(0, at).trim()] = pair.slice(at + 1);
|
|
13629
|
+
}
|
|
13630
|
+
return out;
|
|
13631
|
+
}
|
|
13632
|
+
function exporterHealth(e) {
|
|
13633
|
+
if (!e.enabled)
|
|
13634
|
+
return "paused";
|
|
13635
|
+
if (e.lastErrorAt && (!e.lastExportAt || e.lastErrorAt > e.lastExportAt))
|
|
13636
|
+
return `error ${when(e.lastErrorAt)}: ${e.lastError ?? ""}`;
|
|
13637
|
+
return e.lastExportAt ? `ok ${when(e.lastExportAt)}` : "nothing yet";
|
|
13638
|
+
}
|
|
13639
|
+
function describeExporter(e) {
|
|
13640
|
+
const attrs = Object.entries(e.resourceAttributes).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
13641
|
+
return [
|
|
13642
|
+
`${e.name} (id ${e.id})${e.enabled ? "" : " paused"}`,
|
|
13643
|
+
`endpoint ${e.endpoint}`,
|
|
13644
|
+
`protocol ${e.protocol}`,
|
|
13645
|
+
`content ${e.content}`,
|
|
13646
|
+
`signals ${e.signals.join(", ")}`,
|
|
13647
|
+
`sampled ${Math.round(e.traceSampleRatio * 100)}% of turns`,
|
|
13648
|
+
`agent's own ${e.agentNative ? "captured" : "not captured"}`,
|
|
13649
|
+
`headers ${e.headerNames.length ? e.headerNames.join(", ") : "none"} (values are never shown)`,
|
|
13650
|
+
`attributes ${attrs || "none"}`,
|
|
13651
|
+
`last export ${when(e.lastExportAt)}`,
|
|
13652
|
+
`last error ${e.lastError ? `${when(e.lastErrorAt)}: ${e.lastError}` : "-"}`
|
|
13653
|
+
].join(`
|
|
13654
|
+
`);
|
|
13655
|
+
}
|
|
13656
|
+
function exporterBody(opts) {
|
|
13657
|
+
const body = {};
|
|
13658
|
+
if (opts.name !== undefined)
|
|
13659
|
+
body.name = opts.name;
|
|
13660
|
+
if (opts.endpoint !== undefined)
|
|
13661
|
+
body.endpoint = opts.endpoint;
|
|
13662
|
+
if (opts.protocol !== undefined)
|
|
13663
|
+
body.protocol = opts.protocol;
|
|
13664
|
+
if (opts.clearHeaders)
|
|
13665
|
+
body.headers = {};
|
|
13666
|
+
else if (opts.header?.length)
|
|
13667
|
+
body.headers = parsePairs(opts.header, "--header");
|
|
13668
|
+
if (opts.signals !== undefined)
|
|
13669
|
+
body.signals = opts.signals.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13670
|
+
if (opts.content !== undefined)
|
|
13671
|
+
body.content = opts.content;
|
|
13672
|
+
if (opts.agentNative !== undefined)
|
|
13673
|
+
body.agentNative = opts.agentNative;
|
|
13674
|
+
if (opts.sample !== undefined)
|
|
13675
|
+
body.traceSampleRatio = Number(opts.sample);
|
|
13676
|
+
if (opts.attribute?.length)
|
|
13677
|
+
body.resourceAttributes = parsePairs(opts.attribute, "--attribute");
|
|
13678
|
+
if (opts.enabled !== undefined)
|
|
13679
|
+
body.enabled = opts.enabled;
|
|
13680
|
+
return body;
|
|
13681
|
+
}
|
|
13682
|
+
function exporterOptions(command, adding) {
|
|
13683
|
+
if (adding)
|
|
13684
|
+
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)");
|
|
13685
|
+
else
|
|
13686
|
+
command.option("--name <name>", "a new name").option("--endpoint <url>", "a new endpoint");
|
|
13687
|
+
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");
|
|
13688
|
+
}
|
|
13689
|
+
function registerTelemetryCommands(program) {
|
|
13690
|
+
const telemetry = program.command("telemetry").description("Where the selected project's OpenTelemetry goes");
|
|
13691
|
+
const exporter = telemetry.command("exporter").description("The project's OpenTelemetry collectors");
|
|
13692
|
+
withPaging(exporter.command("list").description("List exporters")).action(async function() {
|
|
13693
|
+
await runCommand(this, async (ctx) => {
|
|
13694
|
+
const projectId = await ctx.projectId();
|
|
13695
|
+
const result = await ctx.signedInClient().request("GET", "/telemetry-exporters", {
|
|
13696
|
+
query: { projectId, ...pageQuery(this) }
|
|
13697
|
+
});
|
|
13698
|
+
emit(ctx, result, () => table(result.telemetryExporters.map((e) => ({
|
|
13699
|
+
id: e.id,
|
|
13700
|
+
name: e.name,
|
|
13701
|
+
endpoint: e.endpoint,
|
|
13702
|
+
content: e.content,
|
|
13703
|
+
signals: e.signals.join(","),
|
|
13704
|
+
sampled: `${Math.round(e.traceSampleRatio * 100)}%`,
|
|
13705
|
+
headers: e.headerNames.join(",") || "-",
|
|
13706
|
+
health: exporterHealth(e)
|
|
13707
|
+
})), [
|
|
13708
|
+
{ key: "id" },
|
|
13709
|
+
{ key: "name", maxWidth: 30 },
|
|
13710
|
+
{ key: "endpoint", maxWidth: 50 },
|
|
13711
|
+
{ key: "content" },
|
|
13712
|
+
{ key: "signals" },
|
|
13713
|
+
{ key: "sampled" },
|
|
13714
|
+
{ key: "headers", maxWidth: 30 },
|
|
13715
|
+
{ key: "health", maxWidth: 60 }
|
|
13716
|
+
]) + pageFooter(result.pagination));
|
|
13717
|
+
});
|
|
13718
|
+
});
|
|
13719
|
+
exporter.command("view <id>").description("Show one exporter (header names only)").action(async function(id) {
|
|
13720
|
+
await runCommand(this, async (ctx) => {
|
|
13721
|
+
const projectId = await ctx.projectId();
|
|
13722
|
+
const result = await ctx.signedInClient().request("GET", `/telemetry-exporter/${id}`, { query: { projectId } });
|
|
13723
|
+
emit(ctx, result, () => describeExporter(result.telemetryExporter));
|
|
13724
|
+
});
|
|
13725
|
+
});
|
|
13726
|
+
exporterOptions(exporter.command("add").description("Export the project's telemetry to a collector (admin)"), true).option("--disabled", "create it paused").action(async function() {
|
|
13727
|
+
await runCommand(this, async (ctx) => {
|
|
13728
|
+
const opts = this.opts();
|
|
13729
|
+
const projectId = await ctx.projectId();
|
|
13730
|
+
const result = await ctx.signedInClient().request("PUT", "/telemetry-exporter", {
|
|
13731
|
+
body: {
|
|
13732
|
+
projectId,
|
|
13733
|
+
...exporterBody(opts),
|
|
13734
|
+
...opts.disabled ? { enabled: false } : {}
|
|
13735
|
+
}
|
|
13736
|
+
});
|
|
13737
|
+
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`);
|
|
13738
|
+
});
|
|
13739
|
+
});
|
|
13740
|
+
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) {
|
|
13741
|
+
await runCommand(this, async (ctx) => {
|
|
13742
|
+
const opts = this.opts();
|
|
13743
|
+
const projectId = await ctx.projectId();
|
|
13744
|
+
const body = exporterBody({
|
|
13745
|
+
...opts,
|
|
13746
|
+
enabled: opts.enable ? true : opts.disable ? false : undefined
|
|
13747
|
+
});
|
|
13748
|
+
const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}`, { body: { projectId, ...body } });
|
|
13749
|
+
emit(ctx, result, () => describeExporter(result.telemetryExporter));
|
|
13750
|
+
});
|
|
13751
|
+
});
|
|
13752
|
+
exporter.command("remove <id>").description("Stop exporting to a collector and forget its headers (admin)").action(async function(id) {
|
|
13753
|
+
await runCommand(this, async (ctx) => {
|
|
13754
|
+
const projectId = await ctx.projectId();
|
|
13755
|
+
const result = await ctx.signedInClient().request("DELETE", `/telemetry-exporter/${id}`, {
|
|
13756
|
+
body: { projectId }
|
|
13757
|
+
});
|
|
13758
|
+
emit(ctx, result, () => `Removed exporter ${id}`);
|
|
13759
|
+
});
|
|
13760
|
+
});
|
|
13761
|
+
exporter.command("test <id>").description("Send the collector a probe span, log, and metric, and show its answers (admin)").action(async function(id) {
|
|
13762
|
+
await runCommand(this, async (ctx) => {
|
|
13763
|
+
const projectId = await ctx.projectId();
|
|
13764
|
+
const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}/test`, { body: { projectId } });
|
|
13765
|
+
emit(ctx, result, () => result.results.map((r) => `${glyph(r.ok ? "complete" : "error")} ${r.signal.padEnd(8)} ${r.status || "-"}${r.error ? ` ${r.error}` : ""}`).join(`
|
|
13766
|
+
`));
|
|
13767
|
+
if (!result.ok)
|
|
13768
|
+
process.exitCode = 1;
|
|
13769
|
+
});
|
|
13770
|
+
});
|
|
13771
|
+
}
|
|
13772
|
+
|
|
12006
13773
|
// src/commands/upgrade.ts
|
|
12007
13774
|
var UPGRADE_DRAIN_MS = 10 * 60000;
|
|
12008
13775
|
async function restartRunningDaemon(socketPath, call = callDaemon) {
|
|
@@ -12272,6 +14039,7 @@ function buildProgram() {
|
|
|
12272
14039
|
registerEventCommands(program);
|
|
12273
14040
|
registerRunnerCommands(program);
|
|
12274
14041
|
registerRoutingCommands(program);
|
|
14042
|
+
registerTelemetryCommands(program);
|
|
12275
14043
|
registerIdentityCommands(program);
|
|
12276
14044
|
registerConfigCommands(program);
|
|
12277
14045
|
registerDaemonCommands(program);
|