bopodev-api 0.1.25 → 0.1.27
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/README.md +44 -0
- package/package.json +4 -4
- package/src/lib/run-artifact-paths.ts +80 -0
- package/src/pricing/anthropic.ts +23 -0
- package/src/pricing/gemini.ts +11 -0
- package/src/pricing/index.ts +29 -0
- package/src/pricing/openai.ts +47 -0
- package/src/pricing/opencode.ts +5 -0
- package/src/pricing/types.ts +8 -0
- package/src/routes/companies.ts +0 -2
- package/src/routes/governance.ts +80 -2
- package/src/routes/issues.ts +16 -1
- package/src/routes/observability.ts +79 -64
- package/src/scripts/onboard-seed.ts +6 -9
- package/src/server.ts +89 -3
- package/src/services/attention-service.ts +109 -48
- package/src/services/governance-service.ts +5 -5
- package/src/services/heartbeat-service.ts +1350 -86
- package/src/services/memory-file-service.ts +0 -3
- package/src/services/model-pricing.ts +4 -128
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
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
|
-
import type { AgentState, HeartbeatContext } from "bopodev-agent-sdk";
|
|
6
|
+
import type { AdapterExecutionResult, 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,8 +35,19 @@ 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 {
|
|
34
|
-
|
|
38
|
+
import {
|
|
39
|
+
isInsidePath,
|
|
40
|
+
normalizeCompanyWorkspacePath,
|
|
41
|
+
resolveCompanyWorkspaceRootPath,
|
|
42
|
+
resolveProjectWorkspacePath
|
|
43
|
+
} from "../lib/instance-paths";
|
|
44
|
+
import { resolveRunArtifactAbsolutePath } from "../lib/run-artifact-paths";
|
|
45
|
+
import {
|
|
46
|
+
assertRuntimeCwdForCompany,
|
|
47
|
+
getProjectWorkspaceContextMap,
|
|
48
|
+
hasText,
|
|
49
|
+
resolveAgentFallbackWorkspace
|
|
50
|
+
} from "../lib/workspace-policy";
|
|
35
51
|
import type { RealtimeHub } from "../realtime/hub";
|
|
36
52
|
import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
|
|
37
53
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
@@ -73,6 +89,39 @@ type HeartbeatWakeContext = {
|
|
|
73
89
|
|
|
74
90
|
const AGENT_COMMENT_EMOJI_REGEX = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
|
|
75
91
|
|
|
92
|
+
type RunDigestSignal = {
|
|
93
|
+
sequence: number;
|
|
94
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
95
|
+
label: string | null;
|
|
96
|
+
text: string | null;
|
|
97
|
+
payload: string | null;
|
|
98
|
+
signalLevel: "high" | "medium" | "low" | "noise";
|
|
99
|
+
groupKey: string | null;
|
|
100
|
+
source: "stdout" | "stderr" | "trace_fallback";
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type RunDigest = {
|
|
104
|
+
status: "completed" | "failed" | "skipped";
|
|
105
|
+
headline: string;
|
|
106
|
+
summary: string;
|
|
107
|
+
successes: string[];
|
|
108
|
+
failures: string[];
|
|
109
|
+
blockers: string[];
|
|
110
|
+
nextAction: string;
|
|
111
|
+
evidence: {
|
|
112
|
+
transcriptSignalCount: number;
|
|
113
|
+
outcomeActionCount: number;
|
|
114
|
+
outcomeBlockerCount: number;
|
|
115
|
+
failureType: string | null;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type RunTerminalPresentation = {
|
|
120
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
121
|
+
publicStatus: "completed" | "failed";
|
|
122
|
+
completionReason: RunCompletionReason;
|
|
123
|
+
};
|
|
124
|
+
|
|
76
125
|
export async function claimIssuesForAgent(
|
|
77
126
|
db: BopoDb,
|
|
78
127
|
companyId: string,
|
|
@@ -334,18 +383,81 @@ export async function runHeartbeatForAgent(
|
|
|
334
383
|
if (blockedProjectBudgetChecks.length > 0) {
|
|
335
384
|
const blockedProjectIds = blockedProjectBudgetChecks.map((entry) => entry.projectId);
|
|
336
385
|
const message = `Heartbeat skipped due to project budget hard-stop: ${blockedProjectIds.join(",")}.`;
|
|
386
|
+
const runDigest = buildRunDigest({
|
|
387
|
+
status: "skipped",
|
|
388
|
+
executionSummary: message,
|
|
389
|
+
outcome: null,
|
|
390
|
+
trace: null,
|
|
391
|
+
signals: []
|
|
392
|
+
});
|
|
393
|
+
const runReport = buildRunCompletionReport({
|
|
394
|
+
companyId,
|
|
395
|
+
agentName: agent.name,
|
|
396
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
397
|
+
issueIds: [],
|
|
398
|
+
executionSummary: message,
|
|
399
|
+
outcome: null,
|
|
400
|
+
trace: null,
|
|
401
|
+
digest: runDigest,
|
|
402
|
+
terminal: resolveRunTerminalPresentation({
|
|
403
|
+
internalStatus: "skipped",
|
|
404
|
+
executionSummary: message,
|
|
405
|
+
outcome: null,
|
|
406
|
+
trace: null
|
|
407
|
+
}),
|
|
408
|
+
cost: buildRunCostSummary({
|
|
409
|
+
tokenInput: 0,
|
|
410
|
+
tokenOutput: 0,
|
|
411
|
+
usdCost: null,
|
|
412
|
+
usdCostStatus: "unknown",
|
|
413
|
+
pricingSource: null,
|
|
414
|
+
source: "none"
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
337
418
|
await db.insert(heartbeatRuns).values({
|
|
338
419
|
id: runId,
|
|
339
420
|
companyId,
|
|
340
421
|
agentId,
|
|
341
422
|
status: "skipped",
|
|
342
|
-
|
|
423
|
+
finishedAt: new Date(),
|
|
424
|
+
message: runListMessage
|
|
343
425
|
});
|
|
344
426
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
345
427
|
companyId,
|
|
346
428
|
runId,
|
|
347
429
|
status: "skipped",
|
|
348
|
-
message
|
|
430
|
+
message: runListMessage,
|
|
431
|
+
finishedAt: new Date()
|
|
432
|
+
});
|
|
433
|
+
await appendAuditEvent(db, {
|
|
434
|
+
companyId,
|
|
435
|
+
actorType: "system",
|
|
436
|
+
eventType: "heartbeat.failed",
|
|
437
|
+
entityType: "heartbeat_run",
|
|
438
|
+
entityId: runId,
|
|
439
|
+
correlationId: options?.requestId ?? runId,
|
|
440
|
+
payload: {
|
|
441
|
+
agentId,
|
|
442
|
+
issueIds: [],
|
|
443
|
+
result: runReport.resultSummary,
|
|
444
|
+
message: runListMessage,
|
|
445
|
+
errorType: runReport.completionReason,
|
|
446
|
+
errorMessage: message,
|
|
447
|
+
report: runReport,
|
|
448
|
+
outcome: null,
|
|
449
|
+
usage: {
|
|
450
|
+
tokenInput: 0,
|
|
451
|
+
tokenOutput: 0,
|
|
452
|
+
usdCostStatus: "unknown",
|
|
453
|
+
source: "none"
|
|
454
|
+
},
|
|
455
|
+
trace: null,
|
|
456
|
+
diagnostics: {
|
|
457
|
+
requestId: options?.requestId,
|
|
458
|
+
trigger: runTrigger
|
|
459
|
+
}
|
|
460
|
+
}
|
|
349
461
|
});
|
|
350
462
|
for (const blockedProject of blockedProjectBudgetChecks) {
|
|
351
463
|
const approvalId = await ensureProjectBudgetOverrideApprovalRequest(db, {
|
|
@@ -386,45 +498,156 @@ export async function runHeartbeatForAgent(
|
|
|
386
498
|
if (!claimed) {
|
|
387
499
|
const skippedRunId = nanoid(14);
|
|
388
500
|
const skippedAt = new Date();
|
|
501
|
+
const overlapMessage = "Heartbeat skipped: another run is already in progress for this agent.";
|
|
502
|
+
const runDigest = buildRunDigest({
|
|
503
|
+
status: "skipped",
|
|
504
|
+
executionSummary: overlapMessage,
|
|
505
|
+
outcome: null,
|
|
506
|
+
trace: null,
|
|
507
|
+
signals: []
|
|
508
|
+
});
|
|
509
|
+
const runReport = buildRunCompletionReport({
|
|
510
|
+
companyId,
|
|
511
|
+
agentName: agent.name,
|
|
512
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
513
|
+
issueIds: [],
|
|
514
|
+
executionSummary: overlapMessage,
|
|
515
|
+
outcome: null,
|
|
516
|
+
trace: null,
|
|
517
|
+
digest: runDigest,
|
|
518
|
+
terminal: resolveRunTerminalPresentation({
|
|
519
|
+
internalStatus: "skipped",
|
|
520
|
+
executionSummary: overlapMessage,
|
|
521
|
+
outcome: null,
|
|
522
|
+
trace: null
|
|
523
|
+
}),
|
|
524
|
+
cost: buildRunCostSummary({
|
|
525
|
+
tokenInput: 0,
|
|
526
|
+
tokenOutput: 0,
|
|
527
|
+
usdCost: null,
|
|
528
|
+
usdCostStatus: "unknown",
|
|
529
|
+
pricingSource: null,
|
|
530
|
+
source: "none"
|
|
531
|
+
})
|
|
532
|
+
});
|
|
533
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
389
534
|
await db.insert(heartbeatRuns).values({
|
|
390
535
|
id: skippedRunId,
|
|
391
536
|
companyId,
|
|
392
537
|
agentId,
|
|
393
538
|
status: "skipped",
|
|
394
539
|
finishedAt: skippedAt,
|
|
395
|
-
message:
|
|
540
|
+
message: runListMessage
|
|
396
541
|
});
|
|
397
542
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
398
543
|
companyId,
|
|
399
544
|
runId: skippedRunId,
|
|
400
545
|
status: "skipped",
|
|
401
|
-
message:
|
|
546
|
+
message: runListMessage,
|
|
402
547
|
finishedAt: skippedAt
|
|
403
548
|
});
|
|
404
549
|
await appendAuditEvent(db, {
|
|
405
550
|
companyId,
|
|
406
551
|
actorType: "system",
|
|
407
|
-
eventType: "heartbeat.
|
|
552
|
+
eventType: "heartbeat.failed",
|
|
408
553
|
entityType: "heartbeat_run",
|
|
409
554
|
entityId: skippedRunId,
|
|
410
555
|
correlationId: options?.requestId ?? skippedRunId,
|
|
411
|
-
payload: {
|
|
556
|
+
payload: {
|
|
557
|
+
agentId,
|
|
558
|
+
issueIds: [],
|
|
559
|
+
result: runReport.resultSummary,
|
|
560
|
+
message: runListMessage,
|
|
561
|
+
errorType: runReport.completionReason,
|
|
562
|
+
errorMessage: overlapMessage,
|
|
563
|
+
report: runReport,
|
|
564
|
+
outcome: null,
|
|
565
|
+
usage: {
|
|
566
|
+
tokenInput: 0,
|
|
567
|
+
tokenOutput: 0,
|
|
568
|
+
usdCostStatus: "unknown",
|
|
569
|
+
source: "none"
|
|
570
|
+
},
|
|
571
|
+
trace: null,
|
|
572
|
+
diagnostics: { requestId: options?.requestId, trigger: runTrigger }
|
|
573
|
+
}
|
|
412
574
|
});
|
|
413
575
|
return skippedRunId;
|
|
414
576
|
}
|
|
415
577
|
} else {
|
|
578
|
+
const budgetMessage = "Heartbeat skipped due to budget hard-stop.";
|
|
579
|
+
const runDigest = buildRunDigest({
|
|
580
|
+
status: "skipped",
|
|
581
|
+
executionSummary: budgetMessage,
|
|
582
|
+
outcome: null,
|
|
583
|
+
trace: null,
|
|
584
|
+
signals: []
|
|
585
|
+
});
|
|
586
|
+
const runReport = buildRunCompletionReport({
|
|
587
|
+
companyId,
|
|
588
|
+
agentName: agent.name,
|
|
589
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
590
|
+
issueIds: [],
|
|
591
|
+
executionSummary: budgetMessage,
|
|
592
|
+
outcome: null,
|
|
593
|
+
trace: null,
|
|
594
|
+
digest: runDigest,
|
|
595
|
+
terminal: resolveRunTerminalPresentation({
|
|
596
|
+
internalStatus: "skipped",
|
|
597
|
+
executionSummary: budgetMessage,
|
|
598
|
+
outcome: null,
|
|
599
|
+
trace: null
|
|
600
|
+
}),
|
|
601
|
+
cost: buildRunCostSummary({
|
|
602
|
+
tokenInput: 0,
|
|
603
|
+
tokenOutput: 0,
|
|
604
|
+
usdCost: null,
|
|
605
|
+
usdCostStatus: "unknown",
|
|
606
|
+
pricingSource: null,
|
|
607
|
+
source: "none"
|
|
608
|
+
})
|
|
609
|
+
});
|
|
610
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
416
611
|
await db.insert(heartbeatRuns).values({
|
|
417
612
|
id: runId,
|
|
418
613
|
companyId,
|
|
419
614
|
agentId,
|
|
420
615
|
status: "skipped",
|
|
421
|
-
|
|
616
|
+
finishedAt: new Date(),
|
|
617
|
+
message: runListMessage
|
|
422
618
|
});
|
|
423
619
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
424
620
|
companyId,
|
|
425
621
|
runId,
|
|
426
622
|
status: "skipped",
|
|
427
|
-
message:
|
|
623
|
+
message: runListMessage,
|
|
624
|
+
finishedAt: new Date()
|
|
625
|
+
});
|
|
626
|
+
await appendAuditEvent(db, {
|
|
627
|
+
companyId,
|
|
628
|
+
actorType: "system",
|
|
629
|
+
eventType: "heartbeat.failed",
|
|
630
|
+
entityType: "heartbeat_run",
|
|
631
|
+
entityId: runId,
|
|
632
|
+
correlationId: options?.requestId ?? runId,
|
|
633
|
+
payload: {
|
|
634
|
+
agentId,
|
|
635
|
+
issueIds: [],
|
|
636
|
+
result: runReport.resultSummary,
|
|
637
|
+
message: runListMessage,
|
|
638
|
+
errorType: runReport.completionReason,
|
|
639
|
+
errorMessage: budgetMessage,
|
|
640
|
+
report: runReport,
|
|
641
|
+
outcome: null,
|
|
642
|
+
usage: {
|
|
643
|
+
tokenInput: 0,
|
|
644
|
+
tokenOutput: 0,
|
|
645
|
+
usdCostStatus: "unknown",
|
|
646
|
+
source: "none"
|
|
647
|
+
},
|
|
648
|
+
trace: null,
|
|
649
|
+
diagnostics: { requestId: options?.requestId, trigger: runTrigger }
|
|
650
|
+
}
|
|
428
651
|
});
|
|
429
652
|
}
|
|
430
653
|
|
|
@@ -518,6 +741,13 @@ export async function runHeartbeatForAgent(
|
|
|
518
741
|
let runtimeLaunchSummary: ReturnType<typeof summarizeRuntimeLaunch> | null = null;
|
|
519
742
|
let primaryIssueId: string | null = null;
|
|
520
743
|
let primaryProjectId: string | null = null;
|
|
744
|
+
let providerUsageLimitDisposition:
|
|
745
|
+
| {
|
|
746
|
+
message: string;
|
|
747
|
+
notifyBoard: boolean;
|
|
748
|
+
pauseAgent: boolean;
|
|
749
|
+
}
|
|
750
|
+
| null = null;
|
|
521
751
|
let transcriptSequence = 0;
|
|
522
752
|
let transcriptWriteQueue = Promise.resolve();
|
|
523
753
|
let transcriptLiveCount = 0;
|
|
@@ -526,6 +756,7 @@ export async function runHeartbeatForAgent(
|
|
|
526
756
|
let transcriptPersistFailureReported = false;
|
|
527
757
|
let pluginFailureSummary: string[] = [];
|
|
528
758
|
const seenResultMessages = new Set<string>();
|
|
759
|
+
const runDigestSignals: RunDigestSignal[] = [];
|
|
529
760
|
|
|
530
761
|
const enqueueTranscriptEvent = (event: {
|
|
531
762
|
kind: string;
|
|
@@ -553,6 +784,21 @@ export async function runHeartbeatForAgent(
|
|
|
553
784
|
if (signalLevel === "high") {
|
|
554
785
|
transcriptLiveHighSignalCount += 1;
|
|
555
786
|
}
|
|
787
|
+
if (isUsefulTranscriptSignal(signalLevel)) {
|
|
788
|
+
runDigestSignals.push({
|
|
789
|
+
sequence,
|
|
790
|
+
kind: normalizeTranscriptKind(event.kind),
|
|
791
|
+
label: event.label ?? null,
|
|
792
|
+
text: event.text ?? null,
|
|
793
|
+
payload: event.payload ?? null,
|
|
794
|
+
signalLevel,
|
|
795
|
+
groupKey: groupKey ?? null,
|
|
796
|
+
source
|
|
797
|
+
});
|
|
798
|
+
if (runDigestSignals.length > 200) {
|
|
799
|
+
runDigestSignals.splice(0, runDigestSignals.length - 200);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
556
802
|
transcriptWriteQueue = transcriptWriteQueue
|
|
557
803
|
.then(async () => {
|
|
558
804
|
await appendHeartbeatRunMessages(db, {
|
|
@@ -652,6 +898,7 @@ export async function runHeartbeatForAgent(
|
|
|
652
898
|
failClosed: false
|
|
653
899
|
});
|
|
654
900
|
const isCommentOrderWake = options?.wakeContext?.reason === "issue_comment_recipient";
|
|
901
|
+
const heartbeatIdlePolicy = resolveHeartbeatIdlePolicy();
|
|
655
902
|
const workItems = isCommentOrderWake ? [] : await claimIssuesForAgent(db, companyId, agentId, runId);
|
|
656
903
|
const wakeWorkItems = await loadWakeContextWorkItems(db, companyId, options?.wakeContext?.issueIds);
|
|
657
904
|
const contextWorkItems = resolveExecutionWorkItems(workItems, wakeWorkItems, options?.wakeContext);
|
|
@@ -730,6 +977,7 @@ export async function runHeartbeatForAgent(
|
|
|
730
977
|
contextWorkItems,
|
|
731
978
|
mergedRuntime
|
|
732
979
|
);
|
|
980
|
+
await mkdir(join(resolveAgentFallbackWorkspace(companyId, agent.id), "operating"), { recursive: true });
|
|
733
981
|
state = {
|
|
734
982
|
...state,
|
|
735
983
|
runtime: workspaceResolution.runtime
|
|
@@ -765,6 +1013,10 @@ export async function runHeartbeatForAgent(
|
|
|
765
1013
|
...context,
|
|
766
1014
|
memoryContext
|
|
767
1015
|
};
|
|
1016
|
+
const isIdleNoWork = contextWorkItems.length === 0 && !isCommentOrderWake;
|
|
1017
|
+
if (heartbeatIdlePolicy === "micro_prompt" && isIdleNoWork) {
|
|
1018
|
+
context = { ...context, idleMicroPrompt: true };
|
|
1019
|
+
}
|
|
768
1020
|
if (workspaceResolution.warnings.length > 0) {
|
|
769
1021
|
await appendAuditEvent(db, {
|
|
770
1022
|
companyId,
|
|
@@ -930,20 +1182,48 @@ export async function runHeartbeatForAgent(
|
|
|
930
1182
|
};
|
|
931
1183
|
}
|
|
932
1184
|
|
|
933
|
-
const execution =
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1185
|
+
const execution: AdapterExecutionResult =
|
|
1186
|
+
heartbeatIdlePolicy === "skip_adapter" && isIdleNoWork
|
|
1187
|
+
? {
|
|
1188
|
+
status: "ok",
|
|
1189
|
+
summary:
|
|
1190
|
+
"Idle heartbeat: no assigned work items; adapter not invoked (BOPO_HEARTBEAT_IDLE_POLICY=skip_adapter).",
|
|
1191
|
+
tokenInput: 0,
|
|
1192
|
+
tokenOutput: 0,
|
|
1193
|
+
usdCost: 0,
|
|
1194
|
+
usage: {
|
|
1195
|
+
inputTokens: 0,
|
|
1196
|
+
cachedInputTokens: 0,
|
|
1197
|
+
outputTokens: 0
|
|
1198
|
+
}
|
|
940
1199
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1200
|
+
: await executeAdapterWithWatchdog({
|
|
1201
|
+
execute: (abortSignal) =>
|
|
1202
|
+
adapter.execute({
|
|
1203
|
+
...context,
|
|
1204
|
+
runtime: {
|
|
1205
|
+
...(context.runtime ?? {}),
|
|
1206
|
+
abortSignal
|
|
1207
|
+
}
|
|
1208
|
+
}),
|
|
1209
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
1210
|
+
runtime: workspaceResolution.runtime,
|
|
1211
|
+
externalAbortSignal: activeRunAbort.signal
|
|
1212
|
+
});
|
|
1213
|
+
const usageLimitHint = execution.dispositionHint?.kind === "provider_usage_limited" ? execution.dispositionHint : null;
|
|
1214
|
+
if (usageLimitHint) {
|
|
1215
|
+
providerUsageLimitDisposition = {
|
|
1216
|
+
message: usageLimitHint.message,
|
|
1217
|
+
notifyBoard: usageLimitHint.notifyBoard,
|
|
1218
|
+
pauseAgent: usageLimitHint.pauseAgent
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
executionSummary =
|
|
1222
|
+
usageLimitHint?.message && usageLimitHint.message.trim().length > 0 ? usageLimitHint.message.trim() : execution.summary;
|
|
1223
|
+
executionSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(executionSummary));
|
|
1224
|
+
const persistedExecutionStatus: "ok" | "failed" | "skipped" = usageLimitHint ? "skipped" : execution.status;
|
|
1225
|
+
const persistedRunStatus: "completed" | "failed" | "skipped" =
|
|
1226
|
+
persistedExecutionStatus === "ok" ? "completed" : persistedExecutionStatus;
|
|
947
1227
|
const normalizedUsage = execution.usage ?? {
|
|
948
1228
|
inputTokens: Math.max(0, execution.tokenInput),
|
|
949
1229
|
cachedInputTokens: 0,
|
|
@@ -972,7 +1252,6 @@ export async function runHeartbeatForAgent(
|
|
|
972
1252
|
if (afterAdapterHook.failures.length > 0) {
|
|
973
1253
|
pluginFailureSummary = [...pluginFailureSummary, ...afterAdapterHook.failures];
|
|
974
1254
|
}
|
|
975
|
-
emitCanonicalResultEvent(executionSummary, "completed");
|
|
976
1255
|
executionTrace = execution.trace ?? null;
|
|
977
1256
|
const runtimeModelId = resolveRuntimeModelId({
|
|
978
1257
|
runtimeModel: persistedRuntime.runtimeModel,
|
|
@@ -983,6 +1262,7 @@ export async function runHeartbeatForAgent(
|
|
|
983
1262
|
const costDecision = await appendFinishedRunCostEntry({
|
|
984
1263
|
db,
|
|
985
1264
|
companyId,
|
|
1265
|
+
runId,
|
|
986
1266
|
providerType: agent.providerType,
|
|
987
1267
|
runtimeModelId: effectivePricingModelId ?? runtimeModelId,
|
|
988
1268
|
pricingProviderType: effectivePricingProviderType,
|
|
@@ -994,7 +1274,7 @@ export async function runHeartbeatForAgent(
|
|
|
994
1274
|
issueId: primaryIssueId,
|
|
995
1275
|
projectId: primaryProjectId,
|
|
996
1276
|
agentId,
|
|
997
|
-
status:
|
|
1277
|
+
status: persistedExecutionStatus
|
|
998
1278
|
});
|
|
999
1279
|
const executionUsdCost = costDecision.usdCost;
|
|
1000
1280
|
await appendProjectBudgetUsage(db, {
|
|
@@ -1007,8 +1287,8 @@ export async function runHeartbeatForAgent(
|
|
|
1007
1287
|
companyId,
|
|
1008
1288
|
agentId,
|
|
1009
1289
|
runId,
|
|
1010
|
-
status:
|
|
1011
|
-
summary:
|
|
1290
|
+
status: persistedExecutionStatus === "ok" ? "ok" : "failed",
|
|
1291
|
+
summary: executionSummary,
|
|
1012
1292
|
outcomeKind: executionOutcome?.kind ?? null,
|
|
1013
1293
|
mission: context.company.mission ?? null,
|
|
1014
1294
|
goalContext: {
|
|
@@ -1030,7 +1310,7 @@ export async function runHeartbeatForAgent(
|
|
|
1030
1310
|
candidateFacts: persistedMemory.candidateFacts
|
|
1031
1311
|
}
|
|
1032
1312
|
});
|
|
1033
|
-
if (execution.status === "ok") {
|
|
1313
|
+
if (execution.status === "ok" && !usageLimitHint) {
|
|
1034
1314
|
for (const fact of persistedMemory.candidateFacts) {
|
|
1035
1315
|
const targetFile = await appendDurableFact({
|
|
1036
1316
|
companyId,
|
|
@@ -1054,7 +1334,7 @@ export async function runHeartbeatForAgent(
|
|
|
1054
1334
|
}
|
|
1055
1335
|
}
|
|
1056
1336
|
const missionAlignment = computeMissionAlignmentSignal({
|
|
1057
|
-
summary:
|
|
1337
|
+
summary: executionSummary,
|
|
1058
1338
|
mission: context.company.mission ?? null,
|
|
1059
1339
|
companyGoals: context.goalContext?.companyGoals ?? [],
|
|
1060
1340
|
projectGoals: context.goalContext?.projectGoals ?? []
|
|
@@ -1079,7 +1359,7 @@ export async function runHeartbeatForAgent(
|
|
|
1079
1359
|
executionUsdCost > 0 ||
|
|
1080
1360
|
effectiveTokenInput > 0 ||
|
|
1081
1361
|
effectiveTokenOutput > 0 ||
|
|
1082
|
-
|
|
1362
|
+
persistedExecutionStatus !== "skipped"
|
|
1083
1363
|
) {
|
|
1084
1364
|
await db
|
|
1085
1365
|
.update(agents)
|
|
@@ -1157,8 +1437,8 @@ export async function runHeartbeatForAgent(
|
|
|
1157
1437
|
runId,
|
|
1158
1438
|
requestId: options?.requestId,
|
|
1159
1439
|
providerType: agent.providerType,
|
|
1160
|
-
status:
|
|
1161
|
-
summary:
|
|
1440
|
+
status: persistedExecutionStatus,
|
|
1441
|
+
summary: executionSummary
|
|
1162
1442
|
},
|
|
1163
1443
|
failClosed: false
|
|
1164
1444
|
});
|
|
@@ -1166,29 +1446,75 @@ export async function runHeartbeatForAgent(
|
|
|
1166
1446
|
pluginFailureSummary = [...pluginFailureSummary, ...beforePersistHook.failures];
|
|
1167
1447
|
}
|
|
1168
1448
|
|
|
1449
|
+
const runDigest = buildRunDigest({
|
|
1450
|
+
status: persistedRunStatus,
|
|
1451
|
+
executionSummary,
|
|
1452
|
+
outcome: executionOutcome,
|
|
1453
|
+
trace: executionTrace,
|
|
1454
|
+
signals: runDigestSignals
|
|
1455
|
+
});
|
|
1456
|
+
const terminalPresentation = resolveRunTerminalPresentation({
|
|
1457
|
+
internalStatus: persistedRunStatus,
|
|
1458
|
+
executionSummary,
|
|
1459
|
+
outcome: executionOutcome,
|
|
1460
|
+
trace: executionTrace
|
|
1461
|
+
});
|
|
1462
|
+
const runCost = buildRunCostSummary({
|
|
1463
|
+
tokenInput: effectiveTokenInput,
|
|
1464
|
+
tokenOutput: effectiveTokenOutput,
|
|
1465
|
+
usdCost: costDecision.usdCostStatus === "unknown" ? null : executionUsdCost,
|
|
1466
|
+
usdCostStatus: costDecision.usdCostStatus,
|
|
1467
|
+
pricingSource: costDecision.pricingSource ?? null,
|
|
1468
|
+
source: readTraceString(execution.trace, "usageSource") ?? "unknown"
|
|
1469
|
+
});
|
|
1470
|
+
const runReport = buildRunCompletionReport({
|
|
1471
|
+
companyId,
|
|
1472
|
+
agentName: agent.name,
|
|
1473
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
1474
|
+
issueIds,
|
|
1475
|
+
executionSummary,
|
|
1476
|
+
outcome: executionOutcome,
|
|
1477
|
+
finalRunOutput: execution.finalRunOutput ?? null,
|
|
1478
|
+
trace: executionTrace,
|
|
1479
|
+
digest: runDigest,
|
|
1480
|
+
terminal: terminalPresentation,
|
|
1481
|
+
cost: runCost,
|
|
1482
|
+
runtimeCwd: workspaceResolution.runtime.cwd
|
|
1483
|
+
});
|
|
1484
|
+
await verifyRunArtifactsOnDisk(companyId, runReport.artifacts);
|
|
1485
|
+
emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
|
|
1486
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
1169
1487
|
await db
|
|
1170
1488
|
.update(heartbeatRuns)
|
|
1171
1489
|
.set({
|
|
1172
|
-
status:
|
|
1490
|
+
status: persistedRunStatus,
|
|
1173
1491
|
finishedAt: new Date(),
|
|
1174
|
-
message:
|
|
1492
|
+
message: runListMessage
|
|
1175
1493
|
})
|
|
1176
1494
|
.where(eq(heartbeatRuns.id, runId));
|
|
1177
1495
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
1178
1496
|
companyId,
|
|
1179
1497
|
runId,
|
|
1180
|
-
status:
|
|
1181
|
-
message:
|
|
1498
|
+
status: persistedRunStatus,
|
|
1499
|
+
message: runListMessage,
|
|
1182
1500
|
finishedAt: new Date()
|
|
1183
1501
|
});
|
|
1502
|
+
await appendAuditEvent(db, {
|
|
1503
|
+
companyId,
|
|
1504
|
+
actorType: "system",
|
|
1505
|
+
eventType: "heartbeat.run_digest",
|
|
1506
|
+
entityType: "heartbeat_run",
|
|
1507
|
+
entityId: runId,
|
|
1508
|
+
correlationId: options?.requestId ?? runId,
|
|
1509
|
+
payload: runDigest
|
|
1510
|
+
});
|
|
1184
1511
|
try {
|
|
1185
1512
|
await appendRunSummaryComments(db, {
|
|
1186
1513
|
companyId,
|
|
1187
1514
|
issueIds,
|
|
1188
1515
|
agentId,
|
|
1189
1516
|
runId,
|
|
1190
|
-
|
|
1191
|
-
executionSummary: execution.summary
|
|
1517
|
+
report: runReport
|
|
1192
1518
|
});
|
|
1193
1519
|
} catch (commentError) {
|
|
1194
1520
|
await appendAuditEvent(db, {
|
|
@@ -1209,6 +1535,7 @@ export async function runHeartbeatForAgent(
|
|
|
1209
1535
|
const fallbackMessages = normalizeTraceTranscript(executionTrace);
|
|
1210
1536
|
const fallbackHighSignalCount = fallbackMessages.filter((message) => message.signalLevel === "high").length;
|
|
1211
1537
|
const shouldAppendFallback =
|
|
1538
|
+
!providerUsageLimitDisposition &&
|
|
1212
1539
|
fallbackMessages.length > 0 &&
|
|
1213
1540
|
(transcriptLiveCount === 0 ||
|
|
1214
1541
|
transcriptLiveUsefulCount < 2 ||
|
|
@@ -1253,6 +1580,24 @@ export async function runHeartbeatForAgent(
|
|
|
1253
1580
|
source: "trace_fallback",
|
|
1254
1581
|
createdAt
|
|
1255
1582
|
}));
|
|
1583
|
+
for (const row of rows) {
|
|
1584
|
+
if (!isUsefulTranscriptSignal(row.signalLevel)) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
runDigestSignals.push({
|
|
1588
|
+
sequence: row.sequence,
|
|
1589
|
+
kind: row.kind,
|
|
1590
|
+
label: row.label,
|
|
1591
|
+
text: row.text,
|
|
1592
|
+
payload: row.payloadJson,
|
|
1593
|
+
signalLevel: row.signalLevel,
|
|
1594
|
+
groupKey: row.groupKey,
|
|
1595
|
+
source: "trace_fallback"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
if (runDigestSignals.length > 200) {
|
|
1599
|
+
runDigestSignals.splice(0, runDigestSignals.length - 200);
|
|
1600
|
+
}
|
|
1256
1601
|
await appendHeartbeatRunMessages(db, {
|
|
1257
1602
|
companyId,
|
|
1258
1603
|
runId,
|
|
@@ -1287,8 +1632,8 @@ export async function runHeartbeatForAgent(
|
|
|
1287
1632
|
runId,
|
|
1288
1633
|
requestId: options?.requestId,
|
|
1289
1634
|
providerType: agent.providerType,
|
|
1290
|
-
status:
|
|
1291
|
-
summary:
|
|
1635
|
+
status: persistedExecutionStatus,
|
|
1636
|
+
summary: executionSummary,
|
|
1292
1637
|
trace: executionTrace,
|
|
1293
1638
|
outcome: executionOutcome
|
|
1294
1639
|
},
|
|
@@ -1298,6 +1643,48 @@ export async function runHeartbeatForAgent(
|
|
|
1298
1643
|
pluginFailureSummary = [...pluginFailureSummary, ...afterPersistHook.failures];
|
|
1299
1644
|
}
|
|
1300
1645
|
|
|
1646
|
+
if (providerUsageLimitDisposition) {
|
|
1647
|
+
await appendAuditEvent(db, {
|
|
1648
|
+
companyId,
|
|
1649
|
+
actorType: "system",
|
|
1650
|
+
eventType: "heartbeat.provider_usage_limited",
|
|
1651
|
+
entityType: "heartbeat_run",
|
|
1652
|
+
entityId: runId,
|
|
1653
|
+
correlationId: options?.requestId ?? runId,
|
|
1654
|
+
payload: {
|
|
1655
|
+
agentId,
|
|
1656
|
+
providerType: agent.providerType,
|
|
1657
|
+
issueIds,
|
|
1658
|
+
message: providerUsageLimitDisposition.message
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
const pauseResult = providerUsageLimitDisposition.pauseAgent
|
|
1662
|
+
? await pauseAgentForProviderUsageLimit(db, {
|
|
1663
|
+
companyId,
|
|
1664
|
+
agentId,
|
|
1665
|
+
requestId: options?.requestId ?? runId,
|
|
1666
|
+
runId,
|
|
1667
|
+
providerType: agent.providerType,
|
|
1668
|
+
message: providerUsageLimitDisposition.message
|
|
1669
|
+
})
|
|
1670
|
+
: { paused: false };
|
|
1671
|
+
if (providerUsageLimitDisposition.notifyBoard) {
|
|
1672
|
+
await appendProviderUsageLimitBoardComments(db, {
|
|
1673
|
+
companyId,
|
|
1674
|
+
issueIds,
|
|
1675
|
+
agentId,
|
|
1676
|
+
runId,
|
|
1677
|
+
providerType: agent.providerType,
|
|
1678
|
+
message: providerUsageLimitDisposition.message,
|
|
1679
|
+
paused: pauseResult.paused
|
|
1680
|
+
});
|
|
1681
|
+
if (options?.realtimeHub) {
|
|
1682
|
+
await publishAttentionSnapshot(db, options.realtimeHub, companyId);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
await publishOfficeOccupantForAgent(db, options?.realtimeHub, companyId, agentId);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1301
1688
|
await appendAuditEvent(db, {
|
|
1302
1689
|
companyId,
|
|
1303
1690
|
actorType: "system",
|
|
@@ -1307,14 +1694,17 @@ export async function runHeartbeatForAgent(
|
|
|
1307
1694
|
correlationId: options?.requestId ?? runId,
|
|
1308
1695
|
payload: {
|
|
1309
1696
|
agentId,
|
|
1310
|
-
|
|
1311
|
-
|
|
1697
|
+
status: persistedRunStatus,
|
|
1698
|
+
result: runReport.resultSummary,
|
|
1699
|
+
message: runListMessage,
|
|
1700
|
+
report: runReport,
|
|
1312
1701
|
outcome: executionOutcome,
|
|
1313
1702
|
issueIds,
|
|
1314
1703
|
usage: {
|
|
1315
1704
|
tokenInput: effectiveTokenInput,
|
|
1316
1705
|
tokenOutput: effectiveTokenOutput,
|
|
1317
1706
|
usdCost: executionUsdCost,
|
|
1707
|
+
usdCostStatus: costDecision.usdCostStatus,
|
|
1318
1708
|
source: readTraceString(execution.trace, "usageSource") ?? "unknown"
|
|
1319
1709
|
},
|
|
1320
1710
|
trace: execution.trace ?? null,
|
|
@@ -1408,6 +1798,7 @@ export async function runHeartbeatForAgent(
|
|
|
1408
1798
|
const failureCostDecision = await appendFinishedRunCostEntry({
|
|
1409
1799
|
db,
|
|
1410
1800
|
companyId,
|
|
1801
|
+
runId,
|
|
1411
1802
|
providerType: agent.providerType,
|
|
1412
1803
|
runtimeModelId,
|
|
1413
1804
|
pricingProviderType: agent.providerType,
|
|
@@ -1423,29 +1814,77 @@ export async function runHeartbeatForAgent(
|
|
|
1423
1814
|
companyId,
|
|
1424
1815
|
projectCostsUsd: buildProjectBudgetCostAllocations(executionWorkItemsForBudget, failureCostDecision.usdCost)
|
|
1425
1816
|
});
|
|
1817
|
+
const runDigest = buildRunDigest({
|
|
1818
|
+
status: "failed",
|
|
1819
|
+
executionSummary,
|
|
1820
|
+
outcome: executionOutcome,
|
|
1821
|
+
trace: executionTrace,
|
|
1822
|
+
signals: runDigestSignals
|
|
1823
|
+
});
|
|
1824
|
+
const runCost = buildRunCostSummary({
|
|
1825
|
+
tokenInput: 0,
|
|
1826
|
+
tokenOutput: 0,
|
|
1827
|
+
usdCost: failureCostDecision.usdCostStatus === "unknown" ? null : failureCostDecision.usdCost,
|
|
1828
|
+
usdCostStatus: failureCostDecision.usdCostStatus,
|
|
1829
|
+
pricingSource: failureCostDecision.pricingSource ?? null,
|
|
1830
|
+
source: readTraceString(executionTrace, "usageSource") ?? "unknown"
|
|
1831
|
+
});
|
|
1832
|
+
const runReport = buildRunCompletionReport({
|
|
1833
|
+
companyId,
|
|
1834
|
+
agentName: agent.name,
|
|
1835
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
1836
|
+
issueIds,
|
|
1837
|
+
executionSummary,
|
|
1838
|
+
outcome: executionOutcome,
|
|
1839
|
+
finalRunOutput: null,
|
|
1840
|
+
trace: executionTrace,
|
|
1841
|
+
digest: runDigest,
|
|
1842
|
+
terminal: resolveRunTerminalPresentation({
|
|
1843
|
+
internalStatus: "failed",
|
|
1844
|
+
executionSummary,
|
|
1845
|
+
outcome: executionOutcome,
|
|
1846
|
+
trace: executionTrace,
|
|
1847
|
+
errorType: classified.type
|
|
1848
|
+
}),
|
|
1849
|
+
cost: runCost,
|
|
1850
|
+
runtimeCwd: runtimeLaunchSummary?.cwd ?? persistedRuntime.runtimeCwd ?? null,
|
|
1851
|
+
errorType: classified.type,
|
|
1852
|
+
errorMessage: classified.message
|
|
1853
|
+
});
|
|
1854
|
+
await verifyRunArtifactsOnDisk(companyId, runReport.artifacts);
|
|
1855
|
+
const runListMessage = buildRunListMessageFromReport(runReport);
|
|
1426
1856
|
await db
|
|
1427
1857
|
.update(heartbeatRuns)
|
|
1428
1858
|
.set({
|
|
1429
1859
|
status: "failed",
|
|
1430
1860
|
finishedAt: new Date(),
|
|
1431
|
-
message:
|
|
1861
|
+
message: runListMessage
|
|
1432
1862
|
})
|
|
1433
1863
|
.where(eq(heartbeatRuns.id, runId));
|
|
1434
1864
|
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
1435
1865
|
companyId,
|
|
1436
1866
|
runId,
|
|
1437
1867
|
status: "failed",
|
|
1438
|
-
message:
|
|
1868
|
+
message: runListMessage,
|
|
1439
1869
|
finishedAt: new Date()
|
|
1440
1870
|
});
|
|
1871
|
+
emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
|
|
1872
|
+
await appendAuditEvent(db, {
|
|
1873
|
+
companyId,
|
|
1874
|
+
actorType: "system",
|
|
1875
|
+
eventType: "heartbeat.run_digest",
|
|
1876
|
+
entityType: "heartbeat_run",
|
|
1877
|
+
entityId: runId,
|
|
1878
|
+
correlationId: options?.requestId ?? runId,
|
|
1879
|
+
payload: runDigest
|
|
1880
|
+
});
|
|
1441
1881
|
try {
|
|
1442
1882
|
await appendRunSummaryComments(db, {
|
|
1443
1883
|
companyId,
|
|
1444
1884
|
issueIds,
|
|
1445
1885
|
agentId,
|
|
1446
1886
|
runId,
|
|
1447
|
-
|
|
1448
|
-
executionSummary
|
|
1887
|
+
report: runReport
|
|
1449
1888
|
});
|
|
1450
1889
|
} catch (commentError) {
|
|
1451
1890
|
await appendAuditEvent(db, {
|
|
@@ -1472,12 +1911,17 @@ export async function runHeartbeatForAgent(
|
|
|
1472
1911
|
payload: {
|
|
1473
1912
|
agentId,
|
|
1474
1913
|
issueIds,
|
|
1475
|
-
result:
|
|
1476
|
-
message:
|
|
1914
|
+
result: runReport.resultSummary,
|
|
1915
|
+
message: runListMessage,
|
|
1477
1916
|
errorType: classified.type,
|
|
1478
1917
|
errorMessage: classified.message,
|
|
1918
|
+
report: runReport,
|
|
1479
1919
|
outcome: executionOutcome,
|
|
1480
1920
|
usage: {
|
|
1921
|
+
tokenInput: 0,
|
|
1922
|
+
tokenOutput: 0,
|
|
1923
|
+
usdCost: failureCostDecision.usdCost,
|
|
1924
|
+
usdCostStatus: failureCostDecision.usdCostStatus,
|
|
1481
1925
|
source: readTraceString(executionTrace, "usageSource") ?? "unknown"
|
|
1482
1926
|
},
|
|
1483
1927
|
trace: executionTrace,
|
|
@@ -1942,6 +2386,7 @@ async function buildHeartbeatContext(
|
|
|
1942
2386
|
fileSizeBytes: number;
|
|
1943
2387
|
relativePath: string;
|
|
1944
2388
|
absolutePath: string;
|
|
2389
|
+
downloadPath: string;
|
|
1945
2390
|
}>
|
|
1946
2391
|
>();
|
|
1947
2392
|
for (const row of attachmentRows) {
|
|
@@ -1957,7 +2402,8 @@ async function buildHeartbeatContext(
|
|
|
1957
2402
|
mimeType: row.mimeType,
|
|
1958
2403
|
fileSizeBytes: row.fileSizeBytes,
|
|
1959
2404
|
relativePath: row.relativePath,
|
|
1960
|
-
absolutePath
|
|
2405
|
+
absolutePath,
|
|
2406
|
+
downloadPath: `/issues/${row.issueId}/attachments/${row.id}/download`
|
|
1961
2407
|
});
|
|
1962
2408
|
attachmentsByIssue.set(row.issueId, existing);
|
|
1963
2409
|
}
|
|
@@ -1985,12 +2431,14 @@ async function buildHeartbeatContext(
|
|
|
1985
2431
|
.filter((goal) => goal.status === "active" && goal.level === "agent")
|
|
1986
2432
|
.map((goal) => goal.title);
|
|
1987
2433
|
const isCommentOrderWake = input.wakeContext?.reason === "issue_comment_recipient";
|
|
2434
|
+
const promptMode = resolveHeartbeatPromptMode();
|
|
1988
2435
|
|
|
1989
2436
|
return {
|
|
1990
2437
|
companyId,
|
|
1991
2438
|
agentId: input.agentId,
|
|
1992
2439
|
providerType: input.providerType,
|
|
1993
2440
|
heartbeatRunId: input.heartbeatRunId,
|
|
2441
|
+
promptMode,
|
|
1994
2442
|
company: {
|
|
1995
2443
|
name: company?.name ?? "Unknown company",
|
|
1996
2444
|
mission: company?.mission ?? null
|
|
@@ -2290,16 +2738,6 @@ function sanitizeAgentSummaryCommentBody(body: string) {
|
|
|
2290
2738
|
return sanitized.length > 0 ? sanitized : "Run update.";
|
|
2291
2739
|
}
|
|
2292
2740
|
|
|
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
2741
|
function extractNaturalRunUpdate(executionSummary: string) {
|
|
2304
2742
|
const normalized = executionSummary.trim();
|
|
2305
2743
|
const jsonSummary = extractSummaryFromJsonLikeText(normalized);
|
|
@@ -2323,6 +2761,697 @@ function extractNaturalRunUpdate(executionSummary: string) {
|
|
|
2323
2761
|
return /[.!?]$/.test(bounded) ? bounded : `${bounded}.`;
|
|
2324
2762
|
}
|
|
2325
2763
|
|
|
2764
|
+
function buildRunDigest(input: {
|
|
2765
|
+
status: "completed" | "failed" | "skipped";
|
|
2766
|
+
executionSummary: string;
|
|
2767
|
+
outcome: ExecutionOutcome | null;
|
|
2768
|
+
trace: unknown;
|
|
2769
|
+
signals: RunDigestSignal[];
|
|
2770
|
+
}): RunDigest {
|
|
2771
|
+
const summary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
|
|
2772
|
+
const successes: string[] = [];
|
|
2773
|
+
const failures: string[] = [];
|
|
2774
|
+
const blockers: string[] = [];
|
|
2775
|
+
if (input.outcome) {
|
|
2776
|
+
for (const action of input.outcome.actions) {
|
|
2777
|
+
const detail = summarizeRunDigestPoint(action.detail);
|
|
2778
|
+
if (!detail) {
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
if (action.status === "ok") {
|
|
2782
|
+
successes.push(detail);
|
|
2783
|
+
} else if (action.status === "error") {
|
|
2784
|
+
failures.push(detail);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
for (const blocker of input.outcome.blockers) {
|
|
2788
|
+
const detail = summarizeRunDigestPoint(blocker.message);
|
|
2789
|
+
if (detail) {
|
|
2790
|
+
blockers.push(detail);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
for (const signal of input.signals) {
|
|
2795
|
+
if (signal.signalLevel !== "high" && signal.signalLevel !== "medium") {
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
const signalText = summarizeRunDigestPoint(signal.text ?? signal.payload ?? "");
|
|
2799
|
+
if (!signalText) {
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
if (signal.kind === "tool_result" || signal.kind === "stderr") {
|
|
2803
|
+
if (looksLikeRunFailureSignal(signalText)) {
|
|
2804
|
+
failures.push(signalText);
|
|
2805
|
+
} else if (signal.kind === "tool_result") {
|
|
2806
|
+
successes.push(signalText);
|
|
2807
|
+
}
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (signal.kind === "result" && !looksLikeRunFailureSignal(signalText)) {
|
|
2811
|
+
successes.push(signalText);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
if (input.status === "completed" && successes.length === 0) {
|
|
2815
|
+
successes.push(summary);
|
|
2816
|
+
}
|
|
2817
|
+
if (input.status === "failed" && failures.length === 0) {
|
|
2818
|
+
failures.push(summary);
|
|
2819
|
+
}
|
|
2820
|
+
if (input.status === "failed" && blockers.length === 0) {
|
|
2821
|
+
const traceFailureType = summarizeRunDigestPoint(readTraceString(input.trace, "failureType") ?? "");
|
|
2822
|
+
if (traceFailureType) {
|
|
2823
|
+
blockers.push(`failure type: ${traceFailureType}`);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const uniqueSuccesses = dedupeRunDigestPoints(successes, 3);
|
|
2827
|
+
const uniqueFailures = dedupeRunDigestPoints(failures, 3);
|
|
2828
|
+
const uniqueBlockers = dedupeRunDigestPoints(blockers, 2);
|
|
2829
|
+
const headline =
|
|
2830
|
+
input.status === "completed"
|
|
2831
|
+
? `Run completed: ${summary}`
|
|
2832
|
+
: input.status === "failed"
|
|
2833
|
+
? `Run failed: ${summary}`
|
|
2834
|
+
: `Run skipped: ${summary}`;
|
|
2835
|
+
const nextAction = resolveRunDigestNextAction({
|
|
2836
|
+
status: input.status,
|
|
2837
|
+
blockers: uniqueBlockers,
|
|
2838
|
+
failures: uniqueFailures
|
|
2839
|
+
});
|
|
2840
|
+
return {
|
|
2841
|
+
status: input.status,
|
|
2842
|
+
headline,
|
|
2843
|
+
summary,
|
|
2844
|
+
successes: uniqueSuccesses,
|
|
2845
|
+
failures: uniqueFailures,
|
|
2846
|
+
blockers: uniqueBlockers,
|
|
2847
|
+
nextAction,
|
|
2848
|
+
evidence: {
|
|
2849
|
+
transcriptSignalCount: input.signals.length,
|
|
2850
|
+
outcomeActionCount: input.outcome?.actions.length ?? 0,
|
|
2851
|
+
outcomeBlockerCount: input.outcome?.blockers.length ?? 0,
|
|
2852
|
+
failureType: readTraceString(input.trace, "failureType")
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
function summarizeRunDigestPoint(value: string | null | undefined) {
|
|
2858
|
+
if (!value) {
|
|
2859
|
+
return "";
|
|
2860
|
+
}
|
|
2861
|
+
const normalized = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(value));
|
|
2862
|
+
if (!normalized || normalized.toLowerCase() === "run update.") {
|
|
2863
|
+
return "";
|
|
2864
|
+
}
|
|
2865
|
+
const bounded = normalized.length > 180 ? `${normalized.slice(0, 177).trimEnd()}...` : normalized;
|
|
2866
|
+
return bounded;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
function dedupeRunDigestPoints(values: string[], limit: number) {
|
|
2870
|
+
const seen = new Set<string>();
|
|
2871
|
+
const deduped: string[] = [];
|
|
2872
|
+
for (const value of values) {
|
|
2873
|
+
const key = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2874
|
+
if (!key || seen.has(key)) {
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
seen.add(key);
|
|
2878
|
+
deduped.push(value);
|
|
2879
|
+
if (deduped.length >= limit) {
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return deduped;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function looksLikeRunFailureSignal(value: string) {
|
|
2887
|
+
const normalized = value.toLowerCase();
|
|
2888
|
+
return /(failed|error|exception|timed out|timeout|unauthorized|not supported|unsupported|no capacity|rate limit|429|500|blocked|unable to)/.test(
|
|
2889
|
+
normalized
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
function resolveRunDigestNextAction(input: { status: "completed" | "failed" | "skipped"; blockers: string[]; failures: string[] }) {
|
|
2894
|
+
if (input.status === "completed") {
|
|
2895
|
+
return "Review outputs and move the issue to the next workflow state.";
|
|
2896
|
+
}
|
|
2897
|
+
const combined = [...input.blockers, ...input.failures].join(" ").toLowerCase();
|
|
2898
|
+
if (combined.includes("auth") || combined.includes("unauthorized") || combined.includes("login")) {
|
|
2899
|
+
return "Fix credentials/authentication, then rerun.";
|
|
2900
|
+
}
|
|
2901
|
+
if (combined.includes("model") && (combined.includes("not supported") || combined.includes("unavailable"))) {
|
|
2902
|
+
return "Select a supported model and rerun.";
|
|
2903
|
+
}
|
|
2904
|
+
if (combined.includes("usage limit") || combined.includes("rate limit") || combined.includes("no capacity")) {
|
|
2905
|
+
return "Retry after provider quota/capacity recovers.";
|
|
2906
|
+
}
|
|
2907
|
+
return "Fix listed failures/blockers and rerun.";
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
function resolveRunTerminalPresentation(input: {
|
|
2911
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
2912
|
+
executionSummary: string;
|
|
2913
|
+
outcome: ExecutionOutcome | null;
|
|
2914
|
+
trace: unknown;
|
|
2915
|
+
errorType?: string | null;
|
|
2916
|
+
}) : RunTerminalPresentation {
|
|
2917
|
+
if (isNoAssignedWorkOutcomeForReport(input.outcome)) {
|
|
2918
|
+
return {
|
|
2919
|
+
internalStatus: input.internalStatus,
|
|
2920
|
+
publicStatus: "completed",
|
|
2921
|
+
completionReason: "no_assigned_work"
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
if (input.internalStatus === "completed") {
|
|
2925
|
+
return {
|
|
2926
|
+
internalStatus: input.internalStatus,
|
|
2927
|
+
publicStatus: "completed",
|
|
2928
|
+
completionReason: "task_completed"
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
const completionReason = inferRunCompletionReason(input);
|
|
2932
|
+
return {
|
|
2933
|
+
internalStatus: input.internalStatus,
|
|
2934
|
+
publicStatus: "failed",
|
|
2935
|
+
completionReason
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
function inferRunCompletionReason(input: {
|
|
2940
|
+
internalStatus: "completed" | "failed" | "skipped";
|
|
2941
|
+
executionSummary: string;
|
|
2942
|
+
outcome: ExecutionOutcome | null;
|
|
2943
|
+
trace: unknown;
|
|
2944
|
+
errorType?: string | null;
|
|
2945
|
+
}): RunCompletionReason {
|
|
2946
|
+
const texts = [
|
|
2947
|
+
input.executionSummary,
|
|
2948
|
+
readTraceString(input.trace, "failureType") ?? "",
|
|
2949
|
+
readTraceString(input.trace, "stderrPreview") ?? "",
|
|
2950
|
+
input.errorType ?? "",
|
|
2951
|
+
...(input.outcome?.blockers ?? []).flatMap((blocker) => [blocker.code, blocker.message]),
|
|
2952
|
+
...(input.outcome?.actions ?? []).flatMap((action) => [action.type, action.detail ?? ""])
|
|
2953
|
+
];
|
|
2954
|
+
const combined = texts.join("\n").toLowerCase();
|
|
2955
|
+
if (
|
|
2956
|
+
combined.includes("insufficient_quota") ||
|
|
2957
|
+
combined.includes("billing_hard_limit_reached") ||
|
|
2958
|
+
combined.includes("out of funds") ||
|
|
2959
|
+
combined.includes("payment required")
|
|
2960
|
+
) {
|
|
2961
|
+
return "provider_out_of_funds";
|
|
2962
|
+
}
|
|
2963
|
+
if (
|
|
2964
|
+
combined.includes("usage limit") ||
|
|
2965
|
+
combined.includes("rate limit") ||
|
|
2966
|
+
combined.includes("429") ||
|
|
2967
|
+
combined.includes("quota")
|
|
2968
|
+
) {
|
|
2969
|
+
return combined.includes("quota") ? "provider_quota_exhausted" : "provider_rate_limited";
|
|
2970
|
+
}
|
|
2971
|
+
if (combined.includes("budget hard-stop")) {
|
|
2972
|
+
return "budget_hard_stop";
|
|
2973
|
+
}
|
|
2974
|
+
if (combined.includes("already in progress") || combined.includes("skipped_overlap")) {
|
|
2975
|
+
return "overlap_in_progress";
|
|
2976
|
+
}
|
|
2977
|
+
if (combined.includes("unauthorized") || combined.includes("auth") || combined.includes("api key")) {
|
|
2978
|
+
return "auth_error";
|
|
2979
|
+
}
|
|
2980
|
+
if (combined.includes("contract") || combined.includes("missing_structured_output")) {
|
|
2981
|
+
return "contract_invalid";
|
|
2982
|
+
}
|
|
2983
|
+
if (combined.includes("watchdog_timeout") || combined.includes("runtime_timeout") || combined.includes("timed out")) {
|
|
2984
|
+
return "timeout";
|
|
2985
|
+
}
|
|
2986
|
+
if (combined.includes("cancelled")) {
|
|
2987
|
+
return "cancelled";
|
|
2988
|
+
}
|
|
2989
|
+
if (combined.includes("enoent") || combined.includes("runtime_missing")) {
|
|
2990
|
+
return "runtime_missing";
|
|
2991
|
+
}
|
|
2992
|
+
if (
|
|
2993
|
+
combined.includes("provider unavailable") ||
|
|
2994
|
+
combined.includes("no capacity") ||
|
|
2995
|
+
combined.includes("unavailable") ||
|
|
2996
|
+
combined.includes("http_error")
|
|
2997
|
+
) {
|
|
2998
|
+
return "provider_unavailable";
|
|
2999
|
+
}
|
|
3000
|
+
if (input.outcome?.kind === "blocked") {
|
|
3001
|
+
return "blocked";
|
|
3002
|
+
}
|
|
3003
|
+
return "runtime_error";
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
function isNoAssignedWorkOutcomeForReport(outcome: ExecutionOutcome | null) {
|
|
3007
|
+
if (!outcome) {
|
|
3008
|
+
return false;
|
|
3009
|
+
}
|
|
3010
|
+
if (outcome.kind !== "skipped") {
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
if (outcome.issueIdsTouched.length === 0) {
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
return outcome.actions.some((action) => action.type === "heartbeat.skip");
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function buildRunCostSummary(input: {
|
|
3020
|
+
tokenInput: number;
|
|
3021
|
+
tokenOutput: number;
|
|
3022
|
+
usdCost: number | null;
|
|
3023
|
+
usdCostStatus: "exact" | "estimated" | "unknown";
|
|
3024
|
+
pricingSource: string | null;
|
|
3025
|
+
source: string | null;
|
|
3026
|
+
}): RunCostSummary {
|
|
3027
|
+
return {
|
|
3028
|
+
tokenInput: Math.max(0, input.tokenInput),
|
|
3029
|
+
tokenOutput: Math.max(0, input.tokenOutput),
|
|
3030
|
+
usdCost: input.usdCostStatus === "unknown" ? null : Math.max(0, input.usdCost ?? 0),
|
|
3031
|
+
usdCostStatus: input.usdCostStatus,
|
|
3032
|
+
pricingSource: input.pricingSource ?? null,
|
|
3033
|
+
source: input.source ?? null
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
function buildRunArtifacts(input: {
|
|
3038
|
+
outcome: ExecutionOutcome | null;
|
|
3039
|
+
finalRunOutput?: AgentFinalRunOutput | null;
|
|
3040
|
+
runtimeCwd?: string | null;
|
|
3041
|
+
workspaceRootPath?: string | null;
|
|
3042
|
+
companyId?: string;
|
|
3043
|
+
}): RunArtifact[] {
|
|
3044
|
+
const sourceArtifacts =
|
|
3045
|
+
input.finalRunOutput?.artifacts && input.finalRunOutput.artifacts.length > 0
|
|
3046
|
+
? input.finalRunOutput.artifacts
|
|
3047
|
+
: input.outcome?.artifacts ?? [];
|
|
3048
|
+
if (sourceArtifacts.length === 0) {
|
|
3049
|
+
return [];
|
|
3050
|
+
}
|
|
3051
|
+
const runtimeCwd = input.runtimeCwd?.trim() ? input.runtimeCwd.trim() : null;
|
|
3052
|
+
const workspaceRootPath = input.workspaceRootPath?.trim() ? input.workspaceRootPath.trim() : null;
|
|
3053
|
+
const companyId = input.companyId?.trim() ? input.companyId.trim() : null;
|
|
3054
|
+
return sourceArtifacts.map((artifact) => {
|
|
3055
|
+
const originalPath = artifact.path.trim();
|
|
3056
|
+
const artifactIsAbsolute = isAbsolute(originalPath);
|
|
3057
|
+
const absolutePath = artifactIsAbsolute ? resolve(originalPath) : runtimeCwd ? resolve(runtimeCwd, originalPath) : null;
|
|
3058
|
+
let relativePathValue: string | null = null;
|
|
3059
|
+
if (absolutePath && workspaceRootPath && isInsidePath(workspaceRootPath, absolutePath)) {
|
|
3060
|
+
relativePathValue = toNormalizedWorkspaceRelativePath(relative(workspaceRootPath, absolutePath));
|
|
3061
|
+
} else if (!artifactIsAbsolute) {
|
|
3062
|
+
relativePathValue = toNormalizedWorkspaceRelativePath(originalPath);
|
|
3063
|
+
} else if (runtimeCwd) {
|
|
3064
|
+
const candidate = toNormalizedWorkspaceRelativePath(relative(runtimeCwd, absolutePath ?? originalPath));
|
|
3065
|
+
relativePathValue = candidate && !candidate.startsWith("../") ? candidate : null;
|
|
3066
|
+
}
|
|
3067
|
+
if (companyId) {
|
|
3068
|
+
const normalizedRelative = normalizeAgentOperatingArtifactRelativePath(relativePathValue, companyId);
|
|
3069
|
+
if (normalizedRelative) {
|
|
3070
|
+
relativePathValue = normalizedRelative;
|
|
3071
|
+
} else {
|
|
3072
|
+
const normalizedOriginal = toNormalizedWorkspaceRelativePath(originalPath);
|
|
3073
|
+
const normalizedFromOriginal = normalizeAgentOperatingArtifactRelativePath(normalizedOriginal, companyId);
|
|
3074
|
+
if (normalizedFromOriginal) {
|
|
3075
|
+
relativePathValue = normalizedFromOriginal;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
const location = relativePathValue ?? absolutePath ?? originalPath;
|
|
3080
|
+
return {
|
|
3081
|
+
path: originalPath,
|
|
3082
|
+
kind: artifact.kind,
|
|
3083
|
+
label: describeArtifact(artifact.kind, location),
|
|
3084
|
+
relativePath: relativePathValue,
|
|
3085
|
+
absolutePath
|
|
3086
|
+
};
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
async function verifyRunArtifactsOnDisk(companyId: string, artifacts: RunArtifact[]) {
|
|
3091
|
+
for (const artifact of artifacts) {
|
|
3092
|
+
const resolved = resolveRunArtifactAbsolutePath(companyId, {
|
|
3093
|
+
path: artifact.path,
|
|
3094
|
+
relativePath: artifact.relativePath ?? undefined,
|
|
3095
|
+
absolutePath: artifact.absolutePath ?? undefined
|
|
3096
|
+
});
|
|
3097
|
+
if (!resolved) {
|
|
3098
|
+
artifact.verifiedOnDisk = false;
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
const stats = await stat(resolved);
|
|
3103
|
+
artifact.verifiedOnDisk = stats.isFile();
|
|
3104
|
+
} catch {
|
|
3105
|
+
artifact.verifiedOnDisk = false;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
function toNormalizedWorkspaceRelativePath(inputPath: string | null | undefined) {
|
|
3111
|
+
const trimmed = inputPath?.trim();
|
|
3112
|
+
if (!trimmed) {
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
const unixSeparated = trimmed.replace(/\\/g, "/");
|
|
3116
|
+
const parts: string[] = [];
|
|
3117
|
+
for (const part of unixSeparated.split("/")) {
|
|
3118
|
+
if (!part || part === ".") {
|
|
3119
|
+
continue;
|
|
3120
|
+
}
|
|
3121
|
+
if (part === "..") {
|
|
3122
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
3123
|
+
parts.pop();
|
|
3124
|
+
} else {
|
|
3125
|
+
parts.push(part);
|
|
3126
|
+
}
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
parts.push(part);
|
|
3130
|
+
}
|
|
3131
|
+
const normalized = parts.join("/");
|
|
3132
|
+
return normalized || null;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
function normalizeAgentOperatingArtifactRelativePath(pathValue: string | null, companyId: string) {
|
|
3136
|
+
const normalized = toNormalizedWorkspaceRelativePath(pathValue);
|
|
3137
|
+
if (!normalized) {
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
const workspaceScopedMatch = normalized.match(/(?:^|\/)(workspace\/[^/]+\/agents\/[^/]+\/operating(?:\/.*)?)$/);
|
|
3141
|
+
if (workspaceScopedMatch) {
|
|
3142
|
+
const scopedPath = toNormalizedWorkspaceRelativePath(workspaceScopedMatch[1]);
|
|
3143
|
+
if (!scopedPath) {
|
|
3144
|
+
return null;
|
|
3145
|
+
}
|
|
3146
|
+
const parsed = scopedPath.match(/^workspace\/([^/]+)\/agents\/([^/]+)\/operating(\/.*)?$/);
|
|
3147
|
+
if (!parsed) {
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
const agentId = parsed[2];
|
|
3151
|
+
const suffix = parsed[3] ?? "";
|
|
3152
|
+
return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
|
|
3153
|
+
}
|
|
3154
|
+
const directMatch = normalized.match(/^agents\/([^/]+)\/operating(\/.*)?$/);
|
|
3155
|
+
if (directMatch) {
|
|
3156
|
+
const [, agentId, suffix = ""] = directMatch;
|
|
3157
|
+
return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
|
|
3158
|
+
}
|
|
3159
|
+
const issueScopedMatch = normalized.match(
|
|
3160
|
+
/^(?:[^/]+\/)?projects\/[^/]+\/issues\/[^/]+\/agents\/([^/]+)\/operating(\/.*)?$/
|
|
3161
|
+
);
|
|
3162
|
+
if (issueScopedMatch) {
|
|
3163
|
+
const [, agentId, suffix = ""] = issueScopedMatch;
|
|
3164
|
+
return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
|
|
3165
|
+
}
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
function describeArtifact(kind: string, location: string) {
|
|
3170
|
+
const normalizedKind = kind.toLowerCase();
|
|
3171
|
+
if (normalizedKind.includes("folder") || normalizedKind.includes("directory") || normalizedKind === "website") {
|
|
3172
|
+
return `Created ${normalizedKind.replace(/_/g, " ")} at ${location}`;
|
|
3173
|
+
}
|
|
3174
|
+
if (normalizedKind.includes("file")) {
|
|
3175
|
+
return `Updated file ${location}`;
|
|
3176
|
+
}
|
|
3177
|
+
return `Produced ${normalizedKind.replace(/_/g, " ")} at ${location}`;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function buildRunCompletionReport(input: {
|
|
3181
|
+
companyId?: string;
|
|
3182
|
+
agentName: string;
|
|
3183
|
+
providerType: HeartbeatProviderType;
|
|
3184
|
+
issueIds: string[];
|
|
3185
|
+
executionSummary: string;
|
|
3186
|
+
outcome: ExecutionOutcome | null;
|
|
3187
|
+
finalRunOutput?: AgentFinalRunOutput | null;
|
|
3188
|
+
trace: unknown;
|
|
3189
|
+
digest: RunDigest;
|
|
3190
|
+
terminal: RunTerminalPresentation;
|
|
3191
|
+
cost: RunCostSummary;
|
|
3192
|
+
runtimeCwd?: string | null;
|
|
3193
|
+
errorType?: string | null;
|
|
3194
|
+
errorMessage?: string | null;
|
|
3195
|
+
}): RunCompletionReport {
|
|
3196
|
+
const workspaceRootPath = input.companyId ? resolveCompanyWorkspaceRootPath(input.companyId) : null;
|
|
3197
|
+
const artifacts = buildRunArtifacts({
|
|
3198
|
+
outcome: input.outcome,
|
|
3199
|
+
finalRunOutput: input.finalRunOutput,
|
|
3200
|
+
runtimeCwd: input.runtimeCwd,
|
|
3201
|
+
workspaceRootPath,
|
|
3202
|
+
companyId: input.companyId
|
|
3203
|
+
});
|
|
3204
|
+
const fallbackSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
|
|
3205
|
+
const employeeComment =
|
|
3206
|
+
input.finalRunOutput?.employee_comment?.trim() || buildLegacyEmployeeComment(fallbackSummary);
|
|
3207
|
+
const results = input.finalRunOutput
|
|
3208
|
+
? input.finalRunOutput.results.filter((value): value is string => Boolean(value))
|
|
3209
|
+
: input.terminal.publicStatus === "completed"
|
|
3210
|
+
? dedupeRunDigestPoints(
|
|
3211
|
+
[
|
|
3212
|
+
input.digest.successes[0],
|
|
3213
|
+
artifacts[0]?.label,
|
|
3214
|
+
input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : null
|
|
3215
|
+
].filter((value): value is string => Boolean(value)),
|
|
3216
|
+
4
|
|
3217
|
+
)
|
|
3218
|
+
: [];
|
|
3219
|
+
const errors =
|
|
3220
|
+
input.finalRunOutput?.errors.filter((value): value is string => Boolean(value)) ??
|
|
3221
|
+
dedupeRunDigestPoints([...input.digest.blockers, ...input.digest.failures].filter((value): value is string => Boolean(value)), 4);
|
|
3222
|
+
const summary = firstMeaningfulReportLine(employeeComment) || results[0] || fallbackSummary;
|
|
3223
|
+
const resultSummary =
|
|
3224
|
+
results[0] ??
|
|
3225
|
+
(input.terminal.publicStatus === "completed"
|
|
3226
|
+
? artifacts[0]?.label ??
|
|
3227
|
+
(input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : summary)
|
|
3228
|
+
: input.finalRunOutput
|
|
3229
|
+
? summary
|
|
3230
|
+
: "No valid final run output was produced.");
|
|
3231
|
+
const statusHeadline =
|
|
3232
|
+
input.terminal.publicStatus === "completed"
|
|
3233
|
+
? `Completed: ${summary}`
|
|
3234
|
+
: `Failed: ${summary}`;
|
|
3235
|
+
const blockers = dedupeRunDigestPoints(errors, 4);
|
|
3236
|
+
const artifactPaths = artifacts
|
|
3237
|
+
.map((artifact) => artifact.relativePath ?? artifact.absolutePath ?? artifact.path)
|
|
3238
|
+
.filter((value): value is string => Boolean(value));
|
|
3239
|
+
const managerReport = {
|
|
3240
|
+
agentName: input.agentName,
|
|
3241
|
+
providerType: input.providerType,
|
|
3242
|
+
whatWasDone: results[0] ?? (input.terminal.publicStatus === "completed" ? input.digest.successes[0] ?? summary : summary),
|
|
3243
|
+
resultSummary,
|
|
3244
|
+
artifactPaths,
|
|
3245
|
+
blockers,
|
|
3246
|
+
nextAction: input.digest.nextAction,
|
|
3247
|
+
costLine: formatRunCostLine(input.cost)
|
|
3248
|
+
};
|
|
3249
|
+
const fallbackOutcome: ExecutionOutcome = input.outcome ?? {
|
|
3250
|
+
kind:
|
|
3251
|
+
input.terminal.completionReason === "no_assigned_work"
|
|
3252
|
+
? "skipped"
|
|
3253
|
+
: input.terminal.publicStatus === "completed"
|
|
3254
|
+
? "completed"
|
|
3255
|
+
: "failed",
|
|
3256
|
+
issueIdsTouched: input.issueIds,
|
|
3257
|
+
artifacts: artifacts.map((artifact) => ({ path: artifact.path, kind: artifact.kind })),
|
|
3258
|
+
actions:
|
|
3259
|
+
results.length > 0
|
|
3260
|
+
? results.slice(0, 4).map((result) => ({
|
|
3261
|
+
type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
|
|
3262
|
+
status: input.terminal.publicStatus === "completed" ? "ok" : "error",
|
|
3263
|
+
detail: result
|
|
3264
|
+
}))
|
|
3265
|
+
: [
|
|
3266
|
+
{
|
|
3267
|
+
type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
|
|
3268
|
+
status: input.terminal.publicStatus === "completed" ? "ok" : "error",
|
|
3269
|
+
detail: managerReport.whatWasDone
|
|
3270
|
+
}
|
|
3271
|
+
],
|
|
3272
|
+
blockers: blockers.map((message) => ({
|
|
3273
|
+
code: input.terminal.completionReason,
|
|
3274
|
+
message,
|
|
3275
|
+
retryable: input.terminal.publicStatus !== "completed"
|
|
3276
|
+
})),
|
|
3277
|
+
nextSuggestedState: input.terminal.publicStatus === "completed" ? "in_review" : "blocked"
|
|
3278
|
+
};
|
|
3279
|
+
return {
|
|
3280
|
+
finalStatus: input.terminal.publicStatus,
|
|
3281
|
+
completionReason: input.terminal.completionReason,
|
|
3282
|
+
statusHeadline,
|
|
3283
|
+
summary,
|
|
3284
|
+
employeeComment,
|
|
3285
|
+
results,
|
|
3286
|
+
errors,
|
|
3287
|
+
resultStatus: artifacts.length > 0 ? "reported" : "none_reported",
|
|
3288
|
+
resultSummary,
|
|
3289
|
+
issueIds: input.issueIds,
|
|
3290
|
+
artifacts,
|
|
3291
|
+
blockers,
|
|
3292
|
+
nextAction: input.digest.nextAction,
|
|
3293
|
+
cost: input.cost,
|
|
3294
|
+
managerReport,
|
|
3295
|
+
outcome: input.outcome ?? fallbackOutcome,
|
|
3296
|
+
debug: {
|
|
3297
|
+
persistedRunStatus: input.terminal.internalStatus,
|
|
3298
|
+
failureType: readTraceString(input.trace, "failureType"),
|
|
3299
|
+
errorType: input.errorType ?? null,
|
|
3300
|
+
errorMessage: input.errorMessage ?? null
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function firstMeaningfulReportLine(value: string) {
|
|
3306
|
+
for (const rawLine of value.split(/\r?\n/)) {
|
|
3307
|
+
const line = rawLine.replace(/^[#>*\-\s`]+/, "").trim();
|
|
3308
|
+
if (line) {
|
|
3309
|
+
return line;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return "";
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
function buildLegacyEmployeeComment(summary: string) {
|
|
3316
|
+
return summary;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function formatRunCostLine(cost: RunCostSummary) {
|
|
3320
|
+
const tokens = `${cost.tokenInput} input / ${cost.tokenOutput} output tokens`;
|
|
3321
|
+
if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
|
|
3322
|
+
return `${tokens}; dollar cost unknown`;
|
|
3323
|
+
}
|
|
3324
|
+
const qualifier = cost.usdCostStatus === "estimated" ? "estimated" : "exact";
|
|
3325
|
+
return `${tokens}; ${qualifier} cost $${cost.usdCost.toFixed(6)}`;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
function buildHumanRunUpdateCommentFromReport(
|
|
3329
|
+
report: RunCompletionReport,
|
|
3330
|
+
options: { runId: string; companyId: string }
|
|
3331
|
+
) {
|
|
3332
|
+
const lines = [
|
|
3333
|
+
report.employeeComment.trim(),
|
|
3334
|
+
"",
|
|
3335
|
+
`- Status: ${report.finalStatus}`,
|
|
3336
|
+
`- Agent: ${report.managerReport.agentName}`,
|
|
3337
|
+
`- Provider: ${report.managerReport.providerType}`,
|
|
3338
|
+
""
|
|
3339
|
+
];
|
|
3340
|
+
if (report.results.length > 0) {
|
|
3341
|
+
lines.push("### Results", "");
|
|
3342
|
+
for (const result of report.results) {
|
|
3343
|
+
lines.push(`- ${result}`);
|
|
3344
|
+
}
|
|
3345
|
+
lines.push("");
|
|
3346
|
+
}
|
|
3347
|
+
lines.push("### Result", "", `- What was done: ${report.managerReport.whatWasDone}`, `- Summary: ${report.managerReport.resultSummary}`);
|
|
3348
|
+
if (report.artifacts.length > 0) {
|
|
3349
|
+
for (const [artifactIndex, artifact] of report.artifacts.entries()) {
|
|
3350
|
+
lines.push(`- Artifact: ${formatRunArtifactMarkdownLink(artifact, { ...options, artifactIndex })}`);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
lines.push("");
|
|
3354
|
+
lines.push("### Cost", "");
|
|
3355
|
+
lines.push(`- Input tokens: \`${report.cost.tokenInput}\``);
|
|
3356
|
+
lines.push(`- Output tokens: \`${report.cost.tokenOutput}\``);
|
|
3357
|
+
lines.push(`- Dollar cost: ${formatRunCostForHumanReport(report.cost)}`);
|
|
3358
|
+
if (report.errors.length > 0) {
|
|
3359
|
+
lines.push("");
|
|
3360
|
+
lines.push("### Errors", "");
|
|
3361
|
+
for (const error of report.errors) {
|
|
3362
|
+
lines.push(`- ${error}`);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
return lines.join("\n");
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function formatRunArtifactMarkdownLink(
|
|
3369
|
+
artifact: RunArtifact,
|
|
3370
|
+
options: { runId: string; companyId: string; artifactIndex: number }
|
|
3371
|
+
) {
|
|
3372
|
+
const label = resolveRunArtifactDisplayPath(artifact);
|
|
3373
|
+
const href = buildRunArtifactLinkHref(options);
|
|
3374
|
+
if (!label) {
|
|
3375
|
+
return "`artifact`";
|
|
3376
|
+
}
|
|
3377
|
+
if (artifact.verifiedOnDisk === false) {
|
|
3378
|
+
return `\`${label}\` (not found under company workspace at run completion)`;
|
|
3379
|
+
}
|
|
3380
|
+
if (!href) {
|
|
3381
|
+
return `\`${label}\``;
|
|
3382
|
+
}
|
|
3383
|
+
return `[${label}](${href})`;
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
function resolveRunArtifactDisplayPath(artifact: RunArtifact) {
|
|
3387
|
+
const relative = toNormalizedWorkspaceRelativePath(artifact.relativePath);
|
|
3388
|
+
if (relative && !relative.startsWith("../")) {
|
|
3389
|
+
return relative;
|
|
3390
|
+
}
|
|
3391
|
+
const pathValue = toNormalizedWorkspaceRelativePath(artifact.path);
|
|
3392
|
+
if (pathValue && !pathValue.startsWith("../") && !isAbsolute(artifact.path)) {
|
|
3393
|
+
return pathValue;
|
|
3394
|
+
}
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
function buildRunArtifactLinkHref(options: { runId: string; companyId: string; artifactIndex: number }) {
|
|
3399
|
+
const apiBaseUrl = resolveControlPlaneApiBaseUrl().replace(/\/+$/, "");
|
|
3400
|
+
const runId = encodeURIComponent(options.runId);
|
|
3401
|
+
const artifactIndex = encodeURIComponent(String(options.artifactIndex));
|
|
3402
|
+
const companyId = encodeURIComponent(options.companyId);
|
|
3403
|
+
return `${apiBaseUrl}/observability/heartbeats/${runId}/artifacts/${artifactIndex}/download?companyId=${companyId}`;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
function formatRunCostForHumanReport(cost: RunCostSummary) {
|
|
3407
|
+
if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
|
|
3408
|
+
return "unknown";
|
|
3409
|
+
}
|
|
3410
|
+
const qualifier = cost.usdCostStatus === "estimated" ? "estimated " : "exact ";
|
|
3411
|
+
return `${qualifier}\`$${cost.usdCost.toFixed(6)}\``;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function buildRunListMessageFromReport(report: RunCompletionReport) {
|
|
3415
|
+
const resultParts =
|
|
3416
|
+
report.finalStatus === "completed"
|
|
3417
|
+
? report.results.length > 0
|
|
3418
|
+
? report.results.slice(0, 2)
|
|
3419
|
+
: [report.resultSummary]
|
|
3420
|
+
: [];
|
|
3421
|
+
const parts = [report.statusHeadline, ...resultParts];
|
|
3422
|
+
if (report.artifacts.length > 0) {
|
|
3423
|
+
parts.push(`Artifacts: ${report.managerReport.artifactPaths.join(", ")}`);
|
|
3424
|
+
}
|
|
3425
|
+
if (report.cost.usdCostStatus === "unknown") {
|
|
3426
|
+
parts.push("Cost: unknown");
|
|
3427
|
+
} else if (report.cost.usdCost !== null && report.cost.usdCost !== undefined) {
|
|
3428
|
+
parts.push(`Cost: $${report.cost.usdCost.toFixed(6)}`);
|
|
3429
|
+
}
|
|
3430
|
+
const compact = parts.filter(Boolean).join(" | ");
|
|
3431
|
+
return compact.length > 220 ? `${compact.slice(0, 217).trimEnd()}...` : compact;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
function isMachineNoiseLine(text: string) {
|
|
3435
|
+
const normalized = text.trim();
|
|
3436
|
+
if (!normalized) {
|
|
3437
|
+
return true;
|
|
3438
|
+
}
|
|
3439
|
+
if (normalized.length > 220) {
|
|
3440
|
+
return true;
|
|
3441
|
+
}
|
|
3442
|
+
const patterns = [
|
|
3443
|
+
/^command:\s*/i,
|
|
3444
|
+
/^\s*[\[{].*[\]}]\s*$/,
|
|
3445
|
+
/\/bin\/(bash|zsh|sh)/i,
|
|
3446
|
+
/(^|\s)(\/Users\/|\/home\/|\/private\/var\/|[A-Za-z]:\\)/,
|
|
3447
|
+
/\b(stderr|stdout|stack trace|exit code|payload_json|tokeninput|tokenoutput|usdcost)\b/i,
|
|
3448
|
+
/(^|\s)at\s+\S+:\d+:\d+/,
|
|
3449
|
+
/```/,
|
|
3450
|
+
/\{[\s\S]*"(summary|tokenInput|tokenOutput|usdCost|trace|error)"[\s\S]*\}/i
|
|
3451
|
+
];
|
|
3452
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
3453
|
+
}
|
|
3454
|
+
|
|
2326
3455
|
function extractSummaryFromJsonLikeText(input: string) {
|
|
2327
3456
|
const fencedMatch = input.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
2328
3457
|
const candidate = fencedMatch?.[1]?.trim() ?? input.match(/\{[\s\S]*\}\s*$/)?.[0]?.trim();
|
|
@@ -2354,19 +3483,18 @@ async function appendRunSummaryComments(
|
|
|
2354
3483
|
issueIds: string[];
|
|
2355
3484
|
agentId: string;
|
|
2356
3485
|
runId: string;
|
|
2357
|
-
|
|
2358
|
-
executionSummary: string;
|
|
3486
|
+
report: RunCompletionReport;
|
|
2359
3487
|
}
|
|
2360
3488
|
) {
|
|
2361
3489
|
if (input.issueIds.length === 0) {
|
|
2362
3490
|
return;
|
|
2363
3491
|
}
|
|
2364
|
-
const commentBody =
|
|
2365
|
-
|
|
2366
|
-
|
|
3492
|
+
const commentBody = buildHumanRunUpdateCommentFromReport(input.report, {
|
|
3493
|
+
runId: input.runId,
|
|
3494
|
+
companyId: input.companyId
|
|
2367
3495
|
});
|
|
2368
3496
|
for (const issueId of input.issueIds) {
|
|
2369
|
-
const
|
|
3497
|
+
const existingRunComments = await db
|
|
2370
3498
|
.select({ id: issueComments.id })
|
|
2371
3499
|
.from(issueComments)
|
|
2372
3500
|
.where(
|
|
@@ -2378,6 +3506,58 @@ async function appendRunSummaryComments(
|
|
|
2378
3506
|
eq(issueComments.authorId, input.agentId)
|
|
2379
3507
|
)
|
|
2380
3508
|
)
|
|
3509
|
+
.orderBy(desc(issueComments.createdAt));
|
|
3510
|
+
if (existingRunComments.length > 0) {
|
|
3511
|
+
await db.delete(issueComments).where(
|
|
3512
|
+
and(
|
|
3513
|
+
eq(issueComments.companyId, input.companyId),
|
|
3514
|
+
inArray(
|
|
3515
|
+
issueComments.id,
|
|
3516
|
+
existingRunComments.map((comment) => comment.id)
|
|
3517
|
+
)
|
|
3518
|
+
)
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
await addIssueComment(db, {
|
|
3522
|
+
companyId: input.companyId,
|
|
3523
|
+
issueId,
|
|
3524
|
+
authorType: "agent",
|
|
3525
|
+
authorId: input.agentId,
|
|
3526
|
+
runId: input.runId,
|
|
3527
|
+
body: commentBody
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
async function appendProviderUsageLimitBoardComments(
|
|
3533
|
+
db: BopoDb,
|
|
3534
|
+
input: {
|
|
3535
|
+
companyId: string;
|
|
3536
|
+
issueIds: string[];
|
|
3537
|
+
agentId: string;
|
|
3538
|
+
runId: string;
|
|
3539
|
+
providerType: string;
|
|
3540
|
+
message: string;
|
|
3541
|
+
paused: boolean;
|
|
3542
|
+
}
|
|
3543
|
+
) {
|
|
3544
|
+
if (input.issueIds.length === 0) {
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
const commentBody = buildProviderUsageLimitBoardCommentBody(input);
|
|
3548
|
+
for (const issueId of input.issueIds) {
|
|
3549
|
+
const [existingRunComment] = await db
|
|
3550
|
+
.select({ id: issueComments.id })
|
|
3551
|
+
.from(issueComments)
|
|
3552
|
+
.where(
|
|
3553
|
+
and(
|
|
3554
|
+
eq(issueComments.companyId, input.companyId),
|
|
3555
|
+
eq(issueComments.issueId, issueId),
|
|
3556
|
+
eq(issueComments.runId, input.runId),
|
|
3557
|
+
eq(issueComments.authorType, "system"),
|
|
3558
|
+
eq(issueComments.authorId, input.agentId)
|
|
3559
|
+
)
|
|
3560
|
+
)
|
|
2381
3561
|
.limit(1);
|
|
2382
3562
|
if (existingRunComment) {
|
|
2383
3563
|
continue;
|
|
@@ -2385,14 +3565,70 @@ async function appendRunSummaryComments(
|
|
|
2385
3565
|
await addIssueComment(db, {
|
|
2386
3566
|
companyId: input.companyId,
|
|
2387
3567
|
issueId,
|
|
2388
|
-
authorType: "
|
|
3568
|
+
authorType: "system",
|
|
2389
3569
|
authorId: input.agentId,
|
|
2390
3570
|
runId: input.runId,
|
|
3571
|
+
recipients: [
|
|
3572
|
+
{
|
|
3573
|
+
recipientType: "board",
|
|
3574
|
+
deliveryStatus: "pending"
|
|
3575
|
+
}
|
|
3576
|
+
],
|
|
2391
3577
|
body: commentBody
|
|
2392
3578
|
});
|
|
2393
3579
|
}
|
|
2394
3580
|
}
|
|
2395
3581
|
|
|
3582
|
+
function buildProviderUsageLimitBoardCommentBody(input: {
|
|
3583
|
+
providerType: string;
|
|
3584
|
+
message: string;
|
|
3585
|
+
paused: boolean;
|
|
3586
|
+
}) {
|
|
3587
|
+
const providerLabel = input.providerType.replace(/[_-]+/g, " ").trim();
|
|
3588
|
+
const normalizedProvider = providerLabel.charAt(0).toUpperCase() + providerLabel.slice(1);
|
|
3589
|
+
const agentStateLine = input.paused ? "Agent paused." : "Agent already paused.";
|
|
3590
|
+
return `${normalizedProvider} usage limit reached.\nRun failed due to provider limits.\n${agentStateLine}\nNext: resume after usage reset or billing/credential fix.`;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
async function pauseAgentForProviderUsageLimit(
|
|
3594
|
+
db: BopoDb,
|
|
3595
|
+
input: {
|
|
3596
|
+
companyId: string;
|
|
3597
|
+
agentId: string;
|
|
3598
|
+
requestId: string;
|
|
3599
|
+
runId: string;
|
|
3600
|
+
providerType: string;
|
|
3601
|
+
message: string;
|
|
3602
|
+
}
|
|
3603
|
+
) {
|
|
3604
|
+
const [agentRow] = await db
|
|
3605
|
+
.select({ status: agents.status })
|
|
3606
|
+
.from(agents)
|
|
3607
|
+
.where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)))
|
|
3608
|
+
.limit(1);
|
|
3609
|
+
if (!agentRow || agentRow.status === "paused" || agentRow.status === "terminated") {
|
|
3610
|
+
return { paused: false as const };
|
|
3611
|
+
}
|
|
3612
|
+
await db
|
|
3613
|
+
.update(agents)
|
|
3614
|
+
.set({ status: "paused", updatedAt: new Date() })
|
|
3615
|
+
.where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)));
|
|
3616
|
+
await appendAuditEvent(db, {
|
|
3617
|
+
companyId: input.companyId,
|
|
3618
|
+
actorType: "system",
|
|
3619
|
+
eventType: "agent.paused_auto_provider_limit",
|
|
3620
|
+
entityType: "agent",
|
|
3621
|
+
entityId: input.agentId,
|
|
3622
|
+
correlationId: input.requestId,
|
|
3623
|
+
payload: {
|
|
3624
|
+
runId: input.runId,
|
|
3625
|
+
providerType: input.providerType,
|
|
3626
|
+
reason: input.message
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
return { paused: true as const };
|
|
3630
|
+
}
|
|
3631
|
+
|
|
2396
3632
|
function parseAgentState(stateBlob: string | null) {
|
|
2397
3633
|
if (!stateBlob) {
|
|
2398
3634
|
return { state: {} as AgentState, parseError: null };
|
|
@@ -2735,10 +3971,7 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
2735
3971
|
}
|
|
2736
3972
|
|
|
2737
3973
|
if (projectIssue?.id) {
|
|
2738
|
-
const issueScopedWorkspaceCwd =
|
|
2739
|
-
companyId,
|
|
2740
|
-
join(selectedWorkspaceCwd, "issues", projectIssue.id)
|
|
2741
|
-
);
|
|
3974
|
+
const issueScopedWorkspaceCwd = resolveProjectIssueWorkspaceCwd(companyId, selectedWorkspaceCwd, projectIssue.id);
|
|
2742
3975
|
await mkdir(issueScopedWorkspaceCwd, { recursive: true });
|
|
2743
3976
|
selectedWorkspaceCwd = issueScopedWorkspaceCwd;
|
|
2744
3977
|
}
|
|
@@ -2787,6 +4020,10 @@ async function resolveRuntimeWorkspaceForWorkItems(
|
|
|
2787
4020
|
};
|
|
2788
4021
|
}
|
|
2789
4022
|
|
|
4023
|
+
function resolveProjectIssueWorkspaceCwd(companyId: string, projectWorkspaceCwd: string, issueId: string) {
|
|
4024
|
+
return normalizeCompanyWorkspacePath(companyId, join(projectWorkspaceCwd, "issues", issueId));
|
|
4025
|
+
}
|
|
4026
|
+
|
|
2790
4027
|
function resolveGitWorktreeIsolationEnabled() {
|
|
2791
4028
|
const value = String(process.env.BOPO_ENABLE_GIT_WORKTREE_ISOLATION ?? "")
|
|
2792
4029
|
.trim()
|
|
@@ -3108,6 +4345,24 @@ function clearResumeState(
|
|
|
3108
4345
|
};
|
|
3109
4346
|
}
|
|
3110
4347
|
|
|
4348
|
+
function resolveHeartbeatPromptMode(): "full" | "compact" {
|
|
4349
|
+
const raw = process.env.BOPO_HEARTBEAT_PROMPT_MODE?.trim().toLowerCase();
|
|
4350
|
+
return raw === "compact" ? "compact" : "full";
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
type HeartbeatIdlePolicy = "full" | "skip_adapter" | "micro_prompt";
|
|
4354
|
+
|
|
4355
|
+
function resolveHeartbeatIdlePolicy(): HeartbeatIdlePolicy {
|
|
4356
|
+
const raw = process.env.BOPO_HEARTBEAT_IDLE_POLICY?.trim().toLowerCase();
|
|
4357
|
+
if (raw === "skip_adapter") {
|
|
4358
|
+
return "skip_adapter";
|
|
4359
|
+
}
|
|
4360
|
+
if (raw === "micro_prompt") {
|
|
4361
|
+
return "micro_prompt";
|
|
4362
|
+
}
|
|
4363
|
+
return "full";
|
|
4364
|
+
}
|
|
4365
|
+
|
|
3111
4366
|
function resolveControlPlaneEnv(runtimeEnv: Record<string, string>, suffix: string) {
|
|
3112
4367
|
const next = runtimeEnv[`BOPODEV_${suffix}`];
|
|
3113
4368
|
return hasText(next) ? (next as string) : "";
|
|
@@ -3124,8 +4379,13 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
3124
4379
|
canHireAgents: boolean;
|
|
3125
4380
|
wakeContext?: HeartbeatWakeContext;
|
|
3126
4381
|
}) {
|
|
4382
|
+
const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(input.companyId);
|
|
4383
|
+
const agentHome = resolveAgentFallbackWorkspace(input.companyId, input.agentId);
|
|
4384
|
+
const agentOperatingDir = join(agentHome, "operating");
|
|
3127
4385
|
const apiBaseUrl = resolveControlPlaneApiBaseUrl();
|
|
3128
|
-
|
|
4386
|
+
// agents:write is required for PUT /agents/:self (bootstrapPrompt, runtimeConfig). Route handlers
|
|
4387
|
+
// still forbid agents from updating other agents' rows and from POST /agents unless canHireAgents.
|
|
4388
|
+
const actorPermissions = ["issues:write", "agents:write"].join(",");
|
|
3129
4389
|
const actorHeaders = JSON.stringify({
|
|
3130
4390
|
"x-company-id": input.companyId,
|
|
3131
4391
|
"x-actor-type": "agent",
|
|
@@ -3139,7 +4399,12 @@ function buildHeartbeatRuntimeEnv(input: {
|
|
|
3139
4399
|
return {
|
|
3140
4400
|
BOPODEV_AGENT_ID: input.agentId,
|
|
3141
4401
|
BOPODEV_COMPANY_ID: input.companyId,
|
|
4402
|
+
BOPODEV_COMPANY_WORKSPACE_ROOT: companyWorkspaceRoot,
|
|
4403
|
+
BOPODEV_AGENT_HOME: agentHome,
|
|
4404
|
+
BOPODEV_AGENT_OPERATING_DIR: agentOperatingDir,
|
|
3142
4405
|
BOPODEV_RUN_ID: input.heartbeatRunId,
|
|
4406
|
+
BOPODEV_HEARTBEAT_PROMPT_MODE: resolveHeartbeatPromptMode(),
|
|
4407
|
+
BOPODEV_HEARTBEAT_IDLE_POLICY: resolveHeartbeatIdlePolicy(),
|
|
3143
4408
|
BOPODEV_FORCE_MANAGED_CODEX_HOME: "false",
|
|
3144
4409
|
BOPODEV_API_BASE_URL: apiBaseUrl,
|
|
3145
4410
|
BOPODEV_API_URL: apiBaseUrl,
|
|
@@ -3414,6 +4679,7 @@ function resolveRuntimeModelId(input: { runtimeModel?: string; stateBlob?: strin
|
|
|
3414
4679
|
async function appendFinishedRunCostEntry(input: {
|
|
3415
4680
|
db: BopoDb;
|
|
3416
4681
|
companyId: string;
|
|
4682
|
+
runId?: string | null;
|
|
3417
4683
|
providerType: string;
|
|
3418
4684
|
runtimeModelId: string | null;
|
|
3419
4685
|
pricingProviderType?: string | null;
|
|
@@ -3440,25 +4706,22 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
3440
4706
|
const shouldPersist = input.status === "ok" || input.status === "failed";
|
|
3441
4707
|
const runtimeUsdCost = Math.max(0, input.runtimeUsdCost ?? 0);
|
|
3442
4708
|
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;
|
|
4709
|
+
const usdCostStatus: "exact" | "estimated" | "unknown" =
|
|
4710
|
+
runtimeUsdCost > 0 ? "exact" : pricedUsdCost > 0 ? "estimated" : "unknown";
|
|
4711
|
+
const effectiveUsdCost = usdCostStatus === "exact" ? runtimeUsdCost : usdCostStatus === "estimated" ? pricedUsdCost : 0;
|
|
3451
4712
|
const effectivePricingSource = pricingDecision.pricingSource;
|
|
3452
4713
|
const shouldPersistWithUsage =
|
|
3453
|
-
shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 ||
|
|
4714
|
+
shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 || usdCostStatus !== "unknown");
|
|
3454
4715
|
if (shouldPersistWithUsage) {
|
|
3455
4716
|
await appendCost(input.db, {
|
|
3456
4717
|
companyId: input.companyId,
|
|
4718
|
+
runId: input.runId ?? null,
|
|
3457
4719
|
providerType: input.providerType,
|
|
3458
4720
|
runtimeModelId: input.runtimeModelId,
|
|
3459
4721
|
pricingProviderType: pricingDecision.pricingProviderType,
|
|
3460
4722
|
pricingModelId: pricingDecision.pricingModelId,
|
|
3461
4723
|
pricingSource: effectivePricingSource,
|
|
4724
|
+
usdCostStatus,
|
|
3462
4725
|
tokenInput: input.tokenInput,
|
|
3463
4726
|
tokenOutput: input.tokenOutput,
|
|
3464
4727
|
usdCost: effectiveUsdCost.toFixed(6),
|
|
@@ -3471,7 +4734,8 @@ async function appendFinishedRunCostEntry(input: {
|
|
|
3471
4734
|
return {
|
|
3472
4735
|
...pricingDecision,
|
|
3473
4736
|
pricingSource: effectivePricingSource,
|
|
3474
|
-
usdCost: effectiveUsdCost
|
|
4737
|
+
usdCost: effectiveUsdCost,
|
|
4738
|
+
usdCostStatus
|
|
3475
4739
|
};
|
|
3476
4740
|
}
|
|
3477
4741
|
|