bopodev-api 0.1.25 → 0.1.26
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/package.json +4 -4
- package/src/routes/governance.ts +80 -2
- package/src/routes/observability.ts +157 -9
- package/src/scripts/onboard-seed.ts +5 -6
- package/src/services/attention-service.ts +23 -2
- package/src/services/governance-service.ts +4 -5
- package/src/services/heartbeat-service.ts +1251 -69
- package/src/services/memory-file-service.ts +0 -3
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
4
4
|
import { nanoid } from "nanoid";
|
|
5
5
|
import { resolveAdapter } from "bopodev-agent-sdk";
|
|
6
6
|
import type { AgentState, HeartbeatContext } from "bopodev-agent-sdk";
|
|
7
7
|
import {
|
|
8
|
+
type AgentFinalRunOutput,
|
|
8
9
|
ControlPlaneHeadersJsonSchema,
|
|
9
10
|
ControlPlaneRequestHeadersSchema,
|
|
10
11
|
ControlPlaneRuntimeEnvSchema,
|
|
11
12
|
ExecutionOutcomeSchema,
|
|
12
|
-
type ExecutionOutcome
|
|
13
|
+
type ExecutionOutcome,
|
|
14
|
+
type RunArtifact,
|
|
15
|
+
type RunCompletionReason,
|
|
16
|
+
type RunCompletionReport,
|
|
17
|
+
type RunCostSummary
|
|
13
18
|
} from "bopodev-contracts";
|
|
14
19
|
import type { BopoDb } from "bopodev-db";
|
|
15
20
|
import {
|
|
@@ -30,7 +35,12 @@ import {
|
|
|
30
35
|
import { appendAuditEvent, appendCost } from "bopodev-db";
|
|
31
36
|
import { parseRuntimeConfigFromAgentRow } from "../lib/agent-config";
|
|
32
37
|
import { bootstrapRepositoryWorkspace, ensureIsolatedGitWorktree, GitRuntimeError } from "../lib/git-runtime";
|
|
33
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
isInsidePath,
|
|
40
|
+
normalizeCompanyWorkspacePath,
|
|
41
|
+
resolveCompanyWorkspaceRootPath,
|
|
42
|
+
resolveProjectWorkspacePath
|
|
43
|
+
} from "../lib/instance-paths";
|
|
34
44
|
import { assertRuntimeCwdForCompany, getProjectWorkspaceContextMap, hasText, resolveAgentFallbackWorkspace } from "../lib/workspace-policy";
|
|
35
45
|
import type { RealtimeHub } from "../realtime/hub";
|
|
36
46
|
import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
|
|
@@ -73,6 +83,39 @@ type HeartbeatWakeContext = {
|
|
|
73
83
|
|
|
74
84
|
const AGENT_COMMENT_EMOJI_REGEX = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
|
|
75
85
|
|
|
86
|
+
type RunDigestSignal = {
|
|
87
|
+
sequence: number;
|
|
88
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
89
|
+
label: string | null;
|
|
90
|
+
text: string | null;
|
|
91
|
+
payload: string | null;
|
|
92
|
+
signalLevel: "high" | "medium" | "low" | "noise";
|
|
93
|
+
groupKey: string | null;
|
|
94
|
+
source: "stdout" | "stderr" | "trace_fallback";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type RunDigest = {
|
|
98
|
+
status: "completed" | "failed" | "skipped";
|
|
99
|
+
headline: string;
|
|
100
|
+
summary: string;
|
|
101
|
+
successes: string[];
|
|
102
|
+
failures: string[];
|
|
103
|
+
blockers: string[];
|
|
104
|
+
nextAction: string;
|
|
105
|
+
evidence: {
|
|
106
|
+
transcriptSignalCount: number;
|
|
107
|
+
outcomeActionCount: number;
|
|
108
|
+
outcomeBlockerCount: number;
|
|
109
|
+
failureType: string | null;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type RunTerminalPresentation = {
|
|
114
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
115
|
+
publicStatus: "completed" | "failed";
|
|
116
|
+
completionReason: RunCompletionReason;
|
|
117
|
+
};
|
|
118
|
+
|
|
76
119
|
export async function claimIssuesForAgent(
|
|
77
120
|
db: BopoDb,
|
|
78
121
|
companyId: string,
|
|
@@ -334,18 +377,81 @@ export async function runHeartbeatForAgent(
|
|
|
334
377
|
if (blockedProjectBudgetChecks.length > 0) {
|
|
335
378
|
const blockedProjectIds = blockedProjectBudgetChecks.map((entry) => entry.projectId);
|
|
336
379
|
const message = `Heartbeat skipped due to project budget hard-stop: ${blockedProjectIds.join(",")}.`;
|
|
380
|
+
const runDigest = buildRunDigest({
|
|
381
|
+
status: "skipped",
|
|
382
|
+
executionSummary: message,
|
|
383
|
+
outcome: null,
|
|
384
|
+
trace: null,
|
|
385
|
+
signals: []
|
|
386
|
+
});
|
|
387
|
+
const runReport = buildRunCompletionReport({
|
|
388
|
+
companyId,
|
|
389
|
+
agentName: agent.name,
|
|
390
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
391
|
+
issueIds: [],
|
|
392
|
+
executionSummary: message,
|
|
393
|
+
outcome: null,
|
|
394
|
+
trace: null,
|
|
395
|
+
digest: runDigest,
|
|
396
|
+
terminal: resolveRunTerminalPresentation({
|
|
397
|
+
internalStatus: "skipped",
|
|
398
|
+
executionSummary: message,
|
|
399
|
+
outcome: null,
|
|
400
|
+
trace: null
|
|
401
|
+
}),
|
|
402
|
+
cost: buildRunCostSummary({
|
|
403
|
+
tokenInput: 0,
|
|
404
|
+
tokenOutput: 0,
|
|
405
|
+
usdCost: null,
|
|
406
|
+
usdCostStatus: "unknown",
|
|
407
|
+
pricingSource: null,
|
|
408
|
+
source: "none"
|
|
409
|
+
})
|
|
410
|
+
});
|
|
411
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
337
412
|
await db.insert(heartbeatRuns).values({
|
|
338
413
|
id: runId,
|
|
339
414
|
companyId,
|
|
340
415
|
agentId,
|
|
341
416
|
status: "skipped",
|
|
342
|
-
|
|
417
|
+
finishedAt: new Date(),
|
|
418
|
+
message: runListMessage
|
|
343
419
|
});
|
|
344
420
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
345
421
|
companyId,
|
|
346
422
|
runId,
|
|
347
423
|
status: "skipped",
|
|
348
|
-
message
|
|
424
|
+
message: runListMessage,
|
|
425
|
+
finishedAt: new Date()
|
|
426
|
+
});
|
|
427
|
+
await appendAuditEvent(db, {
|
|
428
|
+
companyId,
|
|
429
|
+
actorType: "system",
|
|
430
|
+
eventType: "heartbeat.failed",
|
|
431
|
+
entityType: "heartbeat_run",
|
|
432
|
+
entityId: runId,
|
|
433
|
+
correlationId: options?.requestId ?? runId,
|
|
434
|
+
payload: {
|
|
435
|
+
agentId,
|
|
436
|
+
issueIds: [],
|
|
437
|
+
result: runReport.resultSummary,
|
|
438
|
+
message: runListMessage,
|
|
439
|
+
errorType: runReport.completionReason,
|
|
440
|
+
errorMessage: message,
|
|
441
|
+
report: runReport,
|
|
442
|
+
outcome: null,
|
|
443
|
+
usage: {
|
|
444
|
+
tokenInput: 0,
|
|
445
|
+
tokenOutput: 0,
|
|
446
|
+
usdCostStatus: "unknown",
|
|
447
|
+
source: "none"
|
|
448
|
+
},
|
|
449
|
+
trace: null,
|
|
450
|
+
diagnostics: {
|
|
451
|
+
requestId: options?.requestId,
|
|
452
|
+
trigger: runTrigger
|
|
453
|
+
}
|
|
454
|
+
}
|
|
349
455
|
});
|
|
350
456
|
for (const blockedProject of blockedProjectBudgetChecks) {
|
|
351
457
|
const approvalId = await ensureProjectBudgetOverrideApprovalRequest(db, {
|
|
@@ -386,45 +492,156 @@ export async function runHeartbeatForAgent(
|
|
|
386
492
|
if (!claimed) {
|
|
387
493
|
const skippedRunId = nanoid(14);
|
|
388
494
|
const skippedAt = new Date();
|
|
495
|
+
const overlapMessage = "Heartbeat skipped: another run is already in progress for this agent.";
|
|
496
|
+
const runDigest = buildRunDigest({
|
|
497
|
+
status: "skipped",
|
|
498
|
+
executionSummary: overlapMessage,
|
|
499
|
+
outcome: null,
|
|
500
|
+
trace: null,
|
|
501
|
+
signals: []
|
|
502
|
+
});
|
|
503
|
+
const runReport = buildRunCompletionReport({
|
|
504
|
+
companyId,
|
|
505
|
+
agentName: agent.name,
|
|
506
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
507
|
+
issueIds: [],
|
|
508
|
+
executionSummary: overlapMessage,
|
|
509
|
+
outcome: null,
|
|
510
|
+
trace: null,
|
|
511
|
+
digest: runDigest,
|
|
512
|
+
terminal: resolveRunTerminalPresentation({
|
|
513
|
+
internalStatus: "skipped",
|
|
514
|
+
executionSummary: overlapMessage,
|
|
515
|
+
outcome: null,
|
|
516
|
+
trace: null
|
|
517
|
+
}),
|
|
518
|
+
cost: buildRunCostSummary({
|
|
519
|
+
tokenInput: 0,
|
|
520
|
+
tokenOutput: 0,
|
|
521
|
+
usdCost: null,
|
|
522
|
+
usdCostStatus: "unknown",
|
|
523
|
+
pricingSource: null,
|
|
524
|
+
source: "none"
|
|
525
|
+
})
|
|
526
|
+
});
|
|
527
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
389
528
|
await db.insert(heartbeatRuns).values({
|
|
390
529
|
id: skippedRunId,
|
|
391
530
|
companyId,
|
|
392
531
|
agentId,
|
|
393
532
|
status: "skipped",
|
|
394
533
|
finishedAt: skippedAt,
|
|
395
|
-
message:
|
|
534
|
+
message: runListMessage
|
|
396
535
|
});
|
|
397
536
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
398
537
|
companyId,
|
|
399
538
|
runId: skippedRunId,
|
|
400
539
|
status: "skipped",
|
|
401
|
-
message:
|
|
540
|
+
message: runListMessage,
|
|
402
541
|
finishedAt: skippedAt
|
|
403
542
|
});
|
|
404
543
|
await appendAuditEvent(db, {
|
|
405
544
|
companyId,
|
|
406
545
|
actorType: "system",
|
|
407
|
-
eventType: "heartbeat.
|
|
546
|
+
eventType: "heartbeat.failed",
|
|
408
547
|
entityType: "heartbeat_run",
|
|
409
548
|
entityId: skippedRunId,
|
|
410
549
|
correlationId: options?.requestId ?? skippedRunId,
|
|
411
|
-
payload: {
|
|
550
|
+
payload: {
|
|
551
|
+
agentId,
|
|
552
|
+
issueIds: [],
|
|
553
|
+
result: runReport.resultSummary,
|
|
554
|
+
message: runListMessage,
|
|
555
|
+
errorType: runReport.completionReason,
|
|
556
|
+
errorMessage: overlapMessage,
|
|
557
|
+
report: runReport,
|
|
558
|
+
outcome: null,
|
|
559
|
+
usage: {
|
|
560
|
+
tokenInput: 0,
|
|
561
|
+
tokenOutput: 0,
|
|
562
|
+
usdCostStatus: "unknown",
|
|
563
|
+
source: "none"
|
|
564
|
+
},
|
|
565
|
+
trace: null,
|
|
566
|
+
diagnostics: { requestId: options?.requestId, trigger: runTrigger }
|
|
567
|
+
}
|
|
412
568
|
});
|
|
413
569
|
return skippedRunId;
|
|
414
570
|
}
|
|
415
571
|
} else {
|
|
572
|
+
const budgetMessage = "Heartbeat skipped due to budget hard-stop.";
|
|
573
|
+
const runDigest = buildRunDigest({
|
|
574
|
+
status: "skipped",
|
|
575
|
+
executionSummary: budgetMessage,
|
|
576
|
+
outcome: null,
|
|
577
|
+
trace: null,
|
|
578
|
+
signals: []
|
|
579
|
+
});
|
|
580
|
+
const runReport = buildRunCompletionReport({
|
|
581
|
+
companyId,
|
|
582
|
+
agentName: agent.name,
|
|
583
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
584
|
+
issueIds: [],
|
|
585
|
+
executionSummary: budgetMessage,
|
|
586
|
+
outcome: null,
|
|
587
|
+
trace: null,
|
|
588
|
+
digest: runDigest,
|
|
589
|
+
terminal: resolveRunTerminalPresentation({
|
|
590
|
+
internalStatus: "skipped",
|
|
591
|
+
executionSummary: budgetMessage,
|
|
592
|
+
outcome: null,
|
|
593
|
+
trace: null
|
|
594
|
+
}),
|
|
595
|
+
cost: buildRunCostSummary({
|
|
596
|
+
tokenInput: 0,
|
|
597
|
+
tokenOutput: 0,
|
|
598
|
+
usdCost: null,
|
|
599
|
+
usdCostStatus: "unknown",
|
|
600
|
+
pricingSource: null,
|
|
601
|
+
source: "none"
|
|
602
|
+
})
|
|
603
|
+
});
|
|
604
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
416
605
|
await db.insert(heartbeatRuns).values({
|
|
417
606
|
id: runId,
|
|
418
607
|
companyId,
|
|
419
608
|
agentId,
|
|
420
609
|
status: "skipped",
|
|
421
|
-
|
|
610
|
+
finishedAt: new Date(),
|
|
611
|
+
message: runListMessage
|
|
422
612
|
});
|
|
423
613
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
424
614
|
companyId,
|
|
425
615
|
runId,
|
|
426
616
|
status: "skipped",
|
|
427
|
-
message:
|
|
617
|
+
message: runListMessage,
|
|
618
|
+
finishedAt: new Date()
|
|
619
|
+
});
|
|
620
|
+
await appendAuditEvent(db, {
|
|
621
|
+
companyId,
|
|
622
|
+
actorType: "system",
|
|
623
|
+
eventType: "heartbeat.failed",
|
|
624
|
+
entityType: "heartbeat_run",
|
|
625
|
+
entityId: runId,
|
|
626
|
+
correlationId: options?.requestId ?? runId,
|
|
627
|
+
payload: {
|
|
628
|
+
agentId,
|
|
629
|
+
issueIds: [],
|
|
630
|
+
result: runReport.resultSummary,
|
|
631
|
+
message: runListMessage,
|
|
632
|
+
errorType: runReport.completionReason,
|
|
633
|
+
errorMessage: budgetMessage,
|
|
634
|
+
report: runReport,
|
|
635
|
+
outcome: null,
|
|
636
|
+
usage: {
|
|
637
|
+
tokenInput: 0,
|
|
638
|
+
tokenOutput: 0,
|
|
639
|
+
usdCostStatus: "unknown",
|
|
640
|
+
source: "none"
|
|
641
|
+
},
|
|
642
|
+
trace: null,
|
|
643
|
+
diagnostics: { requestId: options?.requestId, trigger: runTrigger }
|
|
644
|
+
}
|
|
428
645
|
});
|
|
429
646
|
}
|
|
430
647
|
|
|
@@ -518,6 +735,13 @@ export async function runHeartbeatForAgent(
|
|
|
518
735
|
let runtimeLaunchSummary: ReturnType<typeof summarizeRuntimeLaunch> | null = null;
|
|
519
736
|
let primaryIssueId: string | null = null;
|
|
520
737
|
let primaryProjectId: string | null = null;
|
|
738
|
+
let providerUsageLimitDisposition:
|
|
739
|
+
| {
|
|
740
|
+
message: string;
|
|
741
|
+
notifyBoard: boolean;
|
|
742
|
+
pauseAgent: boolean;
|
|
743
|
+
}
|
|
744
|
+
| null = null;
|
|
521
745
|
let transcriptSequence = 0;
|
|
522
746
|
let transcriptWriteQueue = Promise.resolve();
|
|
523
747
|
let transcriptLiveCount = 0;
|
|
@@ -526,6 +750,7 @@ export async function runHeartbeatForAgent(
|
|
|
526
750
|
let transcriptPersistFailureReported = false;
|
|
527
751
|
let pluginFailureSummary: string[] = [];
|
|
528
752
|
const seenResultMessages = new Set<string>();
|
|
753
|
+
const runDigestSignals: RunDigestSignal[] = [];
|
|
529
754
|
|
|
530
755
|
const enqueueTranscriptEvent = (event: {
|
|
531
756
|
kind: string;
|
|
@@ -553,6 +778,21 @@ export async function runHeartbeatForAgent(
|
|
|
553
778
|
if (signalLevel === "high") {
|
|
554
779
|
transcriptLiveHighSignalCount += 1;
|
|
555
780
|
}
|
|
781
|
+
if (isUsefulTranscriptSignal(signalLevel)) {
|
|
782
|
+
runDigestSignals.push({
|
|
783
|
+
sequence,
|
|
784
|
+
kind: normalizeTranscriptKind(event.kind),
|
|
785
|
+
label: event.label ?? null,
|
|
786
|
+
text: event.text ?? null,
|
|
787
|
+
payload: event.payload ?? null,
|
|
788
|
+
signalLevel,
|
|
789
|
+
groupKey: groupKey ?? null,
|
|
790
|
+
source
|
|
791
|
+
});
|
|
792
|
+
if (runDigestSignals.length > 200) {
|
|
793
|
+
runDigestSignals.splice(0, runDigestSignals.length - 200);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
556
796
|
transcriptWriteQueue = transcriptWriteQueue
|
|
557
797
|
.then(async () => {
|
|
558
798
|
await appendHeartbeatRunMessages(db, {
|
|
@@ -943,7 +1183,20 @@ export async function runHeartbeatForAgent(
|
|
|
943
1183
|
runtime: workspaceResolution.runtime,
|
|
944
1184
|
externalAbortSignal: activeRunAbort.signal
|
|
945
1185
|
});
|
|
946
|
-
|
|
1186
|
+
const usageLimitHint = execution.dispositionHint?.kind === "provider_usage_limited" ? execution.dispositionHint : null;
|
|
1187
|
+
if (usageLimitHint) {
|
|
1188
|
+
providerUsageLimitDisposition = {
|
|
1189
|
+
message: usageLimitHint.message,
|
|
1190
|
+
notifyBoard: usageLimitHint.notifyBoard,
|
|
1191
|
+
pauseAgent: usageLimitHint.pauseAgent
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
executionSummary =
|
|
1195
|
+
usageLimitHint?.message && usageLimitHint.message.trim().length > 0 ? usageLimitHint.message.trim() : execution.summary;
|
|
1196
|
+
executionSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(executionSummary));
|
|
1197
|
+
const persistedExecutionStatus: "ok" | "failed" | "skipped" = usageLimitHint ? "skipped" : execution.status;
|
|
1198
|
+
const persistedRunStatus: "completed" | "failed" | "skipped" =
|
|
1199
|
+
persistedExecutionStatus === "ok" ? "completed" : persistedExecutionStatus;
|
|
947
1200
|
const normalizedUsage = execution.usage ?? {
|
|
948
1201
|
inputTokens: Math.max(0, execution.tokenInput),
|
|
949
1202
|
cachedInputTokens: 0,
|
|
@@ -972,7 +1225,6 @@ export async function runHeartbeatForAgent(
|
|
|
972
1225
|
if (afterAdapterHook.failures.length > 0) {
|
|
973
1226
|
pluginFailureSummary = [...pluginFailureSummary, ...afterAdapterHook.failures];
|
|
974
1227
|
}
|
|
975
|
-
emitCanonicalResultEvent(executionSummary, "completed");
|
|
976
1228
|
executionTrace = execution.trace ?? null;
|
|
977
1229
|
const runtimeModelId = resolveRuntimeModelId({
|
|
978
1230
|
runtimeModel: persistedRuntime.runtimeModel,
|
|
@@ -983,6 +1235,7 @@ export async function runHeartbeatForAgent(
|
|
|
983
1235
|
const costDecision = await appendFinishedRunCostEntry({
|
|
984
1236
|
db,
|
|
985
1237
|
companyId,
|
|
1238
|
+
runId,
|
|
986
1239
|
providerType: agent.providerType,
|
|
987
1240
|
runtimeModelId: effectivePricingModelId ?? runtimeModelId,
|
|
988
1241
|
pricingProviderType: effectivePricingProviderType,
|
|
@@ -994,7 +1247,7 @@ export async function runHeartbeatForAgent(
|
|
|
994
1247
|
issueId: primaryIssueId,
|
|
995
1248
|
projectId: primaryProjectId,
|
|
996
1249
|
agentId,
|
|
997
|
-
status:
|
|
1250
|
+
status: persistedExecutionStatus
|
|
998
1251
|
});
|
|
999
1252
|
const executionUsdCost = costDecision.usdCost;
|
|
1000
1253
|
await appendProjectBudgetUsage(db, {
|
|
@@ -1007,8 +1260,8 @@ export async function runHeartbeatForAgent(
|
|
|
1007
1260
|
companyId,
|
|
1008
1261
|
agentId,
|
|
1009
1262
|
runId,
|
|
1010
|
-
status:
|
|
1011
|
-
summary:
|
|
1263
|
+
status: persistedExecutionStatus === "ok" ? "ok" : "failed",
|
|
1264
|
+
summary: executionSummary,
|
|
1012
1265
|
outcomeKind: executionOutcome?.kind ?? null,
|
|
1013
1266
|
mission: context.company.mission ?? null,
|
|
1014
1267
|
goalContext: {
|
|
@@ -1030,7 +1283,7 @@ export async function runHeartbeatForAgent(
|
|
|
1030
1283
|
candidateFacts: persistedMemory.candidateFacts
|
|
1031
1284
|
}
|
|
1032
1285
|
});
|
|
1033
|
-
if (execution.status === "ok") {
|
|
1286
|
+
if (execution.status === "ok" && !usageLimitHint) {
|
|
1034
1287
|
for (const fact of persistedMemory.candidateFacts) {
|
|
1035
1288
|
const targetFile = await appendDurableFact({
|
|
1036
1289
|
companyId,
|
|
@@ -1054,7 +1307,7 @@ export async function runHeartbeatForAgent(
|
|
|
1054
1307
|
}
|
|
1055
1308
|
}
|
|
1056
1309
|
const missionAlignment = computeMissionAlignmentSignal({
|
|
1057
|
-
summary:
|
|
1310
|
+
summary: executionSummary,
|
|
1058
1311
|
mission: context.company.mission ?? null,
|
|
1059
1312
|
companyGoals: context.goalContext?.companyGoals ?? [],
|
|
1060
1313
|
projectGoals: context.goalContext?.projectGoals ?? []
|
|
@@ -1079,7 +1332,7 @@ export async function runHeartbeatForAgent(
|
|
|
1079
1332
|
executionUsdCost > 0 ||
|
|
1080
1333
|
effectiveTokenInput > 0 ||
|
|
1081
1334
|
effectiveTokenOutput > 0 ||
|
|
1082
|
-
|
|
1335
|
+
persistedExecutionStatus !== "skipped"
|
|
1083
1336
|
) {
|
|
1084
1337
|
await db
|
|
1085
1338
|
.update(agents)
|
|
@@ -1157,8 +1410,8 @@ export async function runHeartbeatForAgent(
|
|
|
1157
1410
|
runId,
|
|
1158
1411
|
requestId: options?.requestId,
|
|
1159
1412
|
providerType: agent.providerType,
|
|
1160
|
-
status:
|
|
1161
|
-
summary:
|
|
1413
|
+
status: persistedExecutionStatus,
|
|
1414
|
+
summary: executionSummary
|
|
1162
1415
|
},
|
|
1163
1416
|
failClosed: false
|
|
1164
1417
|
});
|
|
@@ -1166,29 +1419,74 @@ export async function runHeartbeatForAgent(
|
|
|
1166
1419
|
pluginFailureSummary = [...pluginFailureSummary, ...beforePersistHook.failures];
|
|
1167
1420
|
}
|
|
1168
1421
|
|
|
1422
|
+
const runDigest = buildRunDigest({
|
|
1423
|
+
status: persistedRunStatus,
|
|
1424
|
+
executionSummary,
|
|
1425
|
+
outcome: executionOutcome,
|
|
1426
|
+
trace: executionTrace,
|
|
1427
|
+
signals: runDigestSignals
|
|
1428
|
+
});
|
|
1429
|
+
const terminalPresentation = resolveRunTerminalPresentation({
|
|
1430
|
+
internalStatus: persistedRunStatus,
|
|
1431
|
+
executionSummary,
|
|
1432
|
+
outcome: executionOutcome,
|
|
1433
|
+
trace: executionTrace
|
|
1434
|
+
});
|
|
1435
|
+
const runCost = buildRunCostSummary({
|
|
1436
|
+
tokenInput: effectiveTokenInput,
|
|
1437
|
+
tokenOutput: effectiveTokenOutput,
|
|
1438
|
+
usdCost: costDecision.usdCostStatus === "unknown" ? null : executionUsdCost,
|
|
1439
|
+
usdCostStatus: costDecision.usdCostStatus,
|
|
1440
|
+
pricingSource: costDecision.pricingSource ?? null,
|
|
1441
|
+
source: readTraceString(execution.trace, "usageSource") ?? "unknown"
|
|
1442
|
+
});
|
|
1443
|
+
const runReport = buildRunCompletionReport({
|
|
1444
|
+
companyId,
|
|
1445
|
+
agentName: agent.name,
|
|
1446
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
1447
|
+
issueIds,
|
|
1448
|
+
executionSummary,
|
|
1449
|
+
outcome: executionOutcome,
|
|
1450
|
+
finalRunOutput: execution.finalRunOutput ?? null,
|
|
1451
|
+
trace: executionTrace,
|
|
1452
|
+
digest: runDigest,
|
|
1453
|
+
terminal: terminalPresentation,
|
|
1454
|
+
cost: runCost,
|
|
1455
|
+
runtimeCwd: workspaceResolution.runtime.cwd
|
|
1456
|
+
});
|
|
1457
|
+
emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
|
|
1458
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
1169
1459
|
await db
|
|
1170
1460
|
.update(heartbeatRuns)
|
|
1171
1461
|
.set({
|
|
1172
|
-
status:
|
|
1462
|
+
status: persistedRunStatus,
|
|
1173
1463
|
finishedAt: new Date(),
|
|
1174
|
-
message:
|
|
1464
|
+
message: runListMessage
|
|
1175
1465
|
})
|
|
1176
1466
|
.where(eq(heartbeatRuns.id, runId));
|
|
1177
1467
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
1178
1468
|
companyId,
|
|
1179
1469
|
runId,
|
|
1180
|
-
status:
|
|
1181
|
-
message:
|
|
1470
|
+
status: persistedRunStatus,
|
|
1471
|
+
message: runListMessage,
|
|
1182
1472
|
finishedAt: new Date()
|
|
1183
1473
|
});
|
|
1474
|
+
await appendAuditEvent(db, {
|
|
1475
|
+
companyId,
|
|
1476
|
+
actorType: "system",
|
|
1477
|
+
eventType: "heartbeat.run_digest",
|
|
1478
|
+
entityType: "heartbeat_run",
|
|
1479
|
+
entityId: runId,
|
|
1480
|
+
correlationId: options?.requestId ?? runId,
|
|
1481
|
+
payload: runDigest
|
|
1482
|
+
});
|
|
1184
1483
|
try {
|
|
1185
1484
|
await appendRunSummaryComments(db, {
|
|
1186
1485
|
companyId,
|
|
1187
1486
|
issueIds,
|
|
1188
1487
|
agentId,
|
|
1189
1488
|
runId,
|
|
1190
|
-
|
|
1191
|
-
executionSummary: execution.summary
|
|
1489
|
+
report: runReport
|
|
1192
1490
|
});
|
|
1193
1491
|
} catch (commentError) {
|
|
1194
1492
|
await appendAuditEvent(db, {
|
|
@@ -1209,6 +1507,7 @@ export async function runHeartbeatForAgent(
|
|
|
1209
1507
|
const fallbackMessages = normalizeTraceTranscript(executionTrace);
|
|
1210
1508
|
const fallbackHighSignalCount = fallbackMessages.filter((message) => message.signalLevel === "high").length;
|
|
1211
1509
|
const shouldAppendFallback =
|
|
1510
|
+
!providerUsageLimitDisposition &&
|
|
1212
1511
|
fallbackMessages.length > 0 &&
|
|
1213
1512
|
(transcriptLiveCount === 0 ||
|
|
1214
1513
|
transcriptLiveUsefulCount < 2 ||
|
|
@@ -1253,6 +1552,24 @@ export async function runHeartbeatForAgent(
|
|
|
1253
1552
|
source: "trace_fallback",
|
|
1254
1553
|
createdAt
|
|
1255
1554
|
}));
|
|
1555
|
+
for (const row of rows) {
|
|
1556
|
+
if (!isUsefulTranscriptSignal(row.signalLevel)) {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
runDigestSignals.push({
|
|
1560
|
+
sequence: row.sequence,
|
|
1561
|
+
kind: row.kind,
|
|
1562
|
+
label: row.label,
|
|
1563
|
+
text: row.text,
|
|
1564
|
+
payload: row.payloadJson,
|
|
1565
|
+
signalLevel: row.signalLevel,
|
|
1566
|
+
groupKey: row.groupKey,
|
|
1567
|
+
source: "trace_fallback"
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (runDigestSignals.length > 200) {
|
|
1571
|
+
runDigestSignals.splice(0, runDigestSignals.length - 200);
|
|
1572
|
+
}
|
|
1256
1573
|
await appendHeartbeatRunMessages(db, {
|
|
1257
1574
|
companyId,
|
|
1258
1575
|
runId,
|
|
@@ -1287,8 +1604,8 @@ export async function runHeartbeatForAgent(
|
|
|
1287
1604
|
runId,
|
|
1288
1605
|
requestId: options?.requestId,
|
|
1289
1606
|
providerType: agent.providerType,
|
|
1290
|
-
status:
|
|
1291
|
-
summary:
|
|
1607
|
+
status: persistedExecutionStatus,
|
|
1608
|
+
summary: executionSummary,
|
|
1292
1609
|
trace: executionTrace,
|
|
1293
1610
|
outcome: executionOutcome
|
|
1294
1611
|
},
|
|
@@ -1298,6 +1615,48 @@ export async function runHeartbeatForAgent(
|
|
|
1298
1615
|
pluginFailureSummary = [...pluginFailureSummary, ...afterPersistHook.failures];
|
|
1299
1616
|
}
|
|
1300
1617
|
|
|
1618
|
+
if (providerUsageLimitDisposition) {
|
|
1619
|
+
await appendAuditEvent(db, {
|
|
1620
|
+
companyId,
|
|
1621
|
+
actorType: "system",
|
|
1622
|
+
eventType: "heartbeat.provider_usage_limited",
|
|
1623
|
+
entityType: "heartbeat_run",
|
|
1624
|
+
entityId: runId,
|
|
1625
|
+
correlationId: options?.requestId ?? runId,
|
|
1626
|
+
payload: {
|
|
1627
|
+
agentId,
|
|
1628
|
+
providerType: agent.providerType,
|
|
1629
|
+
issueIds,
|
|
1630
|
+
message: providerUsageLimitDisposition.message
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
const pauseResult = providerUsageLimitDisposition.pauseAgent
|
|
1634
|
+
? await pauseAgentForProviderUsageLimit(db, {
|
|
1635
|
+
companyId,
|
|
1636
|
+
agentId,
|
|
1637
|
+
requestId: options?.requestId ?? runId,
|
|
1638
|
+
runId,
|
|
1639
|
+
providerType: agent.providerType,
|
|
1640
|
+
message: providerUsageLimitDisposition.message
|
|
1641
|
+
})
|
|
1642
|
+
: { paused: false };
|
|
1643
|
+
if (providerUsageLimitDisposition.notifyBoard) {
|
|
1644
|
+
await appendProviderUsageLimitBoardComments(db, {
|
|
1645
|
+
companyId,
|
|
1646
|
+
issueIds,
|
|
1647
|
+
agentId,
|
|
1648
|
+
runId,
|
|
1649
|
+
providerType: agent.providerType,
|
|
1650
|
+
message: providerUsageLimitDisposition.message,
|
|
1651
|
+
paused: pauseResult.paused
|
|
1652
|
+
});
|
|
1653
|
+
if (options?.realtimeHub) {
|
|
1654
|
+
await publishAttentionSnapshot(db, options.realtimeHub, companyId);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
await publishOfficeOccupantForAgent(db, options?.realtimeHub, companyId, agentId);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1301
1660
|
await appendAuditEvent(db, {
|
|
1302
1661
|
companyId,
|
|
1303
1662
|
actorType: "system",
|
|
@@ -1307,14 +1666,17 @@ export async function runHeartbeatForAgent(
|
|
|
1307
1666
|
correlationId: options?.requestId ?? runId,
|
|
1308
1667
|
payload: {
|
|
1309
1668
|
agentId,
|
|
1310
|
-
|
|
1311
|
-
|
|
1669
|
+
status: persistedRunStatus,
|
|
1670
|
+
result: runReport.resultSummary,
|
|
1671
|
+
message: runListMessage,
|
|
1672
|
+
report: runReport,
|
|
1312
1673
|
outcome: executionOutcome,
|
|
1313
1674
|
issueIds,
|
|
1314
1675
|
usage: {
|
|
1315
1676
|
tokenInput: effectiveTokenInput,
|
|
1316
1677
|
tokenOutput: effectiveTokenOutput,
|
|
1317
1678
|
usdCost: executionUsdCost,
|
|
1679
|
+
usdCostStatus: costDecision.usdCostStatus,
|
|
1318
1680
|
source: readTraceString(execution.trace, "usageSource") ?? "unknown"
|
|
1319
1681
|
},
|
|
1320
1682
|
trace: execution.trace ?? null,
|
|
@@ -1408,6 +1770,7 @@ export async function runHeartbeatForAgent(
|
|
|
1408
1770
|
const failureCostDecision = await appendFinishedRunCostEntry({
|
|
1409
1771
|
db,
|
|
1410
1772
|
companyId,
|
|
1773
|
+
runId,
|
|
1411
1774
|
providerType: agent.providerType,
|
|
1412
1775
|
runtimeModelId,
|
|
1413
1776
|
pricingProviderType: agent.providerType,
|
|
@@ -1423,29 +1786,76 @@ export async function runHeartbeatForAgent(
|
|
|
1423
1786
|
companyId,
|
|
1424
1787
|
projectCostsUsd: buildProjectBudgetCostAllocations(executionWorkItemsForBudget, failureCostDecision.usdCost)
|
|
1425
1788
|
});
|
|
1789
|
+
const runDigest = buildRunDigest({
|
|
1790
|
+
status: "failed",
|
|
1791
|
+
executionSummary,
|
|
1792
|
+
outcome: executionOutcome,
|
|
1793
|
+
trace: executionTrace,
|
|
1794
|
+
signals: runDigestSignals
|
|
1795
|
+
});
|
|
1796
|
+
const runCost = buildRunCostSummary({
|
|
1797
|
+
tokenInput: 0,
|
|
1798
|
+
tokenOutput: 0,
|
|
1799
|
+
usdCost: failureCostDecision.usdCostStatus === "unknown" ? null : failureCostDecision.usdCost,
|
|
1800
|
+
usdCostStatus: failureCostDecision.usdCostStatus,
|
|
1801
|
+
pricingSource: failureCostDecision.pricingSource ?? null,
|
|
1802
|
+
source: readTraceString(executionTrace, "usageSource") ?? "unknown"
|
|
1803
|
+
});
|
|
1804
|
+
const runReport = buildRunCompletionReport({
|
|
1805
|
+
companyId,
|
|
1806
|
+
agentName: agent.name,
|
|
1807
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
1808
|
+
issueIds,
|
|
1809
|
+
executionSummary,
|
|
1810
|
+
outcome: executionOutcome,
|
|
1811
|
+
finalRunOutput: null,
|
|
1812
|
+
trace: executionTrace,
|
|
1813
|
+
digest: runDigest,
|
|
1814
|
+
terminal: resolveRunTerminalPresentation({
|
|
1815
|
+
internalStatus: "failed",
|
|
1816
|
+
executionSummary,
|
|
1817
|
+
outcome: executionOutcome,
|
|
1818
|
+
trace: executionTrace,
|
|
1819
|
+
errorType: classified.type
|
|
1820
|
+
}),
|
|
1821
|
+
cost: runCost,
|
|
1822
|
+
runtimeCwd: runtimeLaunchSummary?.cwd ?? persistedRuntime.runtimeCwd ?? null,
|
|
1823
|
+
errorType: classified.type,
|
|
1824
|
+
errorMessage: classified.message
|
|
1825
|
+
});
|
|
1826
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
1426
1827
|
await db
|
|
1427
1828
|
.update(heartbeatRuns)
|
|
1428
1829
|
.set({
|
|
1429
1830
|
status: "failed",
|
|
1430
1831
|
finishedAt: new Date(),
|
|
1431
|
-
message:
|
|
1832
|
+
message: runListMessage
|
|
1432
1833
|
})
|
|
1433
1834
|
.where(eq(heartbeatRuns.id, runId));
|
|
1434
1835
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
1435
1836
|
companyId,
|
|
1436
1837
|
runId,
|
|
1437
1838
|
status: "failed",
|
|
1438
|
-
message:
|
|
1839
|
+
message: runListMessage,
|
|
1439
1840
|
finishedAt: new Date()
|
|
1440
1841
|
});
|
|
1842
|
+
emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
|
|
1843
|
+
await appendAuditEvent(db, {
|
|
1844
|
+
companyId,
|
|
1845
|
+
actorType: "system",
|
|
1846
|
+
eventType: "heartbeat.run_digest",
|
|
1847
|
+
entityType: "heartbeat_run",
|
|
1848
|
+
entityId: runId,
|
|
1849
|
+
correlationId: options?.requestId ?? runId,
|
|
1850
|
+
payload: runDigest
|
|
1851
|
+
});
|
|
1441
1852
|
try {
|
|
1442
1853
|
await appendRunSummaryComments(db, {
|
|
1443
1854
|
companyId,
|
|
1444
1855
|
issueIds,
|
|
1445
1856
|
agentId,
|
|
1446
1857
|
runId,
|
|
1447
|
-
|
|
1448
|
-
executionSummary
|
|
1858
|
+
report: runReport
|
|
1449
1859
|
});
|
|
1450
1860
|
} catch (commentError) {
|
|
1451
1861
|
await appendAuditEvent(db, {
|
|
@@ -1472,12 +1882,17 @@ export async function runHeartbeatForAgent(
|
|
|
1472
1882
|
payload: {
|
|
1473
1883
|
agentId,
|
|
1474
1884
|
issueIds,
|
|
1475
|
-
result:
|
|
1476
|
-
message:
|
|
1885
|
+
result: runReport.resultSummary,
|
|
1886
|
+
message: runListMessage,
|
|
1477
1887
|
errorType: classified.type,
|
|
1478
1888
|
errorMessage: classified.message,
|
|
1889
|
+
report: runReport,
|
|
1479
1890
|
outcome: executionOutcome,
|
|
1480
1891
|
usage: {
|
|
1892
|
+
tokenInput: 0,
|
|
1893
|
+
tokenOutput: 0,
|
|
1894
|
+
usdCost: failureCostDecision.usdCost,
|
|
1895
|
+
usdCostStatus: failureCostDecision.usdCostStatus,
|
|
1481
1896
|
source: readTraceString(executionTrace, "usageSource") ?? "unknown"
|
|
1482
1897
|
},
|
|
1483
1898
|
trace: executionTrace,
|
|
@@ -2290,16 +2705,6 @@ function sanitizeAgentSummaryCommentBody(body: string) {
|
|
|
2290
2705
|
return sanitized.length > 0 ? sanitized : "Run update.";
|
|
2291
2706
|
}
|
|
2292
2707
|
|
|
2293
|
-
function buildRunSummaryCommentBody(input: { status: "completed" | "failed"; executionSummary: string }) {
|
|
2294
|
-
const summary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
|
|
2295
|
-
if (input.status === "failed") {
|
|
2296
|
-
return summary.toLowerCase().startsWith("couldn't")
|
|
2297
|
-
? summary
|
|
2298
|
-
: `Couldn't complete this run: ${summary.charAt(0).toLowerCase()}${summary.slice(1)}`;
|
|
2299
|
-
}
|
|
2300
|
-
return summary;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
2708
|
function extractNaturalRunUpdate(executionSummary: string) {
|
|
2304
2709
|
const normalized = executionSummary.trim();
|
|
2305
2710
|
const jsonSummary = extractSummaryFromJsonLikeText(normalized);
|
|
@@ -2323,6 +2728,676 @@ function extractNaturalRunUpdate(executionSummary: string) {
|
|
|
2323
2728
|
return /[.!?]$/.test(bounded) ? bounded : `${bounded}.`;
|
|
2324
2729
|
}
|
|
2325
2730
|
|
|
2731
|
+
function buildRunDigest(input: {
|
|
2732
|
+
status: "completed" | "failed" | "skipped";
|
|
2733
|
+
executionSummary: string;
|
|
2734
|
+
outcome: ExecutionOutcome | null;
|
|
2735
|
+
trace: unknown;
|
|
2736
|
+
signals: RunDigestSignal[];
|
|
2737
|
+
}): RunDigest {
|
|
2738
|
+
const summary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
|
|
2739
|
+
const successes: string[] = [];
|
|
2740
|
+
const failures: string[] = [];
|
|
2741
|
+
const blockers: string[] = [];
|
|
2742
|
+
if (input.outcome) {
|
|
2743
|
+
for (const action of input.outcome.actions) {
|
|
2744
|
+
const detail = summarizeRunDigestPoint(action.detail);
|
|
2745
|
+
if (!detail) {
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
if (action.status === "ok") {
|
|
2749
|
+
successes.push(detail);
|
|
2750
|
+
} else if (action.status === "error") {
|
|
2751
|
+
failures.push(detail);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
for (const blocker of input.outcome.blockers) {
|
|
2755
|
+
const detail = summarizeRunDigestPoint(blocker.message);
|
|
2756
|
+
if (detail) {
|
|
2757
|
+
blockers.push(detail);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
for (const signal of input.signals) {
|
|
2762
|
+
if (signal.signalLevel !== "high" && signal.signalLevel !== "medium") {
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
const signalText = summarizeRunDigestPoint(signal.text ?? signal.payload ?? "");
|
|
2766
|
+
if (!signalText) {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (signal.kind === "tool_result" || signal.kind === "stderr") {
|
|
2770
|
+
if (looksLikeRunFailureSignal(signalText)) {
|
|
2771
|
+
failures.push(signalText);
|
|
2772
|
+
} else if (signal.kind === "tool_result") {
|
|
2773
|
+
successes.push(signalText);
|
|
2774
|
+
}
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
if (signal.kind === "result" && !looksLikeRunFailureSignal(signalText)) {
|
|
2778
|
+
successes.push(signalText);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (input.status === "completed" && successes.length === 0) {
|
|
2782
|
+
successes.push(summary);
|
|
2783
|
+
}
|
|
2784
|
+
if (input.status === "failed" && failures.length === 0) {
|
|
2785
|
+
failures.push(summary);
|
|
2786
|
+
}
|
|
2787
|
+
if (input.status === "failed" && blockers.length === 0) {
|
|
2788
|
+
const traceFailureType = summarizeRunDigestPoint(readTraceString(input.trace, "failureType") ?? "");
|
|
2789
|
+
if (traceFailureType) {
|
|
2790
|
+
blockers.push(`failure type: ${traceFailureType}`);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const uniqueSuccesses = dedupeRunDigestPoints(successes, 3);
|
|
2794
|
+
const uniqueFailures = dedupeRunDigestPoints(failures, 3);
|
|
2795
|
+
const uniqueBlockers = dedupeRunDigestPoints(blockers, 2);
|
|
2796
|
+
const headline =
|
|
2797
|
+
input.status === "completed"
|
|
2798
|
+
? `Run completed: ${summary}`
|
|
2799
|
+
: input.status === "failed"
|
|
2800
|
+
? `Run failed: ${summary}`
|
|
2801
|
+
: `Run skipped: ${summary}`;
|
|
2802
|
+
const nextAction = resolveRunDigestNextAction({
|
|
2803
|
+
status: input.status,
|
|
2804
|
+
blockers: uniqueBlockers,
|
|
2805
|
+
failures: uniqueFailures
|
|
2806
|
+
});
|
|
2807
|
+
return {
|
|
2808
|
+
status: input.status,
|
|
2809
|
+
headline,
|
|
2810
|
+
summary,
|
|
2811
|
+
successes: uniqueSuccesses,
|
|
2812
|
+
failures: uniqueFailures,
|
|
2813
|
+
blockers: uniqueBlockers,
|
|
2814
|
+
nextAction,
|
|
2815
|
+
evidence: {
|
|
2816
|
+
transcriptSignalCount: input.signals.length,
|
|
2817
|
+
outcomeActionCount: input.outcome?.actions.length ?? 0,
|
|
2818
|
+
outcomeBlockerCount: input.outcome?.blockers.length ?? 0,
|
|
2819
|
+
failureType: readTraceString(input.trace, "failureType")
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function summarizeRunDigestPoint(value: string | null | undefined) {
|
|
2825
|
+
if (!value) {
|
|
2826
|
+
return "";
|
|
2827
|
+
}
|
|
2828
|
+
const normalized = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(value));
|
|
2829
|
+
if (!normalized || normalized.toLowerCase() === "run update.") {
|
|
2830
|
+
return "";
|
|
2831
|
+
}
|
|
2832
|
+
const bounded = normalized.length > 180 ? `${normalized.slice(0, 177).trimEnd()}...` : normalized;
|
|
2833
|
+
return bounded;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
function dedupeRunDigestPoints(values: string[], limit: number) {
|
|
2837
|
+
const seen = new Set<string>();
|
|
2838
|
+
const deduped: string[] = [];
|
|
2839
|
+
for (const value of values) {
|
|
2840
|
+
const key = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2841
|
+
if (!key || seen.has(key)) {
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
seen.add(key);
|
|
2845
|
+
deduped.push(value);
|
|
2846
|
+
if (deduped.length >= limit) {
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return deduped;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function looksLikeRunFailureSignal(value: string) {
|
|
2854
|
+
const normalized = value.toLowerCase();
|
|
2855
|
+
return /(failed|error|exception|timed out|timeout|unauthorized|not supported|unsupported|no capacity|rate limit|429|500|blocked|unable to)/.test(
|
|
2856
|
+
normalized
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
function resolveRunDigestNextAction(input: { status: "completed" | "failed" | "skipped"; blockers: string[]; failures: string[] }) {
|
|
2861
|
+
if (input.status === "completed") {
|
|
2862
|
+
return "Review outputs and move the issue to the next workflow state.";
|
|
2863
|
+
}
|
|
2864
|
+
const combined = [...input.blockers, ...input.failures].join(" ").toLowerCase();
|
|
2865
|
+
if (combined.includes("auth") || combined.includes("unauthorized") || combined.includes("login")) {
|
|
2866
|
+
return "Fix credentials/authentication, then rerun.";
|
|
2867
|
+
}
|
|
2868
|
+
if (combined.includes("model") && (combined.includes("not supported") || combined.includes("unavailable"))) {
|
|
2869
|
+
return "Select a supported model and rerun.";
|
|
2870
|
+
}
|
|
2871
|
+
if (combined.includes("usage limit") || combined.includes("rate limit") || combined.includes("no capacity")) {
|
|
2872
|
+
return "Retry after provider quota/capacity recovers.";
|
|
2873
|
+
}
|
|
2874
|
+
return "Fix listed failures/blockers and rerun.";
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
function resolveRunTerminalPresentation(input: {
|
|
2878
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
2879
|
+
executionSummary: string;
|
|
2880
|
+
outcome: ExecutionOutcome | null;
|
|
2881
|
+
trace: unknown;
|
|
2882
|
+
errorType?: string | null;
|
|
2883
|
+
}) : RunTerminalPresentation {
|
|
2884
|
+
if (isNoAssignedWorkOutcomeForReport(input.outcome)) {
|
|
2885
|
+
return {
|
|
2886
|
+
internalStatus: input.internalStatus,
|
|
2887
|
+
publicStatus: "completed",
|
|
2888
|
+
completionReason: "no_assigned_work"
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
if (input.internalStatus === "completed") {
|
|
2892
|
+
return {
|
|
2893
|
+
internalStatus: input.internalStatus,
|
|
2894
|
+
publicStatus: "completed",
|
|
2895
|
+
completionReason: "task_completed"
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
const completionReason = inferRunCompletionReason(input);
|
|
2899
|
+
return {
|
|
2900
|
+
internalStatus: input.internalStatus,
|
|
2901
|
+
publicStatus: "failed",
|
|
2902
|
+
completionReason
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
function inferRunCompletionReason(input: {
|
|
2907
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
2908
|
+
executionSummary: string;
|
|
2909
|
+
outcome: ExecutionOutcome | null;
|
|
2910
|
+
trace: unknown;
|
|
2911
|
+
errorType?: string | null;
|
|
2912
|
+
}): RunCompletionReason {
|
|
2913
|
+
const texts = [
|
|
2914
|
+
input.executionSummary,
|
|
2915
|
+
readTraceString(input.trace, "failureType") ?? "",
|
|
2916
|
+
readTraceString(input.trace, "stderrPreview") ?? "",
|
|
2917
|
+
input.errorType ?? "",
|
|
2918
|
+
...(input.outcome?.blockers ?? []).flatMap((blocker) => [blocker.code, blocker.message]),
|
|
2919
|
+
...(input.outcome?.actions ?? []).flatMap((action) => [action.type, action.detail ?? ""])
|
|
2920
|
+
];
|
|
2921
|
+
const combined = texts.join("\n").toLowerCase();
|
|
2922
|
+
if (
|
|
2923
|
+
combined.includes("insufficient_quota") ||
|
|
2924
|
+
combined.includes("billing_hard_limit_reached") ||
|
|
2925
|
+
combined.includes("out of funds") ||
|
|
2926
|
+
combined.includes("payment required")
|
|
2927
|
+
) {
|
|
2928
|
+
return "provider_out_of_funds";
|
|
2929
|
+
}
|
|
2930
|
+
if (
|
|
2931
|
+
combined.includes("usage limit") ||
|
|
2932
|
+
combined.includes("rate limit") ||
|
|
2933
|
+
combined.includes("429") ||
|
|
2934
|
+
combined.includes("quota")
|
|
2935
|
+
) {
|
|
2936
|
+
return combined.includes("quota") ? "provider_quota_exhausted" : "provider_rate_limited";
|
|
2937
|
+
}
|
|
2938
|
+
if (combined.includes("budget hard-stop")) {
|
|
2939
|
+
return "budget_hard_stop";
|
|
2940
|
+
}
|
|
2941
|
+
if (combined.includes("already in progress") || combined.includes("skipped_overlap")) {
|
|
2942
|
+
return "overlap_in_progress";
|
|
2943
|
+
}
|
|
2944
|
+
if (combined.includes("unauthorized") || combined.includes("auth") || combined.includes("api key")) {
|
|
2945
|
+
return "auth_error";
|
|
2946
|
+
}
|
|
2947
|
+
if (combined.includes("contract") || combined.includes("missing_structured_output")) {
|
|
2948
|
+
return "contract_invalid";
|
|
2949
|
+
}
|
|
2950
|
+
if (combined.includes("watchdog_timeout") || combined.includes("runtime_timeout") || combined.includes("timed out")) {
|
|
2951
|
+
return "timeout";
|
|
2952
|
+
}
|
|
2953
|
+
if (combined.includes("cancelled")) {
|
|
2954
|
+
return "cancelled";
|
|
2955
|
+
}
|
|
2956
|
+
if (combined.includes("enoent") || combined.includes("runtime_missing")) {
|
|
2957
|
+
return "runtime_missing";
|
|
2958
|
+
}
|
|
2959
|
+
if (
|
|
2960
|
+
combined.includes("provider unavailable") ||
|
|
2961
|
+
combined.includes("no capacity") ||
|
|
2962
|
+
combined.includes("unavailable") ||
|
|
2963
|
+
combined.includes("http_error")
|
|
2964
|
+
) {
|
|
2965
|
+
return "provider_unavailable";
|
|
2966
|
+
}
|
|
2967
|
+
if (input.outcome?.kind === "blocked") {
|
|
2968
|
+
return "blocked";
|
|
2969
|
+
}
|
|
2970
|
+
return "runtime_error";
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function isNoAssignedWorkOutcomeForReport(outcome: ExecutionOutcome | null) {
|
|
2974
|
+
if (!outcome) {
|
|
2975
|
+
return false;
|
|
2976
|
+
}
|
|
2977
|
+
if (outcome.kind !== "skipped") {
|
|
2978
|
+
return false;
|
|
2979
|
+
}
|
|
2980
|
+
if (outcome.issueIdsTouched.length === 0) {
|
|
2981
|
+
return true;
|
|
2982
|
+
}
|
|
2983
|
+
return outcome.actions.some((action) => action.type === "heartbeat.skip");
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
function buildRunCostSummary(input: {
|
|
2987
|
+
tokenInput: number;
|
|
2988
|
+
tokenOutput: number;
|
|
2989
|
+
usdCost: number | null;
|
|
2990
|
+
usdCostStatus: "exact" | "estimated" | "unknown";
|
|
2991
|
+
pricingSource: string | null;
|
|
2992
|
+
source: string | null;
|
|
2993
|
+
}): RunCostSummary {
|
|
2994
|
+
return {
|
|
2995
|
+
tokenInput: Math.max(0, input.tokenInput),
|
|
2996
|
+
tokenOutput: Math.max(0, input.tokenOutput),
|
|
2997
|
+
usdCost: input.usdCostStatus === "unknown" ? null : Math.max(0, input.usdCost ?? 0),
|
|
2998
|
+
usdCostStatus: input.usdCostStatus,
|
|
2999
|
+
pricingSource: input.pricingSource ?? null,
|
|
3000
|
+
source: input.source ?? null
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
function buildRunArtifacts(input: {
|
|
3005
|
+
outcome: ExecutionOutcome | null;
|
|
3006
|
+
finalRunOutput?: AgentFinalRunOutput | null;
|
|
3007
|
+
runtimeCwd?: string | null;
|
|
3008
|
+
workspaceRootPath?: string | null;
|
|
3009
|
+
companyId?: string;
|
|
3010
|
+
}): RunArtifact[] {
|
|
3011
|
+
const sourceArtifacts =
|
|
3012
|
+
input.finalRunOutput?.artifacts && input.finalRunOutput.artifacts.length > 0
|
|
3013
|
+
? input.finalRunOutput.artifacts
|
|
3014
|
+
: input.outcome?.artifacts ?? [];
|
|
3015
|
+
if (sourceArtifacts.length === 0) {
|
|
3016
|
+
return [];
|
|
3017
|
+
}
|
|
3018
|
+
const runtimeCwd = input.runtimeCwd?.trim() ? input.runtimeCwd.trim() : null;
|
|
3019
|
+
const workspaceRootPath = input.workspaceRootPath?.trim() ? input.workspaceRootPath.trim() : null;
|
|
3020
|
+
const companyId = input.companyId?.trim() ? input.companyId.trim() : null;
|
|
3021
|
+
return sourceArtifacts.map((artifact) => {
|
|
3022
|
+
const originalPath = artifact.path.trim();
|
|
3023
|
+
const artifactIsAbsolute = isAbsolute(originalPath);
|
|
3024
|
+
const absolutePath = artifactIsAbsolute ? resolve(originalPath) : runtimeCwd ? resolve(runtimeCwd, originalPath) : null;
|
|
3025
|
+
let relativePathValue: string | null = null;
|
|
3026
|
+
if (absolutePath && workspaceRootPath && isInsidePath(workspaceRootPath, absolutePath)) {
|
|
3027
|
+
relativePathValue = toNormalizedWorkspaceRelativePath(relative(workspaceRootPath, absolutePath));
|
|
3028
|
+
} else if (!artifactIsAbsolute) {
|
|
3029
|
+
relativePathValue = toNormalizedWorkspaceRelativePath(originalPath);
|
|
3030
|
+
} else if (runtimeCwd) {
|
|
3031
|
+
const candidate = toNormalizedWorkspaceRelativePath(relative(runtimeCwd, absolutePath ?? originalPath));
|
|
3032
|
+
relativePathValue = candidate && !candidate.startsWith("../") ? candidate : null;
|
|
3033
|
+
}
|
|
3034
|
+
if (companyId) {
|
|
3035
|
+
const normalizedRelative = normalizeAgentOperatingArtifactRelativePath(relativePathValue, companyId);
|
|
3036
|
+
if (normalizedRelative) {
|
|
3037
|
+
relativePathValue = normalizedRelative;
|
|
3038
|
+
} else {
|
|
3039
|
+
const normalizedOriginal = toNormalizedWorkspaceRelativePath(originalPath);
|
|
3040
|
+
const normalizedFromOriginal = normalizeAgentOperatingArtifactRelativePath(normalizedOriginal, companyId);
|
|
3041
|
+
if (normalizedFromOriginal) {
|
|
3042
|
+
relativePathValue = normalizedFromOriginal;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
const location = relativePathValue ?? absolutePath ?? originalPath;
|
|
3047
|
+
return {
|
|
3048
|
+
path: originalPath,
|
|
3049
|
+
kind: artifact.kind,
|
|
3050
|
+
label: describeArtifact(artifact.kind, location),
|
|
3051
|
+
relativePath: relativePathValue,
|
|
3052
|
+
absolutePath
|
|
3053
|
+
};
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function toNormalizedWorkspaceRelativePath(inputPath: string | null | undefined) {
|
|
3058
|
+
const trimmed = inputPath?.trim();
|
|
3059
|
+
if (!trimmed) {
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
const unixSeparated = trimmed.replace(/\\/g, "/");
|
|
3063
|
+
const parts: string[] = [];
|
|
3064
|
+
for (const part of unixSeparated.split("/")) {
|
|
3065
|
+
if (!part || part === ".") {
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
if (part === "..") {
|
|
3069
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
3070
|
+
parts.pop();
|
|
3071
|
+
} else {
|
|
3072
|
+
parts.push(part);
|
|
3073
|
+
}
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
parts.push(part);
|
|
3077
|
+
}
|
|
3078
|
+
const normalized = parts.join("/");
|
|
3079
|
+
return normalized || null;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function normalizeAgentOperatingArtifactRelativePath(pathValue: string | null, companyId: string) {
|
|
3083
|
+
const normalized = toNormalizedWorkspaceRelativePath(pathValue);
|
|
3084
|
+
if (!normalized) {
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
const workspaceScopedMatch = normalized.match(/(?:^|\/)(workspace\/[^/]+\/agents\/[^/]+\/operating(?:\/.*)?)$/);
|
|
3088
|
+
if (workspaceScopedMatch) {
|
|
3089
|
+
const scopedPath = toNormalizedWorkspaceRelativePath(workspaceScopedMatch[1]);
|
|
3090
|
+
if (!scopedPath) {
|
|
3091
|
+
return null;
|
|
3092
|
+
}
|
|
3093
|
+
const parsed = scopedPath.match(/^workspace\/([^/]+)\/agents\/([^/]+)\/operating(\/.*)?$/);
|
|
3094
|
+
if (!parsed) {
|
|
3095
|
+
return null;
|
|
3096
|
+
}
|
|
3097
|
+
const embeddedCompanyId = parsed[1]?.trim() || companyId;
|
|
3098
|
+
const agentId = parsed[2];
|
|
3099
|
+
const suffix = parsed[3] ?? "";
|
|
3100
|
+
const effectiveCompanyId = embeddedCompanyId;
|
|
3101
|
+
return `workspace/${effectiveCompanyId}/agents/${agentId}/operating${suffix}`;
|
|
3102
|
+
}
|
|
3103
|
+
const directMatch = normalized.match(/^agents\/([^/]+)\/operating(\/.*)?$/);
|
|
3104
|
+
if (directMatch) {
|
|
3105
|
+
const [, agentId, suffix = ""] = directMatch;
|
|
3106
|
+
return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
|
|
3107
|
+
}
|
|
3108
|
+
const issueScopedMatch = normalized.match(
|
|
3109
|
+
/^(?:[^/]+\/)?projects\/[^/]+\/issues\/[^/]+\/agents\/([^/]+)\/operating(\/.*)?$/
|
|
3110
|
+
);
|
|
3111
|
+
if (issueScopedMatch) {
|
|
3112
|
+
const [, agentId, suffix = ""] = issueScopedMatch;
|
|
3113
|
+
return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
|
|
3114
|
+
}
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function describeArtifact(kind: string, location: string) {
|
|
3119
|
+
const normalizedKind = kind.toLowerCase();
|
|
3120
|
+
if (normalizedKind.includes("folder") || normalizedKind.includes("directory") || normalizedKind === "website") {
|
|
3121
|
+
return `Created ${normalizedKind.replace(/_/g, " ")} at ${location}`;
|
|
3122
|
+
}
|
|
3123
|
+
if (normalizedKind.includes("file")) {
|
|
3124
|
+
return `Updated file ${location}`;
|
|
3125
|
+
}
|
|
3126
|
+
return `Produced ${normalizedKind.replace(/_/g, " ")} at ${location}`;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
function buildRunCompletionReport(input: {
|
|
3130
|
+
companyId?: string;
|
|
3131
|
+
agentName: string;
|
|
3132
|
+
providerType: HeartbeatProviderType;
|
|
3133
|
+
issueIds: string[];
|
|
3134
|
+
executionSummary: string;
|
|
3135
|
+
outcome: ExecutionOutcome | null;
|
|
3136
|
+
finalRunOutput?: AgentFinalRunOutput | null;
|
|
3137
|
+
trace: unknown;
|
|
3138
|
+
digest: RunDigest;
|
|
3139
|
+
terminal: RunTerminalPresentation;
|
|
3140
|
+
cost: RunCostSummary;
|
|
3141
|
+
runtimeCwd?: string | null;
|
|
3142
|
+
errorType?: string | null;
|
|
3143
|
+
errorMessage?: string | null;
|
|
3144
|
+
}): RunCompletionReport {
|
|
3145
|
+
const workspaceRootPath = input.companyId ? resolveCompanyWorkspaceRootPath(input.companyId) : null;
|
|
3146
|
+
const artifacts = buildRunArtifacts({
|
|
3147
|
+
outcome: input.outcome,
|
|
3148
|
+
finalRunOutput: input.finalRunOutput,
|
|
3149
|
+
runtimeCwd: input.runtimeCwd,
|
|
3150
|
+
workspaceRootPath,
|
|
3151
|
+
companyId: input.companyId
|
|
3152
|
+
});
|
|
3153
|
+
const fallbackSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
|
|
3154
|
+
const employeeComment =
|
|
3155
|
+
input.finalRunOutput?.employee_comment?.trim() || buildLegacyEmployeeComment(fallbackSummary);
|
|
3156
|
+
const results = input.finalRunOutput
|
|
3157
|
+
? input.finalRunOutput.results.filter((value): value is string => Boolean(value))
|
|
3158
|
+
: input.terminal.publicStatus === "completed"
|
|
3159
|
+
? dedupeRunDigestPoints(
|
|
3160
|
+
[
|
|
3161
|
+
input.digest.successes[0],
|
|
3162
|
+
artifacts[0]?.label,
|
|
3163
|
+
input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : null
|
|
3164
|
+
].filter((value): value is string => Boolean(value)),
|
|
3165
|
+
4
|
|
3166
|
+
)
|
|
3167
|
+
: [];
|
|
3168
|
+
const errors =
|
|
3169
|
+
input.finalRunOutput?.errors.filter((value): value is string => Boolean(value)) ??
|
|
3170
|
+
dedupeRunDigestPoints([...input.digest.blockers, ...input.digest.failures].filter((value): value is string => Boolean(value)), 4);
|
|
3171
|
+
const summary = firstMeaningfulReportLine(employeeComment) || results[0] || fallbackSummary;
|
|
3172
|
+
const resultSummary =
|
|
3173
|
+
results[0] ??
|
|
3174
|
+
(input.terminal.publicStatus === "completed"
|
|
3175
|
+
? artifacts[0]?.label ??
|
|
3176
|
+
(input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : summary)
|
|
3177
|
+
: input.finalRunOutput
|
|
3178
|
+
? summary
|
|
3179
|
+
: "No valid final run output was produced.");
|
|
3180
|
+
const statusHeadline =
|
|
3181
|
+
input.terminal.publicStatus === "completed"
|
|
3182
|
+
? `Completed: ${summary}`
|
|
3183
|
+
: `Failed: ${summary}`;
|
|
3184
|
+
const blockers = dedupeRunDigestPoints(errors, 4);
|
|
3185
|
+
const artifactPaths = artifacts
|
|
3186
|
+
.map((artifact) => artifact.relativePath ?? artifact.absolutePath ?? artifact.path)
|
|
3187
|
+
.filter((value): value is string => Boolean(value));
|
|
3188
|
+
const managerReport = {
|
|
3189
|
+
agentName: input.agentName,
|
|
3190
|
+
providerType: input.providerType,
|
|
3191
|
+
whatWasDone: results[0] ?? (input.terminal.publicStatus === "completed" ? input.digest.successes[0] ?? summary : summary),
|
|
3192
|
+
resultSummary,
|
|
3193
|
+
artifactPaths,
|
|
3194
|
+
blockers,
|
|
3195
|
+
nextAction: input.digest.nextAction,
|
|
3196
|
+
costLine: formatRunCostLine(input.cost)
|
|
3197
|
+
};
|
|
3198
|
+
const fallbackOutcome: ExecutionOutcome = input.outcome ?? {
|
|
3199
|
+
kind:
|
|
3200
|
+
input.terminal.completionReason === "no_assigned_work"
|
|
3201
|
+
? "skipped"
|
|
3202
|
+
: input.terminal.publicStatus === "completed"
|
|
3203
|
+
? "completed"
|
|
3204
|
+
: "failed",
|
|
3205
|
+
issueIdsTouched: input.issueIds,
|
|
3206
|
+
artifacts: artifacts.map((artifact) => ({ path: artifact.path, kind: artifact.kind })),
|
|
3207
|
+
actions:
|
|
3208
|
+
results.length > 0
|
|
3209
|
+
? results.slice(0, 4).map((result) => ({
|
|
3210
|
+
type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
|
|
3211
|
+
status: input.terminal.publicStatus === "completed" ? "ok" : "error",
|
|
3212
|
+
detail: result
|
|
3213
|
+
}))
|
|
3214
|
+
: [
|
|
3215
|
+
{
|
|
3216
|
+
type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
|
|
3217
|
+
status: input.terminal.publicStatus === "completed" ? "ok" : "error",
|
|
3218
|
+
detail: managerReport.whatWasDone
|
|
3219
|
+
}
|
|
3220
|
+
],
|
|
3221
|
+
blockers: blockers.map((message) => ({
|
|
3222
|
+
code: input.terminal.completionReason,
|
|
3223
|
+
message,
|
|
3224
|
+
retryable: input.terminal.publicStatus !== "completed"
|
|
3225
|
+
})),
|
|
3226
|
+
nextSuggestedState: input.terminal.publicStatus === "completed" ? "in_review" : "blocked"
|
|
3227
|
+
};
|
|
3228
|
+
return {
|
|
3229
|
+
finalStatus: input.terminal.publicStatus,
|
|
3230
|
+
completionReason: input.terminal.completionReason,
|
|
3231
|
+
statusHeadline,
|
|
3232
|
+
summary,
|
|
3233
|
+
employeeComment,
|
|
3234
|
+
results,
|
|
3235
|
+
errors,
|
|
3236
|
+
resultStatus: artifacts.length > 0 ? "reported" : "none_reported",
|
|
3237
|
+
resultSummary,
|
|
3238
|
+
issueIds: input.issueIds,
|
|
3239
|
+
artifacts,
|
|
3240
|
+
blockers,
|
|
3241
|
+
nextAction: input.digest.nextAction,
|
|
3242
|
+
cost: input.cost,
|
|
3243
|
+
managerReport,
|
|
3244
|
+
outcome: input.outcome ?? fallbackOutcome,
|
|
3245
|
+
debug: {
|
|
3246
|
+
persistedRunStatus: input.terminal.internalStatus,
|
|
3247
|
+
failureType: readTraceString(input.trace, "failureType"),
|
|
3248
|
+
errorType: input.errorType ?? null,
|
|
3249
|
+
errorMessage: input.errorMessage ?? null
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function firstMeaningfulReportLine(value: string) {
|
|
3255
|
+
for (const rawLine of value.split(/\r?\n/)) {
|
|
3256
|
+
const line = rawLine.replace(/^[#>*\-\s`]+/, "").trim();
|
|
3257
|
+
if (line) {
|
|
3258
|
+
return line;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return "";
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
function buildLegacyEmployeeComment(summary: string) {
|
|
3265
|
+
return summary;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
function formatRunCostLine(cost: RunCostSummary) {
|
|
3269
|
+
const tokens = `${cost.tokenInput} input / ${cost.tokenOutput} output tokens`;
|
|
3270
|
+
if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
|
|
3271
|
+
return `${tokens}; dollar cost unknown`;
|
|
3272
|
+
}
|
|
3273
|
+
const qualifier = cost.usdCostStatus === "estimated" ? "estimated" : "exact";
|
|
3274
|
+
return `${tokens}; ${qualifier} cost $${cost.usdCost.toFixed(6)}`;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
function buildHumanRunUpdateCommentFromReport(
|
|
3278
|
+
report: RunCompletionReport,
|
|
3279
|
+
options: { runId: string; companyId: string }
|
|
3280
|
+
) {
|
|
3281
|
+
const lines = [
|
|
3282
|
+
report.employeeComment.trim(),
|
|
3283
|
+
"",
|
|
3284
|
+
`- Status: ${report.finalStatus}`,
|
|
3285
|
+
`- Agent: ${report.managerReport.agentName}`,
|
|
3286
|
+
`- Provider: ${report.managerReport.providerType}`,
|
|
3287
|
+
""
|
|
3288
|
+
];
|
|
3289
|
+
if (report.results.length > 0) {
|
|
3290
|
+
lines.push("### Results", "");
|
|
3291
|
+
for (const result of report.results) {
|
|
3292
|
+
lines.push(`- ${result}`);
|
|
3293
|
+
}
|
|
3294
|
+
lines.push("");
|
|
3295
|
+
}
|
|
3296
|
+
lines.push("### Result", "", `- What was done: ${report.managerReport.whatWasDone}`, `- Summary: ${report.managerReport.resultSummary}`);
|
|
3297
|
+
if (report.artifacts.length > 0) {
|
|
3298
|
+
for (const [artifactIndex, artifact] of report.artifacts.entries()) {
|
|
3299
|
+
lines.push(`- Artifact: ${formatRunArtifactMarkdownLink(artifact, { ...options, artifactIndex })}`);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
lines.push("");
|
|
3303
|
+
lines.push("### Cost", "");
|
|
3304
|
+
lines.push(`- Input tokens: \`${report.cost.tokenInput}\``);
|
|
3305
|
+
lines.push(`- Output tokens: \`${report.cost.tokenOutput}\``);
|
|
3306
|
+
lines.push(`- Dollar cost: ${formatRunCostForHumanReport(report.cost)}`);
|
|
3307
|
+
if (report.errors.length > 0) {
|
|
3308
|
+
lines.push("");
|
|
3309
|
+
lines.push("### Errors", "");
|
|
3310
|
+
for (const error of report.errors) {
|
|
3311
|
+
lines.push(`- ${error}`);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
return lines.join("\n");
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function formatRunArtifactMarkdownLink(
|
|
3318
|
+
artifact: RunArtifact,
|
|
3319
|
+
options: { runId: string; companyId: string; artifactIndex: number }
|
|
3320
|
+
) {
|
|
3321
|
+
const label = resolveRunArtifactDisplayPath(artifact);
|
|
3322
|
+
const href = buildRunArtifactLinkHref(options);
|
|
3323
|
+
if (!label) {
|
|
3324
|
+
return "`artifact`";
|
|
3325
|
+
}
|
|
3326
|
+
if (!href) {
|
|
3327
|
+
return `\`${label}\``;
|
|
3328
|
+
}
|
|
3329
|
+
return `[${label}](${href})`;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function resolveRunArtifactDisplayPath(artifact: RunArtifact) {
|
|
3333
|
+
const relative = toNormalizedWorkspaceRelativePath(artifact.relativePath);
|
|
3334
|
+
if (relative && !relative.startsWith("../")) {
|
|
3335
|
+
return relative;
|
|
3336
|
+
}
|
|
3337
|
+
const pathValue = toNormalizedWorkspaceRelativePath(artifact.path);
|
|
3338
|
+
if (pathValue && !pathValue.startsWith("../") && !isAbsolute(artifact.path)) {
|
|
3339
|
+
return pathValue;
|
|
3340
|
+
}
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function buildRunArtifactLinkHref(options: { runId: string; companyId: string; artifactIndex: number }) {
|
|
3345
|
+
const apiBaseUrl = resolveControlPlaneApiBaseUrl().replace(/\/+$/, "");
|
|
3346
|
+
const runId = encodeURIComponent(options.runId);
|
|
3347
|
+
const artifactIndex = encodeURIComponent(String(options.artifactIndex));
|
|
3348
|
+
const companyId = encodeURIComponent(options.companyId);
|
|
3349
|
+
return `${apiBaseUrl}/observability/heartbeats/${runId}/artifacts/${artifactIndex}/download?companyId=${companyId}`;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
function formatRunCostForHumanReport(cost: RunCostSummary) {
|
|
3353
|
+
if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
|
|
3354
|
+
return "unknown";
|
|
3355
|
+
}
|
|
3356
|
+
const qualifier = cost.usdCostStatus === "estimated" ? "estimated " : "exact ";
|
|
3357
|
+
return `${qualifier}\`$${cost.usdCost.toFixed(6)}\``;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
function buildRunListMessageFromReport(report: RunCompletionReport) {
|
|
3361
|
+
const resultParts =
|
|
3362
|
+
report.finalStatus === "completed"
|
|
3363
|
+
? report.results.length > 0
|
|
3364
|
+
? report.results.slice(0, 2)
|
|
3365
|
+
: [report.resultSummary]
|
|
3366
|
+
: [];
|
|
3367
|
+
const parts = [report.statusHeadline, ...resultParts];
|
|
3368
|
+
if (report.artifacts.length > 0) {
|
|
3369
|
+
parts.push(`Artifacts: ${report.managerReport.artifactPaths.join(", ")}`);
|
|
3370
|
+
}
|
|
3371
|
+
if (report.cost.usdCostStatus === "unknown") {
|
|
3372
|
+
parts.push("Cost: unknown");
|
|
3373
|
+
} else if (report.cost.usdCost !== null && report.cost.usdCost !== undefined) {
|
|
3374
|
+
parts.push(`Cost: $${report.cost.usdCost.toFixed(6)}`);
|
|
3375
|
+
}
|
|
3376
|
+
const compact = parts.filter(Boolean).join(" | ");
|
|
3377
|
+
return compact.length > 220 ? `${compact.slice(0, 217).trimEnd()}...` : compact;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
function isMachineNoiseLine(text: string) {
|
|
3381
|
+
const normalized = text.trim();
|
|
3382
|
+
if (!normalized) {
|
|
3383
|
+
return true;
|
|
3384
|
+
}
|
|
3385
|
+
if (normalized.length > 220) {
|
|
3386
|
+
return true;
|
|
3387
|
+
}
|
|
3388
|
+
const patterns = [
|
|
3389
|
+
/^command:\s*/i,
|
|
3390
|
+
/^\s*[\[{].*[\]}]\s*$/,
|
|
3391
|
+
/\/bin\/(bash|zsh|sh)/i,
|
|
3392
|
+
/(^|\s)(\/Users\/|\/home\/|\/private\/var\/|[A-Za-z]:\\)/,
|
|
3393
|
+
/\b(stderr|stdout|stack trace|exit code|payload_json|tokeninput|tokenoutput|usdcost)\b/i,
|
|
3394
|
+
/(^|\s)at\s+\S+:\d+:\d+/,
|
|
3395
|
+
/```/,
|
|
3396
|
+
/\{[\s\S]*"(summary|tokenInput|tokenOutput|usdCost|trace|error)"[\s\S]*\}/i
|
|
3397
|
+
];
|
|
3398
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
3399
|
+
}
|
|
3400
|
+
|
|
2326
3401
|
function extractSummaryFromJsonLikeText(input: string) {
|
|
2327
3402
|
const fencedMatch = input.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
2328
3403
|
const candidate = fencedMatch?.[1]?.trim() ?? input.match(/\{[\s\S]*\}\s*$/)?.[0]?.trim();
|
|
@@ -2354,19 +3429,18 @@ async function appendRunSummaryComments(
|
|
|
2354
3429
|
issueIds: string[];
|
|
2355
3430
|
agentId: string;
|
|
2356
3431
|
runId: string;
|
|
2357
|
-
|
|
2358
|
-
executionSummary: string;
|
|
3432
|
+
report: RunCompletionReport;
|
|
2359
3433
|
}
|
|
2360
3434
|
) {
|
|
2361
3435
|
if (input.issueIds.length === 0) {
|
|
2362
3436
|
return;
|
|
2363
3437
|
}
|
|
2364
|
-
const commentBody =
|
|
2365
|
-
|
|
2366
|
-
|
|
3438
|
+
const commentBody = buildHumanRunUpdateCommentFromReport(input.report, {
|
|
3439
|
+
runId: input.runId,
|
|
3440
|
+
companyId: input.companyId
|
|
2367
3441
|
});
|
|
2368
3442
|
for (const issueId of input.issueIds) {
|
|
2369
|
-
const
|
|
3443
|
+
const existingRunComments = await db
|
|
2370
3444
|
.select({ id: issueComments.id })
|
|
2371
3445
|
.from(issueComments)
|
|
2372
3446
|
.where(
|
|
@@ -2378,6 +3452,58 @@ async function appendRunSummaryComments(
|
|
|
2378
3452
|
eq(issueComments.authorId, input.agentId)
|
|
2379
3453
|
)
|
|
2380
3454
|
)
|
|
3455
|
+
.orderBy(desc(issueComments.createdAt));
|
|
3456
|
+
if (existingRunComments.length > 0) {
|
|
3457
|
+
await db.delete(issueComments).where(
|
|
3458
|
+
and(
|
|
3459
|
+
eq(issueComments.companyId, input.companyId),
|
|
3460
|
+
inArray(
|
|
3461
|
+
issueComments.id,
|
|
3462
|
+
existingRunComments.map((comment) => comment.id)
|
|
3463
|
+
)
|
|
3464
|
+
)
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
await addIssueComment(db, {
|
|
3468
|
+
companyId: input.companyId,
|
|
3469
|
+
issueId,
|
|
3470
|
+
authorType: "agent",
|
|
3471
|
+
authorId: input.agentId,
|
|
3472
|
+
runId: input.runId,
|
|
3473
|
+
body: commentBody
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
async function appendProviderUsageLimitBoardComments(
|
|
3479
|
+
db: BopoDb,
|
|
3480
|
+
input: {
|
|
3481
|
+
companyId: string;
|
|
3482
|
+
issueIds: string[];
|
|
3483
|
+
agentId: string;
|
|
3484
|
+
runId: string;
|
|
3485
|
+
providerType: string;
|
|
3486
|
+
message: string;
|
|
3487
|
+
paused: boolean;
|
|
3488
|
+
}
|
|
3489
|
+
) {
|
|
3490
|
+
if (input.issueIds.length === 0) {
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
const commentBody = buildProviderUsageLimitBoardCommentBody(input);
|
|
3494
|
+
for (const issueId of input.issueIds) {
|
|
3495
|
+
const [existingRunComment] = await db
|
|
3496
|
+
.select({ id: issueComments.id })
|
|
3497
|
+
.from(issueComments)
|
|
3498
|
+
.where(
|
|
3499
|
+
and(
|
|
3500
|
+
eq(issueComments.companyId, input.companyId),
|
|
3501
|
+
eq(issueComments.issueId, issueId),
|
|
3502
|
+
eq(issueComments.runId, input.runId),
|
|
3503
|
+
eq(issueComments.authorType, "system"),
|
|
3504
|
+
eq(issueComments.authorId, input.agentId)
|
|
3505
|
+
)
|
|
3506
|
+
)
|
|
2381
3507
|
.limit(1);
|
|
2382
3508
|
if (existingRunComment) {
|
|
2383
3509
|
continue;
|
|
@@ -2385,14 +3511,70 @@ async function appendRunSummaryComments(
|
|
|
2385
3511
|
await addIssueComment(db, {
|
|
2386
3512
|
companyId: input.companyId,
|
|
2387
3513
|
issueId,
|
|
2388
|
-
authorType: "
|
|
3514
|
+
authorType: "system",
|
|
2389
3515
|
authorId: input.agentId,
|
|
2390
3516
|
runId: input.runId,
|
|
3517
|
+
recipients: [
|
|
3518
|
+
{
|
|
3519
|
+
recipientType: "board",
|
|
3520
|
+
deliveryStatus: "pending"
|
|
3521
|
+
}
|
|
3522
|
+
],
|
|
2391
3523
|
body: commentBody
|
|
2392
3524
|
});
|
|
2393
3525
|
}
|
|
2394
3526
|
}
|
|
2395
3527
|
|
|
3528
|
+
function buildProviderUsageLimitBoardCommentBody(input: {
|
|
3529
|
+
providerType: string;
|
|
3530
|
+
message: string;
|
|
3531
|
+
paused: boolean;
|
|
3532
|
+
}) {
|
|
3533
|
+
const providerLabel = input.providerType.replace(/[_-]+/g, " ").trim();
|
|
3534
|
+
const normalizedProvider = providerLabel.charAt(0).toUpperCase() + providerLabel.slice(1);
|
|
3535
|
+
const agentStateLine = input.paused ? "Agent paused." : "Agent already paused.";
|
|
3536
|
+
return `${normalizedProvider} usage limit reached.\nRun failed due to provider limits.\n${agentStateLine}\nNext: resume after usage reset or billing/credential fix.`;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
async function pauseAgentForProviderUsageLimit(
|
|
3540
|
+
db: BopoDb,
|
|
3541
|
+
input: {
|
|
3542
|
+
companyId: string;
|
|
3543
|
+
agentId: string;
|
|
3544
|
+
requestId: string;
|
|
3545
|
+
runId: string;
|
|
3546
|
+
providerType: string;
|
|
3547
|
+
message: string;
|
|
3548
|
+
}
|
|
3549
|
+
) {
|
|
3550
|
+
const [agentRow] = await db
|
|
3551
|
+
.select({ status: agents.status })
|
|
3552
|
+
.from(agents)
|
|
3553
|
+
.where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)))
|
|
3554
|
+
.limit(1);
|
|
3555
|
+
if (!agentRow || agentRow.status === "paused" || agentRow.status === "terminated") {
|
|
3556
|
+
return { paused: false as const };
|
|
3557
|
+
}
|
|
3558
|
+
await db
|
|
3559
|
+
.update(agents)
|
|
3560
|
+
.set({ status: "paused", updatedAt: new Date() })
|
|
3561
|
+
.where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)));
|
|
3562
|
+
await appendAuditEvent(db, {
|
|
3563
|
+
companyId: input.companyId,
|
|
3564
|
+
actorType: "system",
|
|
3565
|
+
eventType: "agent.paused_auto_provider_limit",
|
|
3566
|
+
entityType: "agent",
|
|
3567
|
+
entityId: input.agentId,
|
|
3568
|
+
correlationId: input.requestId,
|
|
3569
|
+
payload: {
|
|
3570
|
+
runId: input.runId,
|
|
3571
|
+
providerType: input.providerType,
|
|
3572
|
+
reason: input.message
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
return { paused: true as const };
|
|
3576
|
+
}
|
|
3577
|
+
|
|
2396
3578
|
function parseAgentState(stateBlob: string | null) {
|
|
2397
3579
|
if (!stateBlob) {
|
|
2398
3580
|
return { state: {} as AgentState, parseError: null };
|
|
@@ -2735,10 +3917,7 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
2735
3917
|
}
|
|
2736
3918
|
|
|
2737
3919
|
if (projectIssue?.id) {
|
|
2738
|
-
const issueScopedWorkspaceCwd =
|
|
2739
|
-
companyId,
|
|
2740
|
-
join(selectedWorkspaceCwd, "issues", projectIssue.id)
|
|
2741
|
-
);
|
|
3920
|
+
const issueScopedWorkspaceCwd = resolveProjectIssueWorkspaceCwd(companyId, selectedWorkspaceCwd, projectIssue.id);
|
|
2742
3921
|
await mkdir(issueScopedWorkspaceCwd, { recursive: true });
|
|
2743
3922
|
selectedWorkspaceCwd = issueScopedWorkspaceCwd;
|
|
2744
3923
|
}
|
|
@@ -2787,6 +3966,10 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
2787
3966
|
};
|
|
2788
3967
|
}
|
|
2789
3968
|
|
|
3969
|
+
function resolveProjectIssueWorkspaceCwd(companyId: string, projectWorkspaceCwd: string, issueId: string) {
|
|
3970
|
+
return normalizeCompanyWorkspacePath(companyId, join(projectWorkspaceCwd, "issues", issueId));
|
|
3971
|
+
}
|
|
3972
|
+
|
|
2790
3973
|
function resolveGitWorktreeIsolationEnabled() {
|
|
2791
3974
|
const value = String(process.env.BOPO_ENABLE_GIT_WORKTREE_ISOLATION ?? "")
|
|
2792
3975
|
.trim()
|
|
@@ -3414,6 +4597,7 @@ function resolveRuntimeModelId(input: { runtimeModel?: string; stateBlob?: strin
|
|
|
3414
4597
|
async function appendFinishedRunCostEntry(input: {
|
|
3415
4598
|
db: BopoDb;
|
|
3416
4599
|
companyId: string;
|
|
4600
|
+
runId?: string | null;
|
|
3417
4601
|
providerType: string;
|
|
3418
4602
|
runtimeModelId: string | null;
|
|
3419
4603
|
pricingProviderType?: string | null;
|
|
@@ -3440,25 +4624,22 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
3440
4624
|
const shouldPersist = input.status === "ok" || input.status === "failed";
|
|
3441
4625
|
const runtimeUsdCost = Math.max(0, input.runtimeUsdCost ?? 0);
|
|
3442
4626
|
const pricedUsdCost = Math.max(0, pricingDecision.usdCost);
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
const effectiveUsdCost =
|
|
3446
|
-
baseUsdCost > 0
|
|
3447
|
-
? baseUsdCost
|
|
3448
|
-
: input.status === "failed" && input.failureType !== "spawn_error"
|
|
3449
|
-
? 0.000001
|
|
3450
|
-
: 0;
|
|
4627
|
+
const usdCostStatus: "exact" | "estimated" | "unknown" =
|
|
4628
|
+
runtimeUsdCost > 0 ? "exact" : pricedUsdCost > 0 ? "estimated" : "unknown";
|
|
4629
|
+
const effectiveUsdCost = usdCostStatus === "exact" ? runtimeUsdCost : usdCostStatus === "estimated" ? pricedUsdCost : 0;
|
|
3451
4630
|
const effectivePricingSource = pricingDecision.pricingSource;
|
|
3452
4631
|
const shouldPersistWithUsage =
|
|
3453
|
-
shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 ||
|
|
4632
|
+
shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 || usdCostStatus !== "unknown");
|
|
3454
4633
|
if (shouldPersistWithUsage) {
|
|
3455
4634
|
await appendCost(input.db, {
|
|
3456
4635
|
companyId: input.companyId,
|
|
4636
|
+
runId: input.runId ?? null,
|
|
3457
4637
|
providerType: input.providerType,
|
|
3458
4638
|
runtimeModelId: input.runtimeModelId,
|
|
3459
4639
|
pricingProviderType: pricingDecision.pricingProviderType,
|
|
3460
4640
|
pricingModelId: pricingDecision.pricingModelId,
|
|
3461
4641
|
pricingSource: effectivePricingSource,
|
|
4642
|
+
usdCostStatus,
|
|
3462
4643
|
tokenInput: input.tokenInput,
|
|
3463
4644
|
tokenOutput: input.tokenOutput,
|
|
3464
4645
|
usdCost: effectiveUsdCost.toFixed(6),
|
|
@@ -3471,7 +4652,8 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
3471
4652
|
return {
|
|
3472
4653
|
...pricingDecision,
|
|
3473
4654
|
pricingSource: effectivePricingSource,
|
|
3474
|
-
usdCost: effectiveUsdCost
|
|
4655
|
+
usdCost: effectiveUsdCost,
|
|
4656
|
+
usdCostStatus
|
|
3475
4657
|
};
|
|
3476
4658
|
}
|
|
3477
4659
|
|