@peakflo/agent-harness 0.2.4-rc.2 → 0.2.5-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/coding-agent-adapter.d.ts +4 -0
- package/dist/adapters/pi-adapter.js +24 -0
- package/dist/contract.d.ts +3 -1
- package/dist/contract.js +3 -1
- package/dist/control-plane.d.ts +3 -0
- package/dist/control-plane.js +21 -0
- package/dist/harness.js +49 -2
- package/dist/runtime/cold-start-steps.d.ts +11 -0
- package/dist/runtime/cold-start-steps.js +57 -0
- package/dist/runtime/session-runner.d.ts +2 -0
- package/dist/runtime/session-runner.js +62 -10
- package/package.json +1 -1
|
@@ -41,6 +41,10 @@ export interface McpServerConfig {
|
|
|
41
41
|
export interface SessionConfig {
|
|
42
42
|
agentId: string;
|
|
43
43
|
taskId: string;
|
|
44
|
+
/** Leased turn identifier for timing logs. */
|
|
45
|
+
turnId?: string;
|
|
46
|
+
/** Best-effort structured log sink for turn phases. */
|
|
47
|
+
coldStartLog?: (line: string) => void;
|
|
44
48
|
workspaceDir: string;
|
|
45
49
|
/**
|
|
46
50
|
* Refuse dev servers and app scaffolding in shell calls unless the turn's
|
|
@@ -21,6 +21,7 @@ const string_decoder_1 = require("string_decoder");
|
|
|
21
21
|
const util_1 = require("util");
|
|
22
22
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
23
23
|
const model_gateway_1 = require("./model-gateway");
|
|
24
|
+
const cold_start_steps_1 = require("../runtime/cold-start-steps");
|
|
24
25
|
const dev_server_guard_1 = require("../runtime/dev-server-guard");
|
|
25
26
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
26
27
|
const RPC_TIMEOUT_MS = 15_000;
|
|
@@ -516,10 +517,20 @@ class PiAdapter {
|
|
|
516
517
|
}
|
|
517
518
|
}
|
|
518
519
|
async spawnSession(config, resumeId) {
|
|
520
|
+
const steps = config.turnId
|
|
521
|
+
? new cold_start_steps_1.ColdStartSteps(config.taskId, config.turnId, (line) => {
|
|
522
|
+
console.log(line);
|
|
523
|
+
config.coldStartLog?.(line);
|
|
524
|
+
})
|
|
525
|
+
: null;
|
|
519
526
|
const executable = await this.findPiExecutable();
|
|
520
527
|
const gateway = this.installPeakfloGateway();
|
|
528
|
+
const endMcpConfig = steps?.begin('mcp_config_write');
|
|
521
529
|
const mcpConfigPath = this.buildMcpConfig(config);
|
|
530
|
+
endMcpConfig?.();
|
|
531
|
+
const endSkills = steps?.begin('skills_install');
|
|
522
532
|
const skills = this.buildSkillFiles(config);
|
|
533
|
+
endSkills?.();
|
|
523
534
|
const permissionExtension = this.installPermissionExtension();
|
|
524
535
|
const args = [
|
|
525
536
|
'--mode',
|
|
@@ -564,7 +575,9 @@ class PiAdapter {
|
|
|
564
575
|
this.sessions.set(tempId, session);
|
|
565
576
|
this.attachProcess(session);
|
|
566
577
|
try {
|
|
578
|
+
const endPiReady = steps?.begin('pi_process_ready');
|
|
567
579
|
const state = await this.command(session, { type: 'get_state' });
|
|
580
|
+
endPiReady?.();
|
|
568
581
|
const realId = typeof state.data?.sessionFile === 'string'
|
|
569
582
|
? state.data.sessionFile
|
|
570
583
|
: typeof state.data?.sessionId === 'string'
|
|
@@ -893,6 +906,11 @@ class PiAdapter {
|
|
|
893
906
|
handleMessageUpdate(session, update) {
|
|
894
907
|
if (!update)
|
|
895
908
|
return;
|
|
909
|
+
if ((update.type === 'text_delta' || update.type === 'thinking_delta') &&
|
|
910
|
+
String(update.delta || '')) {
|
|
911
|
+
session.firstTokenEnd?.();
|
|
912
|
+
session.firstTokenEnd = undefined;
|
|
913
|
+
}
|
|
896
914
|
const index = String(update.contentIndex ?? 0);
|
|
897
915
|
const key = `${session.projectionId}:${session.turn}:${session.assistantMessageOrdinal}:${index}`;
|
|
898
916
|
if (update.type === 'text_delta') {
|
|
@@ -1050,6 +1068,12 @@ class PiAdapter {
|
|
|
1050
1068
|
session.lastError = null;
|
|
1051
1069
|
session.sawAgentActivity = false;
|
|
1052
1070
|
session.promptMayBeCommandOnly = message.trimStart().startsWith('/');
|
|
1071
|
+
session.firstTokenEnd = config.turnId
|
|
1072
|
+
? new cold_start_steps_1.ColdStartSteps(config.taskId, config.turnId, (line) => {
|
|
1073
|
+
console.log(line);
|
|
1074
|
+
config.coldStartLog?.(line);
|
|
1075
|
+
}).begin('first_model_token')
|
|
1076
|
+
: undefined;
|
|
1053
1077
|
this.sendRecord(session, {
|
|
1054
1078
|
type: 'prompt',
|
|
1055
1079
|
message,
|
package/dist/contract.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* needs to tell such a harness apart, which it does by refusing it
|
|
32
32
|
* rather than silently serving a value that would misbehave.
|
|
33
33
|
*/
|
|
34
|
-
export declare const PROTOCOL_VERSION =
|
|
34
|
+
export declare const PROTOCOL_VERSION = 3;
|
|
35
35
|
/**
|
|
36
36
|
* The floor a harness must meet to be served `maxTurnDurationMs: null`.
|
|
37
37
|
*
|
|
@@ -178,6 +178,8 @@ export declare const ROUTES: {
|
|
|
178
178
|
readonly register: () => string;
|
|
179
179
|
/** Bearer: runner credential. 30s period. */
|
|
180
180
|
readonly heartbeat: (runnerId: string) => string;
|
|
181
|
+
/** Bearer: runner credential. Best-effort timing event; no transcript data. */
|
|
182
|
+
readonly coldStart: (runnerId: string) => string;
|
|
181
183
|
/**
|
|
182
184
|
* Bearer: runner credential. Bounded long-poll: 204 idle, 200 with work.
|
|
183
185
|
*
|
package/dist/contract.js
CHANGED
|
@@ -42,7 +42,7 @@ exports.isPersistablePart = isPersistablePart;
|
|
|
42
42
|
* needs to tell such a harness apart, which it does by refusing it
|
|
43
43
|
* rather than silently serving a value that would misbehave.
|
|
44
44
|
*/
|
|
45
|
-
exports.PROTOCOL_VERSION =
|
|
45
|
+
exports.PROTOCOL_VERSION = 3;
|
|
46
46
|
/**
|
|
47
47
|
* The floor a harness must meet to be served `maxTurnDurationMs: null`.
|
|
48
48
|
*
|
|
@@ -215,6 +215,8 @@ exports.ROUTES = {
|
|
|
215
215
|
register: () => '/api/runners/register',
|
|
216
216
|
/** Bearer: runner credential. 30s period. */
|
|
217
217
|
heartbeat: (runnerId) => `/api/runners/${encodeURIComponent(runnerId)}/heartbeat`,
|
|
218
|
+
/** Bearer: runner credential. Best-effort timing event; no transcript data. */
|
|
219
|
+
coldStart: (runnerId) => `/api/runners/${encodeURIComponent(runnerId)}/cold-start`,
|
|
218
220
|
/**
|
|
219
221
|
* Bearer: runner credential. Bounded long-poll: 204 idle, 200 with work.
|
|
220
222
|
*
|
package/dist/control-plane.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export declare class ControlPlaneClient {
|
|
|
81
81
|
/** Used only for a private IPC handoff to the baked sandbox supervisor. */
|
|
82
82
|
getRunnerCredential(): string | null;
|
|
83
83
|
getRunnerId(): string | null;
|
|
84
|
+
/** Diagnostics must never delay a turn. The caller handles any failure. */
|
|
85
|
+
reportColdStart(line: string): Promise<void>;
|
|
84
86
|
/**
|
|
85
87
|
* Give the lease-renewal worker the minimum information it needs.
|
|
86
88
|
*
|
|
@@ -129,6 +131,7 @@ export declare class ControlPlaneClient {
|
|
|
129
131
|
* 25s poll under a 15s timeout would abort every single call.
|
|
130
132
|
*/
|
|
131
133
|
lease(waitSeconds?: number): Promise<LeaseResponse | null>;
|
|
134
|
+
acknowledgeLease(leaseId: string): Promise<LeaseRenewResponse>;
|
|
132
135
|
renewLease(leaseId: string, status?: RunnerStatus, githubTokenExpiresAt?: string, modelTokenExpiresAt?: string): Promise<LeaseRenewResponse>;
|
|
133
136
|
/** Metadata of the knowledge files the lease's turn may read. Never a URL. */
|
|
134
137
|
knowledgeFiles(leaseId: string, datastoreId?: string): Promise<KnowledgeFilesResponse>;
|
package/dist/control-plane.js
CHANGED
|
@@ -114,6 +114,18 @@ class ControlPlaneClient {
|
|
|
114
114
|
getRunnerId() {
|
|
115
115
|
return this.runnerId;
|
|
116
116
|
}
|
|
117
|
+
/** Diagnostics must never delay a turn. The caller handles any failure. */
|
|
118
|
+
async reportColdStart(line) {
|
|
119
|
+
if (!this.runnerId || !this.runnerCredential)
|
|
120
|
+
return;
|
|
121
|
+
await this.send({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
path: contract_1.ROUTES.coldStart(this.runnerId),
|
|
124
|
+
token: this.runnerCredential,
|
|
125
|
+
body: JSON.parse(line),
|
|
126
|
+
timeoutMs: 2_000
|
|
127
|
+
});
|
|
128
|
+
}
|
|
117
129
|
/**
|
|
118
130
|
* Give the lease-renewal worker the minimum information it needs.
|
|
119
131
|
*
|
|
@@ -248,6 +260,15 @@ class ControlPlaneClient {
|
|
|
248
260
|
/* ------------------------------------------------------------------ */
|
|
249
261
|
/* 4. lease renew */
|
|
250
262
|
/* ------------------------------------------------------------------ */
|
|
263
|
+
async acknowledgeLease(leaseId) {
|
|
264
|
+
const runnerId = this.requireRunnerId();
|
|
265
|
+
return this.send({
|
|
266
|
+
method: 'POST',
|
|
267
|
+
path: contract_1.ROUTES.leaseRenew(runnerId, leaseId),
|
|
268
|
+
token: this.requireCredential(),
|
|
269
|
+
body: { protocolVersion: contract_1.PROTOCOL_VERSION, ackOnly: true }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
251
272
|
async renewLease(leaseId, status, githubTokenExpiresAt, modelTokenExpiresAt) {
|
|
252
273
|
const runnerId = this.requireRunnerId();
|
|
253
274
|
return this.send({
|
package/dist/harness.js
CHANGED
|
@@ -21,6 +21,7 @@ const sqlite_store_1 = require("./store/sqlite-store");
|
|
|
21
21
|
const version_1 = require("./version");
|
|
22
22
|
/** Backoff after a lease poll fails, so an unreachable control plane is not hammered. */
|
|
23
23
|
const LEASE_ERROR_BACKOFF_MS = 5_000;
|
|
24
|
+
const LEASE_MIN_EMPTY_POLL_MS = 250;
|
|
24
25
|
/**
|
|
25
26
|
* How often the model catalogue is re-probed once the harness is running.
|
|
26
27
|
*
|
|
@@ -1031,6 +1032,7 @@ class Harness {
|
|
|
1031
1032
|
continue;
|
|
1032
1033
|
}
|
|
1033
1034
|
let lease = null;
|
|
1035
|
+
const pollStartedAt = Date.now();
|
|
1034
1036
|
try {
|
|
1035
1037
|
lease = await this.client.lease(this.leaseWaitSeconds);
|
|
1036
1038
|
}
|
|
@@ -1050,12 +1052,54 @@ class Harness {
|
|
|
1050
1052
|
continue;
|
|
1051
1053
|
}
|
|
1052
1054
|
if (!lease) {
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
+
// The server normally long-polls. A 204 returned immediately during
|
|
1056
|
+
// a fence or by a faulty proxy must not make this process spin.
|
|
1057
|
+
const waitLeft = LEASE_MIN_EMPTY_POLL_MS - (Date.now() - pollStartedAt);
|
|
1058
|
+
if (waitLeft > 0)
|
|
1059
|
+
await sleep(waitLeft);
|
|
1055
1060
|
continue;
|
|
1056
1061
|
}
|
|
1062
|
+
const leaseReceived = JSON.stringify({
|
|
1063
|
+
event: 'cold_start_step',
|
|
1064
|
+
sessionId: lease.session.sessionId,
|
|
1065
|
+
turnId: lease.session.turnId,
|
|
1066
|
+
step: 'lease_poll_to_pickup',
|
|
1067
|
+
startedAt: new Date(pollStartedAt).toISOString(),
|
|
1068
|
+
endedAt: new Date().toISOString(),
|
|
1069
|
+
durationMs: Date.now() - pollStartedAt
|
|
1070
|
+
});
|
|
1071
|
+
log(leaseReceived);
|
|
1072
|
+
void this.client.reportColdStart(leaseReceived).catch(() => undefined);
|
|
1057
1073
|
this.observeHarnessPin(lease.harnessVersion);
|
|
1058
1074
|
this.observeSandboxToolsPin(lease.sandboxToolsVersion);
|
|
1075
|
+
// Protocol 3 uses a short visibility window until the runner confirms
|
|
1076
|
+
// receipt. A dropped GET response is then reclaimed within nine seconds.
|
|
1077
|
+
// Do not start work unless the acknowledgement extends this lease.
|
|
1078
|
+
let leaseAcknowledged = false;
|
|
1079
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1080
|
+
try {
|
|
1081
|
+
const acknowledged = await this.client.acknowledgeLease(lease.leaseId);
|
|
1082
|
+
if (!acknowledged.renewed) {
|
|
1083
|
+
log('Lease acknowledgement refused', { leaseId: lease.leaseId });
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
leaseAcknowledged = true;
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
log('Lease acknowledgement failed', {
|
|
1091
|
+
leaseId: lease.leaseId,
|
|
1092
|
+
attempt: attempt + 1,
|
|
1093
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1094
|
+
});
|
|
1095
|
+
if (attempt < 2)
|
|
1096
|
+
await sleep(500);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (!leaseAcknowledged) {
|
|
1100
|
+
await sleep(LEASE_ERROR_BACKOFF_MS);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1059
1103
|
void this.runLeasedTurn(lease);
|
|
1060
1104
|
}
|
|
1061
1105
|
await this.stop();
|
|
@@ -1306,6 +1350,9 @@ class Harness {
|
|
|
1306
1350
|
secrets: this.secrets,
|
|
1307
1351
|
events: this.events,
|
|
1308
1352
|
telemetry: this.telemetry,
|
|
1353
|
+
coldStartLog: (line) => {
|
|
1354
|
+
void this.client.reportColdStart(line).catch(() => undefined);
|
|
1355
|
+
},
|
|
1309
1356
|
notifier: this.notifier,
|
|
1310
1357
|
artifacts: this.artifacts,
|
|
1311
1358
|
conversation: this.conversation,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Timing for one leased turn. Values in this log contain no credentials. */
|
|
2
|
+
export declare class ColdStartSteps {
|
|
3
|
+
private readonly sessionId;
|
|
4
|
+
private readonly turnId;
|
|
5
|
+
private readonly log;
|
|
6
|
+
private readonly startedAt;
|
|
7
|
+
private readonly steps;
|
|
8
|
+
constructor(sessionId: string, turnId: string, log: (line: string) => void);
|
|
9
|
+
begin(step: string): () => void;
|
|
10
|
+
summary(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ColdStartSteps = void 0;
|
|
4
|
+
/** Timing for one leased turn. Values in this log contain no credentials. */
|
|
5
|
+
class ColdStartSteps {
|
|
6
|
+
sessionId;
|
|
7
|
+
turnId;
|
|
8
|
+
log;
|
|
9
|
+
startedAt = Date.now();
|
|
10
|
+
steps = {};
|
|
11
|
+
constructor(sessionId, turnId, log) {
|
|
12
|
+
this.sessionId = sessionId;
|
|
13
|
+
this.turnId = turnId;
|
|
14
|
+
this.log = log;
|
|
15
|
+
}
|
|
16
|
+
begin(step) {
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
let ended = false;
|
|
19
|
+
return () => {
|
|
20
|
+
if (ended)
|
|
21
|
+
return;
|
|
22
|
+
ended = true;
|
|
23
|
+
const endedAt = Date.now();
|
|
24
|
+
const durationMs = Math.max(0, endedAt - startedAt);
|
|
25
|
+
this.steps[step] = durationMs;
|
|
26
|
+
try {
|
|
27
|
+
this.log(JSON.stringify({
|
|
28
|
+
event: 'cold_start_step',
|
|
29
|
+
sessionId: this.sessionId,
|
|
30
|
+
turnId: this.turnId,
|
|
31
|
+
step,
|
|
32
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
33
|
+
endedAt: new Date(endedAt).toISOString(),
|
|
34
|
+
durationMs
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// A diagnostic sink must never stop an agent turn.
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
summary() {
|
|
43
|
+
try {
|
|
44
|
+
this.log(JSON.stringify({
|
|
45
|
+
event: 'cold_start_summary',
|
|
46
|
+
sessionId: this.sessionId,
|
|
47
|
+
turnId: this.turnId,
|
|
48
|
+
durationMs: Math.max(0, Date.now() - this.startedAt),
|
|
49
|
+
steps: this.steps
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// A diagnostic sink must never stop an agent turn.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.ColdStartSteps = ColdStartSteps;
|
|
@@ -40,6 +40,8 @@ export interface SessionRunnerDeps {
|
|
|
40
40
|
secrets: SecretProvider;
|
|
41
41
|
events: EventSink;
|
|
42
42
|
telemetry: Telemetry;
|
|
43
|
+
/** Send a structured timing event to the control plane without waiting. */
|
|
44
|
+
coldStartLog?: (line: string) => void;
|
|
43
45
|
notifier: Notifier;
|
|
44
46
|
artifacts: ArtifactInspector;
|
|
45
47
|
/**
|
|
@@ -31,6 +31,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
31
31
|
const model_gateway_1 = require("../adapters/model-gateway");
|
|
32
32
|
const contract_1 = require("../contract");
|
|
33
33
|
const attachments_1 = require("./attachments");
|
|
34
|
+
const cold_start_steps_1 = require("./cold-start-steps");
|
|
34
35
|
const conversation_1 = require("./conversation");
|
|
35
36
|
const knowledge_tools_1 = require("./knowledge-tools");
|
|
36
37
|
const permission_mode_1 = require("./permission-mode");
|
|
@@ -438,6 +439,10 @@ class SessionRunner {
|
|
|
438
439
|
*/
|
|
439
440
|
async run(lease, signal) {
|
|
440
441
|
const { session } = lease;
|
|
442
|
+
const coldStart = new cold_start_steps_1.ColdStartSteps(session.sessionId, session.turnId, (line) => {
|
|
443
|
+
this.logger.log(line);
|
|
444
|
+
this.deps.coldStartLog?.(line);
|
|
445
|
+
});
|
|
441
446
|
const startedAt = new Date().toISOString();
|
|
442
447
|
const release = this.deps.notifier.keepAwake();
|
|
443
448
|
let workspaceDir = null;
|
|
@@ -470,6 +475,18 @@ class SessionRunner {
|
|
|
470
475
|
const refusal = modelGatewayRefusal(lease);
|
|
471
476
|
if (refusal)
|
|
472
477
|
throw new Error(refusal);
|
|
478
|
+
// The catalogue is advisory. Fetch it while the independent workspace
|
|
479
|
+
// is prepared, then install it before the backend process starts.
|
|
480
|
+
const endModels = coldStart.begin('model_catalogue');
|
|
481
|
+
const gatewayModels = session.agent.model.gatewayUrl
|
|
482
|
+
? (0, model_gateway_1.fetchGatewayModels)({
|
|
483
|
+
baseUrl: session.agent.model.gatewayUrl,
|
|
484
|
+
apiKey: lease.modelToken
|
|
485
|
+
}).finally(endModels)
|
|
486
|
+
: Promise.resolve([]);
|
|
487
|
+
if (!session.agent.model.gatewayUrl)
|
|
488
|
+
endModels();
|
|
489
|
+
const endWorkspace = coldStart.begin('workspace_prepare');
|
|
473
490
|
try {
|
|
474
491
|
workspaceDir = await this.deps.workspace.prepare({
|
|
475
492
|
sessionId: session.sessionId,
|
|
@@ -486,6 +503,7 @@ class SessionRunner {
|
|
|
486
503
|
},
|
|
487
504
|
refusalReason: session.workspace.refusalReason
|
|
488
505
|
});
|
|
506
|
+
endWorkspace();
|
|
489
507
|
const repositoryNames = session.workspace.requestedRepositories ??
|
|
490
508
|
(session.workspace.repositories ?? []).map(({ fullName }) => fullName);
|
|
491
509
|
for (const fullName of repositoryNames) {
|
|
@@ -495,6 +513,7 @@ class SessionRunner {
|
|
|
495
513
|
}
|
|
496
514
|
}
|
|
497
515
|
catch (caught) {
|
|
516
|
+
endWorkspace();
|
|
498
517
|
const reason = caught instanceof Error ? caught.message : String(caught);
|
|
499
518
|
const status = session.workspace.refusalReason ? 'refused' : 'failed';
|
|
500
519
|
const names = session.workspace.requestedRepositories ??
|
|
@@ -530,7 +549,13 @@ class SessionRunner {
|
|
|
530
549
|
*/
|
|
531
550
|
const hasDroppedAttachments = (session.input.droppedAttachments?.length ?? 0) > 0;
|
|
532
551
|
if (workspaceDir) {
|
|
533
|
-
|
|
552
|
+
const endSpaceFiles = coldStart.begin('space_files_sync');
|
|
553
|
+
try {
|
|
554
|
+
await this.syncSpaceFilesForTurn(session, workspaceDir);
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
endSpaceFiles();
|
|
558
|
+
}
|
|
534
559
|
}
|
|
535
560
|
if (workspaceDir && (hasLeasedAttachments || hasDroppedAttachments)) {
|
|
536
561
|
const sync = await (0, attachments_1.syncAttachmentsToWorkspace)(session.input.attachments, workspaceDir, {
|
|
@@ -585,10 +610,7 @@ class SessionRunner {
|
|
|
585
610
|
* stop a turn whose model is already known, so we fall back to exactly
|
|
586
611
|
* the previous behaviour.
|
|
587
612
|
*/
|
|
588
|
-
const fetched = await
|
|
589
|
-
baseUrl: session.agent.model.gatewayUrl,
|
|
590
|
-
apiKey: lease.modelToken
|
|
591
|
-
});
|
|
613
|
+
const fetched = await gatewayModels;
|
|
592
614
|
const named = session.agent.model.model
|
|
593
615
|
? [
|
|
594
616
|
{
|
|
@@ -622,7 +644,13 @@ class SessionRunner {
|
|
|
622
644
|
this.deps.modelGateway?.set(null);
|
|
623
645
|
}
|
|
624
646
|
adapter = this.deps.getAdapter(session.agent.backend);
|
|
625
|
-
|
|
647
|
+
const endAdapter = coldStart.begin('adapter_initialize');
|
|
648
|
+
try {
|
|
649
|
+
await adapter.initialize();
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
endAdapter();
|
|
653
|
+
}
|
|
626
654
|
secretToken = this.setUpSecrets(session);
|
|
627
655
|
/*
|
|
628
656
|
* THE KNOWLEDGE TOOLS FOR THIS TURN (P9). Bound to this lease and this
|
|
@@ -638,6 +666,7 @@ class SessionRunner {
|
|
|
638
666
|
knowledgeUrl = await this.deps.knowledge.host.endpointFor(session.sessionId);
|
|
639
667
|
}
|
|
640
668
|
const config = this.buildSessionConfig(session, workspaceDir, secretToken, signal, knowledgeUrl);
|
|
669
|
+
config.coldStartLog = this.deps.coldStartLog;
|
|
641
670
|
/*
|
|
642
671
|
* REMEMBER THE CONVERSATION. This block is the fix for the defect that
|
|
643
672
|
* made this a chat product that could not hold a conversation: the
|
|
@@ -749,7 +778,13 @@ class SessionRunner {
|
|
|
749
778
|
}
|
|
750
779
|
}
|
|
751
780
|
if (!backendSessionId) {
|
|
752
|
-
|
|
781
|
+
const endBackend = coldStart.begin('backend_process_start');
|
|
782
|
+
try {
|
|
783
|
+
backendSessionId = await adapter.createSession(config);
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
endBackend();
|
|
787
|
+
}
|
|
753
788
|
this.deps.telemetry.record('provider.session.started', {
|
|
754
789
|
backend: session.agent.backend,
|
|
755
790
|
runtime: session.runtime
|
|
@@ -789,10 +824,18 @@ class SessionRunner {
|
|
|
789
824
|
: promptText;
|
|
790
825
|
replayed = !resumed && memory.messages.length > 0;
|
|
791
826
|
priorPartIdsForLog = priorPartIds;
|
|
792
|
-
|
|
827
|
+
const endPrompt = coldStart.begin('model_prompt_send');
|
|
828
|
+
try {
|
|
829
|
+
await adapter.sendPrompt(backendSessionId, [{ type: 'text', text: promptWithHostNote, role: 'user' }], config);
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
endPrompt();
|
|
833
|
+
}
|
|
793
834
|
this.deps.telemetry.record('provider.turn.sent', {
|
|
794
835
|
backend: session.agent.backend
|
|
795
836
|
});
|
|
837
|
+
const endFirstOutput = coldStart.begin('first_agent_output');
|
|
838
|
+
const endFirstIngest = coldStart.begin('first_transcript_ingest');
|
|
796
839
|
/*
|
|
797
840
|
* WIRE 3 NEEDS A WAY IN. `run` is one long await, and the user's answer
|
|
798
841
|
* arrives on a control-plane command that the harness handles on a
|
|
@@ -817,7 +860,7 @@ class SessionRunner {
|
|
|
817
860
|
resumed ? priorPartIds : new Set(),
|
|
818
861
|
// THE PROMPT THIS RUNNER JUST SENT. Drain uses it to recognise the
|
|
819
862
|
// backend's own echo of it — see `drain`.
|
|
820
|
-
promptWithHostNote);
|
|
863
|
+
promptWithHostNote, () => endFirstOutput(), () => endFirstIngest());
|
|
821
864
|
partsProduced = drained.parts;
|
|
822
865
|
if (signal.aborted) {
|
|
823
866
|
stopReason = 'aborted';
|
|
@@ -1031,6 +1074,7 @@ class SessionRunner {
|
|
|
1031
1074
|
* the drain. That has not been true since the error path was added.
|
|
1032
1075
|
*/
|
|
1033
1076
|
await this.flush(session.sessionId, this.pending.splice(0), turn, true, modelMessages);
|
|
1077
|
+
coldStart.summary();
|
|
1034
1078
|
this.deps.telemetry.record('provider.turn.memory', {
|
|
1035
1079
|
resumed,
|
|
1036
1080
|
replayed,
|
|
@@ -1101,7 +1145,7 @@ class SessionRunner {
|
|
|
1101
1145
|
*/
|
|
1102
1146
|
async drain(adapter, backendSessionId, config, session, signal, alreadySeenPartIds = new Set(),
|
|
1103
1147
|
/** The exact text `sendPrompt` was given this turn, for echo filtering. */
|
|
1104
|
-
sentPromptText = null) {
|
|
1148
|
+
sentPromptText = null, onFirstOutput, onFirstIngest) {
|
|
1105
1149
|
let providerError;
|
|
1106
1150
|
let timedOut = false;
|
|
1107
1151
|
let idleTimedOut = false;
|
|
@@ -1110,6 +1154,7 @@ class SessionRunner {
|
|
|
1110
1154
|
const seenPartIds = new Set(alreadySeenPartIds);
|
|
1111
1155
|
const partContentLengths = new Map();
|
|
1112
1156
|
let produced = 0;
|
|
1157
|
+
let firstOutputSeen = false;
|
|
1113
1158
|
let idleSeen = 0;
|
|
1114
1159
|
let wake = null;
|
|
1115
1160
|
adapter.onDataAvailable = () => {
|
|
@@ -1236,8 +1281,14 @@ class SessionRunner {
|
|
|
1236
1281
|
*/
|
|
1237
1282
|
produced += await this.emitPendingApproval(adapter, backendSessionId, session);
|
|
1238
1283
|
if (freshParts.length > 0) {
|
|
1284
|
+
if (!firstOutputSeen) {
|
|
1285
|
+
firstOutputSeen = true;
|
|
1286
|
+
onFirstOutput?.();
|
|
1287
|
+
}
|
|
1239
1288
|
produced += this.record(session.sessionId, freshParts);
|
|
1240
1289
|
await this.flush(session.sessionId, this.pending.splice(0), undefined, false);
|
|
1290
|
+
if (firstOutputSeen)
|
|
1291
|
+
onFirstIngest?.();
|
|
1241
1292
|
// Real output resets the quiet counter. A backend is idle BETWEEN
|
|
1242
1293
|
// tool calls as well as at the end of a turn, so ending on the first
|
|
1243
1294
|
// idle would truncate almost every transcript at the first tool.
|
|
@@ -2142,6 +2193,7 @@ class SessionRunner {
|
|
|
2142
2193
|
// would have made them no longer "extracted unchanged", which is worth
|
|
2143
2194
|
// more than the tidier name.
|
|
2144
2195
|
taskId: session.sessionId,
|
|
2196
|
+
turnId: session.turnId,
|
|
2145
2197
|
workspaceDir,
|
|
2146
2198
|
devServerPolicy: session.agent.devServerPolicy,
|
|
2147
2199
|
model: qualifiedModelId(session.agent.model),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peakflo/agent-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-rc.1",
|
|
4
4
|
"description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|