agent-scenario-loop 0.1.2 → 0.1.4
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 +9 -9
- package/app/profile-session.ts +352 -12
- package/dist/core/agent-summary.d.ts +3 -2
- package/dist/core/agent-summary.js +44 -2
- package/dist/core/artifact-contract.d.ts +28 -8
- package/dist/core/artifact-contract.js +676 -26
- package/dist/core/comparison.d.ts +57 -3
- package/dist/core/comparison.js +113 -1
- package/dist/core/planner.d.ts +32 -1
- package/dist/core/planner.js +144 -0
- package/dist/core/run-index.d.ts +4 -0
- package/dist/core/run-index.js +55 -1
- package/dist/core/schema-validator.d.ts +2 -0
- package/dist/core/schema-validator.js +2 -0
- package/dist/runner/android-adb-driver.d.ts +7 -2
- package/dist/runner/android-adb-driver.js +7 -1
- package/dist/runner/android-adb.d.ts +40 -5
- package/dist/runner/android-adb.js +1046 -664
- package/dist/runner/compare-latest.d.ts +8 -4
- package/dist/runner/compare-latest.js +24 -5
- package/dist/runner/example-android-live.d.ts +10 -1
- package/dist/runner/example-android-live.js +55 -0
- package/dist/runner/example-ios-live.d.ts +10 -1
- package/dist/runner/example-ios-live.js +55 -0
- package/dist/runner/ios-simctl.d.ts +6 -0
- package/dist/runner/ios-simctl.js +7 -0
- package/dist/runner/live-comparison.d.ts +2 -2
- package/dist/runner/live-comparison.js +2 -1
- package/dist/runner/live-proof-summary.d.ts +5 -4
- package/dist/runner/live-proof-summary.js +12 -2
- package/dist/runner/live-proof.d.ts +3 -2
- package/dist/runner/live-proof.js +9 -2
- package/dist/runner/profile-android.d.ts +16 -1
- package/dist/runner/profile-android.js +364 -26
- package/dist/runner/profile-ios.d.ts +13 -2
- package/dist/runner/profile-ios.js +341 -19
- package/dist/runner/profile-mobile.d.ts +39 -3
- package/dist/runner/profile-mobile.js +1054 -42
- package/dist/runner/validate-project.js +3 -0
- package/dist/scripts/consumer-rehearsal.d.ts +119 -0
- package/dist/scripts/consumer-rehearsal.js +757 -0
- package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
- package/dist/scripts/downstream-local-package-gate.js +264 -0
- package/dist/scripts/package-smoke.d.ts +96 -0
- package/dist/scripts/package-smoke.js +2282 -0
- package/dist/scripts/release-readiness.d.ts +2 -0
- package/dist/scripts/release-readiness.js +520 -0
- package/docs/adapters.md +7 -1
- package/docs/api.md +2 -2
- package/docs/architecture.md +90 -0
- package/docs/authoring.md +39 -3
- package/docs/concepts.md +3 -24
- package/docs/consumer-rehearsal.md +31 -1
- package/docs/contracts.md +45 -101
- package/docs/external-adapter-protocol.md +219 -0
- package/docs/live-proofs.md +86 -3
- package/docs/principles.md +9 -15
- package/examples/mobile-app/README.md +12 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
- package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
- package/examples/runners/README.md +4 -3
- package/examples/runners/adb-android.json +1 -0
- package/examples/runners/agent-device-android.json +1 -0
- package/examples/runners/agent-device-ios.json +1 -0
- package/examples/runners/argent-android.json +1 -0
- package/examples/runners/argent-ios.json +1 -0
- package/examples/runners/axe-accessibility-provider.json +2 -2
- package/examples/runners/script-accessibility-provider.json +2 -2
- package/examples/runners/script-memory-provider.json +2 -2
- package/examples/runners/script-network-provider.json +2 -2
- package/examples/runners/script-profiler-provider.json +2 -2
- package/examples/runners/xcodebuildmcp-ios.json +1 -0
- package/package.json +12 -3
- package/schemas/causal-run.schema.json +85 -2
- package/schemas/comparison.schema.json +130 -2
- package/schemas/external-adapter-message.schema.json +693 -0
- package/schemas/health.schema.json +72 -0
- package/schemas/live-proof-set.schema.json +1 -1
- package/schemas/live-proof.schema.json +14 -6
- package/schemas/manifest.schema.json +515 -4
- package/schemas/profiler.schema.json +243 -0
- package/schemas/runner-capabilities.schema.json +28 -2
- package/schemas/scenario.schema.json +34 -2
- package/templates/evidence-provider.json +3 -3
- package/templates/primary-runner.json +1 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
package/README.md
CHANGED
|
@@ -14,12 +14,14 @@ Execution tools can change. The scenario and evidence contract should not.
|
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| Understand the idea in plain language | [Concepts](docs/concepts.md) |
|
|
16
16
|
| Understand the project doctrine | [Principles](docs/principles.md) |
|
|
17
|
-
|
|
|
18
|
-
|
|
|
17
|
+
| Understand why ASL is a protocol, not a TypeScript-only library | [Architecture](docs/architecture.md) |
|
|
18
|
+
| Implement or evaluate an out-of-process adapter in any language | [External Adapter Protocol](docs/external-adapter-protocol.md) |
|
|
19
19
|
| Inspect artifacts, schemas, and supported surfaces | [Contracts](docs/contracts.md) |
|
|
20
|
-
|
|
|
20
|
+
| Write your first scenario | [Scenario Authoring](docs/authoring.md) |
|
|
21
21
|
| Add a runner or evidence provider | [Adapter Onboarding](docs/adapters.md) |
|
|
22
|
+
| Rehearse adoption in an existing app | [Consumer App Rehearsal](docs/consumer-rehearsal.md) |
|
|
22
23
|
| Run fixture, Android, or iOS proofs | [Live Proofs](docs/live-proofs.md) |
|
|
24
|
+
| Use the package from code | [Public API](docs/api.md) |
|
|
23
25
|
| Inspect runner behavior and limits | [Runner docs](runner/README.md) |
|
|
24
26
|
| Explore the neutral dogfood app | [examples/mobile-app](examples/mobile-app/README.md) |
|
|
25
27
|
| See runner and provider fixtures | [examples/runners](examples/runners/README.md) |
|
|
@@ -73,12 +75,6 @@ No simulator or device available yet? Run the fixture loop:
|
|
|
73
75
|
pnpm demo:loop -- --out artifacts/demo-loop
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
Read next:
|
|
77
|
-
|
|
78
|
-
- [Scenario Authoring](docs/authoring.md) for scenario shape and truth events
|
|
79
|
-
- [Consumer App Rehearsal](docs/consumer-rehearsal.md) for adoption in an existing app
|
|
80
|
-
- [Live Proofs](docs/live-proofs.md) for Android, iOS, comparison, and release-proof paths
|
|
81
|
-
|
|
82
78
|
## Package Surface
|
|
83
79
|
|
|
84
80
|
The root package exports stable core contracts:
|
|
@@ -123,3 +119,7 @@ pnpm release:check
|
|
|
123
119
|
```
|
|
124
120
|
|
|
125
121
|
The package should remain product-neutral. Product-specific selectors, routes, auth assumptions, accounts, and scenario data belong in the consuming app, not in this repository.
|
|
122
|
+
|
|
123
|
+
## Read next
|
|
124
|
+
|
|
125
|
+
- [Concepts](docs/concepts.md) for the plain-language model
|
package/app/profile-session.ts
CHANGED
|
@@ -12,11 +12,17 @@ export type ProfileSessionState = {
|
|
|
12
12
|
|
|
13
13
|
export type ProfileSessionCommand = {
|
|
14
14
|
id: string;
|
|
15
|
+
commandId?: string;
|
|
15
16
|
scenario?: string;
|
|
16
17
|
runId?: string;
|
|
17
18
|
command: string;
|
|
19
|
+
queueId?: string;
|
|
20
|
+
sequence?: number;
|
|
18
21
|
source?: 'deeplink' | 'storage';
|
|
19
22
|
timestamp: number;
|
|
23
|
+
waitForMilestone?: string;
|
|
24
|
+
waitMs?: number;
|
|
25
|
+
waitTimeoutMs?: number;
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
export type ProfileSignalKind = 'js' | 'memory' | 'network';
|
|
@@ -62,13 +68,35 @@ type StoredProfileSessionEntry = {
|
|
|
62
68
|
scenario: string;
|
|
63
69
|
runId: string;
|
|
64
70
|
timestamp: number;
|
|
71
|
+
atMs?: number;
|
|
65
72
|
startedAt?: number;
|
|
66
73
|
stoppedAt?: number;
|
|
67
74
|
command?: string;
|
|
75
|
+
commandId?: string;
|
|
68
76
|
id?: string;
|
|
77
|
+
queueId?: string;
|
|
78
|
+
reason?: string;
|
|
79
|
+
result?: string;
|
|
80
|
+
sequence?: number;
|
|
81
|
+
source?: 'deeplink' | 'storage';
|
|
82
|
+
status?: 'received' | 'queued' | 'delivered' | 'completed' | 'skipped';
|
|
83
|
+
waitForMilestone?: string;
|
|
84
|
+
waitMs?: number;
|
|
85
|
+
waitTimeoutMs?: number;
|
|
69
86
|
};
|
|
70
87
|
|
|
71
88
|
type StoredProfileSignals = Record<ProfileSignalKind, Record<string, unknown>>;
|
|
89
|
+
type ProfileCommandMilestoneGate = {
|
|
90
|
+
commandId?: string;
|
|
91
|
+
id: string;
|
|
92
|
+
milestone: string;
|
|
93
|
+
queueId?: string;
|
|
94
|
+
runId?: string;
|
|
95
|
+
scenario?: string;
|
|
96
|
+
sequence?: number;
|
|
97
|
+
timeoutId?: ReturnType<typeof setTimeout>;
|
|
98
|
+
waitMs?: number;
|
|
99
|
+
};
|
|
72
100
|
|
|
73
101
|
const INITIAL_STATE: ProfileSessionState = {
|
|
74
102
|
active: false,
|
|
@@ -97,9 +125,15 @@ const listeners = new Set<() => void>();
|
|
|
97
125
|
const profileCommandListeners = new Set<(command: ProfileSessionCommand) => void>();
|
|
98
126
|
const profileCommandTargetHandlers = new Map<string, () => void>();
|
|
99
127
|
const pendingProfileCommands: ProfileSessionCommand[] = [];
|
|
128
|
+
const sequencedProfileCommands: ProfileSessionCommand[] = [];
|
|
129
|
+
const observedProfileEvents: StoredProfileEvent[] = [];
|
|
100
130
|
const processedProfileCommandIds = new Set<string>();
|
|
101
131
|
let lastProfileCommandSignature: string | null = null;
|
|
102
132
|
let lastProfileCommandTimestamp = 0;
|
|
133
|
+
let profileCommandMilestoneGate: ProfileCommandMilestoneGate | null = null;
|
|
134
|
+
let profileCommandProcessingScheduled = false;
|
|
135
|
+
let profileCommandProcessingTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
136
|
+
let profileCommandProcessingAvailableAt = 0;
|
|
103
137
|
|
|
104
138
|
function writeProfileLog(line: string) {
|
|
105
139
|
if (Platform.OS === 'ios') {
|
|
@@ -191,6 +225,7 @@ function appendStoredProfileEvent(event: StoredProfileEvent) {
|
|
|
191
225
|
}
|
|
192
226
|
|
|
193
227
|
function resetStoredProfileArtifacts() {
|
|
228
|
+
observedProfileEvents.length = 0;
|
|
194
229
|
queueProfileStorageMutation(async () => {
|
|
195
230
|
await Promise.all([
|
|
196
231
|
AsyncStorage.removeItem(PROFILE_EVENT_STORAGE_KEY),
|
|
@@ -202,6 +237,9 @@ function resetStoredProfileArtifacts() {
|
|
|
202
237
|
|
|
203
238
|
function clearPendingProfileCommands() {
|
|
204
239
|
pendingProfileCommands.length = 0;
|
|
240
|
+
sequencedProfileCommands.length = 0;
|
|
241
|
+
clearProfileCommandMilestoneGate();
|
|
242
|
+
clearProfileCommandProcessingSchedule();
|
|
205
243
|
queueProfileStorageMutation(async () => {
|
|
206
244
|
await AsyncStorage.removeItem(PROFILE_COMMAND_STORAGE_KEY);
|
|
207
245
|
});
|
|
@@ -285,7 +323,21 @@ export function isProfileSessionFresh(
|
|
|
285
323
|
}
|
|
286
324
|
|
|
287
325
|
function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<string, unknown>) {
|
|
288
|
-
|
|
326
|
+
const timestamp = Date.now();
|
|
327
|
+
const sessionStartedAt = readProfileSessionStartedAt(profileSessionState);
|
|
328
|
+
const atMs =
|
|
329
|
+
typeof payload.atMs === 'number' && Number.isFinite(payload.atMs)
|
|
330
|
+
? payload.atMs
|
|
331
|
+
: sessionStartedAt !== null
|
|
332
|
+
? Math.max(0, timestamp - sessionStartedAt)
|
|
333
|
+
: undefined;
|
|
334
|
+
const logPayload = {
|
|
335
|
+
kind,
|
|
336
|
+
...payload,
|
|
337
|
+
timestamp,
|
|
338
|
+
...(atMs !== undefined ? { atMs } : {}),
|
|
339
|
+
};
|
|
340
|
+
writeProfileLog(buildLogLine('profile-session', logPayload));
|
|
289
341
|
|
|
290
342
|
const scenario = typeof payload.scenario === 'string' ? payload.scenario : null;
|
|
291
343
|
const runId = typeof payload.runId === 'string' ? payload.runId : null;
|
|
@@ -293,12 +345,12 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
|
|
|
293
345
|
return;
|
|
294
346
|
}
|
|
295
347
|
|
|
296
|
-
const timestamp = Date.now();
|
|
297
348
|
const entry: StoredProfileSessionEntry = {
|
|
298
349
|
kind,
|
|
299
350
|
scenario,
|
|
300
351
|
runId,
|
|
301
352
|
timestamp,
|
|
353
|
+
...(atMs !== undefined ? { atMs } : {}),
|
|
302
354
|
};
|
|
303
355
|
|
|
304
356
|
if (kind === 'start' && typeof payload.startedAt === 'number') {
|
|
@@ -316,6 +368,44 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
|
|
|
316
368
|
if (typeof payload.id === 'string') {
|
|
317
369
|
entry.id = payload.id;
|
|
318
370
|
}
|
|
371
|
+
if (typeof payload.commandId === 'string') {
|
|
372
|
+
entry.commandId = payload.commandId;
|
|
373
|
+
} else if (typeof payload.id === 'string') {
|
|
374
|
+
entry.commandId = payload.id;
|
|
375
|
+
}
|
|
376
|
+
if (typeof payload.queueId === 'string') {
|
|
377
|
+
entry.queueId = payload.queueId;
|
|
378
|
+
}
|
|
379
|
+
if (typeof payload.sequence === 'number') {
|
|
380
|
+
entry.sequence = payload.sequence;
|
|
381
|
+
}
|
|
382
|
+
if (payload.source === 'deeplink' || payload.source === 'storage') {
|
|
383
|
+
entry.source = payload.source;
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
payload.status === 'received' ||
|
|
387
|
+
payload.status === 'queued' ||
|
|
388
|
+
payload.status === 'delivered' ||
|
|
389
|
+
payload.status === 'completed' ||
|
|
390
|
+
payload.status === 'skipped'
|
|
391
|
+
) {
|
|
392
|
+
entry.status = payload.status;
|
|
393
|
+
}
|
|
394
|
+
if (typeof payload.reason === 'string') {
|
|
395
|
+
entry.reason = payload.reason;
|
|
396
|
+
}
|
|
397
|
+
if (typeof payload.result === 'string') {
|
|
398
|
+
entry.result = payload.result;
|
|
399
|
+
}
|
|
400
|
+
if (typeof payload.waitForMilestone === 'string') {
|
|
401
|
+
entry.waitForMilestone = payload.waitForMilestone;
|
|
402
|
+
}
|
|
403
|
+
if (typeof payload.waitMs === 'number') {
|
|
404
|
+
entry.waitMs = payload.waitMs;
|
|
405
|
+
}
|
|
406
|
+
if (typeof payload.waitTimeoutMs === 'number') {
|
|
407
|
+
entry.waitTimeoutMs = payload.waitTimeoutMs;
|
|
408
|
+
}
|
|
319
409
|
}
|
|
320
410
|
|
|
321
411
|
appendStoredProfileSessionEntry(entry);
|
|
@@ -326,6 +416,12 @@ function getProfileSessionRoute(url: string): {
|
|
|
326
416
|
scenario?: string;
|
|
327
417
|
runId?: string;
|
|
328
418
|
command?: string;
|
|
419
|
+
commandId?: string;
|
|
420
|
+
queueId?: string;
|
|
421
|
+
sequence?: number;
|
|
422
|
+
waitForMilestone?: string;
|
|
423
|
+
waitMs?: number;
|
|
424
|
+
waitTimeoutMs?: number;
|
|
329
425
|
} | null {
|
|
330
426
|
const parsed = ExpoLinking.parse(url);
|
|
331
427
|
const segments = [parsed.hostname, parsed.path]
|
|
@@ -347,8 +443,26 @@ function getProfileSessionRoute(url: string): {
|
|
|
347
443
|
typeof parsed.queryParams?.runId === 'string' ? parsed.queryParams.runId : undefined;
|
|
348
444
|
const command =
|
|
349
445
|
typeof parsed.queryParams?.command === 'string' ? parsed.queryParams.command : undefined;
|
|
350
|
-
|
|
351
|
-
|
|
446
|
+
const commandId =
|
|
447
|
+
typeof parsed.queryParams?.commandId === 'string' ? parsed.queryParams.commandId : undefined;
|
|
448
|
+
const sequence =
|
|
449
|
+
typeof parsed.queryParams?.sequence === 'string' && Number.isInteger(Number(parsed.queryParams.sequence))
|
|
450
|
+
? Number(parsed.queryParams.sequence)
|
|
451
|
+
: undefined;
|
|
452
|
+
const queueId =
|
|
453
|
+
typeof parsed.queryParams?.queueId === 'string' ? parsed.queryParams.queueId : undefined;
|
|
454
|
+
const waitForMilestone =
|
|
455
|
+
typeof parsed.queryParams?.waitForMilestone === 'string' ? parsed.queryParams.waitForMilestone : undefined;
|
|
456
|
+
const waitTimeoutMs =
|
|
457
|
+
typeof parsed.queryParams?.waitTimeoutMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitTimeoutMs))
|
|
458
|
+
? Number(parsed.queryParams.waitTimeoutMs)
|
|
459
|
+
: undefined;
|
|
460
|
+
const waitMs =
|
|
461
|
+
typeof parsed.queryParams?.waitMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitMs))
|
|
462
|
+
? Number(parsed.queryParams.waitMs)
|
|
463
|
+
: undefined;
|
|
464
|
+
|
|
465
|
+
return { action, scenario, runId, command, commandId, queueId, sequence, waitForMilestone, waitMs, waitTimeoutMs };
|
|
352
466
|
}
|
|
353
467
|
|
|
354
468
|
function queuePendingProfileCommand(command: ProfileSessionCommand) {
|
|
@@ -440,6 +554,132 @@ function markProfileCommandIdProcessed(command: ProfileSessionCommand) {
|
|
|
440
554
|
}
|
|
441
555
|
}
|
|
442
556
|
|
|
557
|
+
function compareProfileCommands(left: ProfileSessionCommand, right: ProfileSessionCommand): number {
|
|
558
|
+
const leftSequence = typeof left.sequence === 'number' ? left.sequence : Number.POSITIVE_INFINITY;
|
|
559
|
+
const rightSequence = typeof right.sequence === 'number' ? right.sequence : Number.POSITIVE_INFINITY;
|
|
560
|
+
if (leftSequence !== rightSequence) {
|
|
561
|
+
return leftSequence - rightSequence;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return left.timestamp - right.timestamp;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function shouldQueueProfileCommand(command: ProfileSessionCommand): boolean {
|
|
568
|
+
return !hasProcessedProfileCommandId(command) &&
|
|
569
|
+
!sequencedProfileCommands.some((queuedCommand) => queuedCommand.id === command.id);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function buildProfileCommandMilestoneGate(command: ProfileSessionCommand): ProfileCommandMilestoneGate | null {
|
|
573
|
+
if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
id: command.id,
|
|
579
|
+
milestone: command.waitForMilestone,
|
|
580
|
+
...(typeof command.commandId === 'string' ? { commandId: command.commandId } : {}),
|
|
581
|
+
...(typeof command.queueId === 'string' ? { queueId: command.queueId } : {}),
|
|
582
|
+
...(typeof command.runId === 'string' ? { runId: command.runId } : {}),
|
|
583
|
+
...(typeof command.scenario === 'string' ? { scenario: command.scenario } : {}),
|
|
584
|
+
...(typeof command.sequence === 'number' ? { sequence: command.sequence } : {}),
|
|
585
|
+
...(typeof command.waitMs === 'number' && command.waitMs > 0 ? { waitMs: command.waitMs } : {}),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function hasObservedProfileCommandMilestone(command: ProfileSessionCommand): boolean {
|
|
590
|
+
if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
if (typeof command.sequence === 'number' && command.sequence > 1) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return observedProfileEvents.some((eventPayload) => (
|
|
598
|
+
eventPayload.event === command.waitForMilestone &&
|
|
599
|
+
(!command.runId || eventPayload.runId === command.runId) &&
|
|
600
|
+
(!command.scenario || eventPayload.scenario === command.scenario)
|
|
601
|
+
));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function clearProfileCommandMilestoneGate() {
|
|
605
|
+
if (profileCommandMilestoneGate?.timeoutId) {
|
|
606
|
+
clearTimeout(profileCommandMilestoneGate.timeoutId);
|
|
607
|
+
}
|
|
608
|
+
profileCommandMilestoneGate = null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function clearProfileCommandProcessingSchedule() {
|
|
612
|
+
if (profileCommandProcessingTimeoutId) {
|
|
613
|
+
clearTimeout(profileCommandProcessingTimeoutId);
|
|
614
|
+
}
|
|
615
|
+
profileCommandProcessingTimeoutId = null;
|
|
616
|
+
profileCommandProcessingScheduled = false;
|
|
617
|
+
profileCommandProcessingAvailableAt = 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function startProfileCommandMilestoneTimeout(command: ProfileSessionCommand) {
|
|
621
|
+
if (
|
|
622
|
+
!profileCommandMilestoneGate ||
|
|
623
|
+
typeof command.waitTimeoutMs !== 'number' ||
|
|
624
|
+
command.waitTimeoutMs <= 0
|
|
625
|
+
) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
profileCommandMilestoneGate.timeoutId = setTimeout(() => {
|
|
630
|
+
if (!profileCommandMilestoneGate || profileCommandMilestoneGate.id !== command.id) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
logProfileSession('command', {
|
|
635
|
+
...command,
|
|
636
|
+
status: 'skipped',
|
|
637
|
+
reason: 'wait-for-milestone-timeout',
|
|
638
|
+
});
|
|
639
|
+
clearProfileCommandMilestoneGate();
|
|
640
|
+
processSequencedProfileCommands();
|
|
641
|
+
}, command.waitTimeoutMs);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function scheduleProfileCommandProcessing(waitMs = 0) {
|
|
645
|
+
const availableAt = waitMs > 0 ? Date.now() + waitMs : 0;
|
|
646
|
+
if (availableAt > profileCommandProcessingAvailableAt) {
|
|
647
|
+
profileCommandProcessingAvailableAt = availableAt;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const delayMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
|
|
651
|
+
if (profileCommandProcessingTimeoutId) {
|
|
652
|
+
clearTimeout(profileCommandProcessingTimeoutId);
|
|
653
|
+
profileCommandProcessingTimeoutId = null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
profileCommandProcessingScheduled = true;
|
|
657
|
+
const run = () => {
|
|
658
|
+
profileCommandProcessingTimeoutId = null;
|
|
659
|
+
const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
|
|
660
|
+
if (remainingMs > 0) {
|
|
661
|
+
profileCommandProcessingScheduled = false;
|
|
662
|
+
scheduleProfileCommandProcessing(remainingMs);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
profileCommandProcessingAvailableAt = 0;
|
|
666
|
+
profileCommandProcessingScheduled = false;
|
|
667
|
+
processSequencedProfileCommands();
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
if (delayMs > 0) {
|
|
671
|
+
profileCommandProcessingTimeoutId = setTimeout(run, delayMs);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof queueMicrotask === 'function') {
|
|
676
|
+
queueMicrotask(run);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
Promise.resolve().then(run);
|
|
681
|
+
}
|
|
682
|
+
|
|
443
683
|
function notifyProfileCommandListeners(command: ProfileSessionCommand) {
|
|
444
684
|
const commandTimestamp = Number.isFinite(command.timestamp) ? command.timestamp : Date.now();
|
|
445
685
|
if (shouldSkipProfileCommandForDuplicateWindow(command, commandTimestamp)) {
|
|
@@ -456,17 +696,108 @@ function notifyProfileCommandListeners(command: ProfileSessionCommand) {
|
|
|
456
696
|
|
|
457
697
|
const targetDispatched = dispatchProfileCommandTarget(command);
|
|
458
698
|
if (targetDispatched) {
|
|
699
|
+
logProfileSession('command', {
|
|
700
|
+
...command,
|
|
701
|
+
status: 'completed',
|
|
702
|
+
result: 'target-dispatched',
|
|
703
|
+
});
|
|
459
704
|
return;
|
|
460
705
|
}
|
|
461
706
|
|
|
462
707
|
if (profileCommandListeners.size === 0) {
|
|
463
708
|
queuePendingProfileCommand(command);
|
|
709
|
+
logProfileSession('command', {
|
|
710
|
+
...command,
|
|
711
|
+
status: 'queued',
|
|
712
|
+
reason: 'no-command-listener',
|
|
713
|
+
});
|
|
464
714
|
return;
|
|
465
715
|
}
|
|
466
716
|
|
|
467
717
|
for (const listener of profileCommandListeners) {
|
|
468
718
|
listener(command);
|
|
469
719
|
}
|
|
720
|
+
logProfileSession('command', {
|
|
721
|
+
...command,
|
|
722
|
+
status: 'delivered',
|
|
723
|
+
result: 'listener-notified',
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function processSequencedProfileCommands() {
|
|
728
|
+
if (profileCommandProcessingScheduled) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
|
|
732
|
+
if (remainingMs > 0) {
|
|
733
|
+
scheduleProfileCommandProcessing(remainingMs);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (profileCommandMilestoneGate) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
while (sequencedProfileCommands.length > 0) {
|
|
741
|
+
const command = sequencedProfileCommands.shift();
|
|
742
|
+
if (!command || hasProcessedProfileCommandId(command)) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
markProfileCommandIdProcessed(command);
|
|
747
|
+
const nextGate = hasObservedProfileCommandMilestone(command)
|
|
748
|
+
? null
|
|
749
|
+
: buildProfileCommandMilestoneGate(command);
|
|
750
|
+
profileCommandMilestoneGate = nextGate;
|
|
751
|
+
logProfileSession('command', {
|
|
752
|
+
...command,
|
|
753
|
+
status: 'received',
|
|
754
|
+
});
|
|
755
|
+
startProfileCommandMilestoneTimeout(command);
|
|
756
|
+
notifyProfileCommandListeners(command);
|
|
757
|
+
|
|
758
|
+
if (profileCommandMilestoneGate) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (typeof command.waitMs === 'number' && command.waitMs > 0) {
|
|
762
|
+
scheduleProfileCommandProcessing(command.waitMs);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function enqueueSequencedProfileCommands(commands: ProfileSessionCommand[]) {
|
|
769
|
+
const nextCommands = commands.filter(shouldQueueProfileCommand);
|
|
770
|
+
if (nextCommands.length === 0) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
sequencedProfileCommands.push(...nextCommands);
|
|
775
|
+
sequencedProfileCommands.sort(compareProfileCommands);
|
|
776
|
+
processSequencedProfileCommands();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function releaseProfileCommandMilestoneGate(eventPayload: StoredProfileEvent) {
|
|
780
|
+
if (!profileCommandMilestoneGate || profileCommandMilestoneGate.milestone !== eventPayload.event) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (
|
|
784
|
+
profileCommandMilestoneGate.runId &&
|
|
785
|
+
profileCommandMilestoneGate.runId !== eventPayload.runId
|
|
786
|
+
) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (
|
|
790
|
+
profileCommandMilestoneGate.scenario &&
|
|
791
|
+
profileCommandMilestoneGate.scenario !== eventPayload.scenario
|
|
792
|
+
) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const waitMs = typeof profileCommandMilestoneGate.waitMs === 'number'
|
|
797
|
+
? profileCommandMilestoneGate.waitMs
|
|
798
|
+
: 0;
|
|
799
|
+
clearProfileCommandMilestoneGate();
|
|
800
|
+
scheduleProfileCommandProcessing(waitMs);
|
|
470
801
|
}
|
|
471
802
|
|
|
472
803
|
function flushPendingProfileCommands(listener: (command: ProfileSessionCommand) => void) {
|
|
@@ -560,11 +891,16 @@ export function applyProfileSessionUrl(url: string | null | undefined): boolean
|
|
|
560
891
|
scenario: route.scenario,
|
|
561
892
|
runId: route.runId,
|
|
562
893
|
command: route.command,
|
|
894
|
+
...(route.commandId ? { commandId: route.commandId } : {}),
|
|
895
|
+
...(route.queueId ? { queueId: route.queueId } : {}),
|
|
896
|
+
...(typeof route.sequence === 'number' ? { sequence: route.sequence } : {}),
|
|
563
897
|
source: 'deeplink' as const,
|
|
564
898
|
timestamp,
|
|
899
|
+
...(route.waitForMilestone ? { waitForMilestone: route.waitForMilestone } : {}),
|
|
900
|
+
...(typeof route.waitMs === 'number' ? { waitMs: route.waitMs } : {}),
|
|
901
|
+
...(typeof route.waitTimeoutMs === 'number' ? { waitTimeoutMs: route.waitTimeoutMs } : {}),
|
|
565
902
|
};
|
|
566
|
-
|
|
567
|
-
notifyProfileCommandListeners(command);
|
|
903
|
+
enqueueSequencedProfileCommands([command]);
|
|
568
904
|
return true;
|
|
569
905
|
}
|
|
570
906
|
|
|
@@ -607,7 +943,12 @@ export function emitProfileEvent(event: string, metadata?: ProfileEventMetadata)
|
|
|
607
943
|
};
|
|
608
944
|
|
|
609
945
|
writeProfileLog(buildLogLine('profile-event', eventPayload));
|
|
946
|
+
observedProfileEvents.push(eventPayload);
|
|
947
|
+
while (observedProfileEvents.length > MAX_STORED_PROFILE_EVENTS) {
|
|
948
|
+
observedProfileEvents.shift();
|
|
949
|
+
}
|
|
610
950
|
appendStoredProfileEvent(eventPayload);
|
|
951
|
+
releaseProfileCommandMilestoneGate(eventPayload);
|
|
611
952
|
}
|
|
612
953
|
|
|
613
954
|
/**
|
|
@@ -747,7 +1088,7 @@ export function useProfileSessionBootstrap(): void {
|
|
|
747
1088
|
return;
|
|
748
1089
|
}
|
|
749
1090
|
|
|
750
|
-
|
|
1091
|
+
const nextCommands: ProfileSessionCommand[] = [];
|
|
751
1092
|
for (const command of storedCommands) {
|
|
752
1093
|
if (
|
|
753
1094
|
!command ||
|
|
@@ -760,8 +1101,7 @@ export function useProfileSessionBootstrap(): void {
|
|
|
760
1101
|
|
|
761
1102
|
if (
|
|
762
1103
|
command.scenario !== activeSession.scenario ||
|
|
763
|
-
command.runId !== activeSession.runId
|
|
764
|
-
(typeof activeSession.startedAt === 'number' && command.timestamp < activeSession.startedAt)
|
|
1104
|
+
command.runId !== activeSession.runId
|
|
765
1105
|
) {
|
|
766
1106
|
continue;
|
|
767
1107
|
}
|
|
@@ -775,10 +1115,10 @@ export function useProfileSessionBootstrap(): void {
|
|
|
775
1115
|
continue;
|
|
776
1116
|
}
|
|
777
1117
|
|
|
778
|
-
|
|
779
|
-
logProfileSession('command', storageCommand);
|
|
780
|
-
notifyProfileCommandListeners(storageCommand);
|
|
1118
|
+
nextCommands.push(storageCommand);
|
|
781
1119
|
}
|
|
1120
|
+
|
|
1121
|
+
enqueueSequencedProfileCommands(nextCommands);
|
|
782
1122
|
};
|
|
783
1123
|
|
|
784
1124
|
syncStoredProfileState()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Builds the minimum agent-facing markdown summary for a run.
|
|
3
3
|
*
|
|
4
|
-
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null}} options
|
|
4
|
+
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null, manifest?: Record<string, unknown> | null}} options
|
|
5
5
|
* @returns {string}
|
|
6
6
|
*/
|
|
7
|
-
declare function buildAgentSummaryMarkdown({ health, verdict, comparison }: AgentSummaryInput): string;
|
|
7
|
+
declare function buildAgentSummaryMarkdown({ health, verdict, comparison, manifest }: AgentSummaryInput): string;
|
|
8
8
|
export { buildAgentSummaryMarkdown, };
|
|
9
9
|
export type { AgentSummaryInput, SummaryRecord, };
|
|
10
10
|
type SummaryRecord = Record<string, unknown>;
|
|
@@ -12,4 +12,5 @@ type AgentSummaryInput = {
|
|
|
12
12
|
health: SummaryRecord;
|
|
13
13
|
verdict: SummaryRecord;
|
|
14
14
|
comparison?: SummaryRecord | null;
|
|
15
|
+
manifest?: SummaryRecord | null;
|
|
15
16
|
};
|
|
@@ -122,13 +122,54 @@ function formatComparisonBasis(comparison) {
|
|
|
122
122
|
}
|
|
123
123
|
return lines;
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Formats attempt terminal semantics for agent-readable summaries.
|
|
127
|
+
*
|
|
128
|
+
* @param {SummaryRecord | null | undefined} manifest
|
|
129
|
+
* @returns {string[]}
|
|
130
|
+
*/
|
|
131
|
+
function formatAttempt(manifest) {
|
|
132
|
+
const attempt = manifest?.attempt;
|
|
133
|
+
if (!attempt || typeof attempt !== 'object' || Array.isArray(attempt)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const attemptRecord = attempt;
|
|
137
|
+
const classification = attemptRecord.classification && typeof attemptRecord.classification === 'object' && !Array.isArray(attemptRecord.classification)
|
|
138
|
+
? attemptRecord.classification
|
|
139
|
+
: {};
|
|
140
|
+
const cleanup = attemptRecord.cleanup && typeof attemptRecord.cleanup === 'object' && !Array.isArray(attemptRecord.cleanup)
|
|
141
|
+
? attemptRecord.cleanup
|
|
142
|
+
: {};
|
|
143
|
+
const partialArtifacts = attemptRecord.partialArtifacts && typeof attemptRecord.partialArtifacts === 'object' && !Array.isArray(attemptRecord.partialArtifacts)
|
|
144
|
+
? attemptRecord.partialArtifacts
|
|
145
|
+
: {};
|
|
146
|
+
const retryOfAttemptId = firstString([attemptRecord.retryOfAttemptId], '');
|
|
147
|
+
const retryReason = firstString([attemptRecord.retryReason], '');
|
|
148
|
+
const lines = [
|
|
149
|
+
'',
|
|
150
|
+
'## attempt',
|
|
151
|
+
'',
|
|
152
|
+
`- Attempt: ${code(firstString([attemptRecord.attemptId], 'unknown-attempt'))} (${attemptRecord.attemptNumber ?? 'unknown'}/${attemptRecord.maxAttempts ?? 'unknown'})`,
|
|
153
|
+
`- Terminal state: ${code(firstString([attemptRecord.terminalState], 'unknown'))}`,
|
|
154
|
+
`- Classification: ${code(firstString([classification.category], 'unknown'))}${classification.code ? ` ${code(classification.code)}` : ''}`,
|
|
155
|
+
`- Cleanup: ${code(firstString([cleanup.status], 'unknown'))}`,
|
|
156
|
+
`- Partial artifacts valid: ${partialArtifacts.valid === true ? 'true' : 'false'} - ${firstString([partialArtifacts.reason], 'no reason recorded')}`,
|
|
157
|
+
];
|
|
158
|
+
if (retryOfAttemptId || retryReason) {
|
|
159
|
+
lines.push(`- Retry lineage: previous=${code(retryOfAttemptId || 'unknown')} reason=${retryReason || 'not recorded'}`);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(partialArtifacts.paths) && partialArtifacts.paths.length > 0) {
|
|
162
|
+
lines.push(`- Partial artifact paths: ${partialArtifacts.paths.map((item) => code(item)).join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
125
166
|
/**
|
|
126
167
|
* Builds the minimum agent-facing markdown summary for a run.
|
|
127
168
|
*
|
|
128
|
-
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null}} options
|
|
169
|
+
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>, comparison?: Record<string, unknown> | null, manifest?: Record<string, unknown> | null}} options
|
|
129
170
|
* @returns {string}
|
|
130
171
|
*/
|
|
131
|
-
function buildAgentSummaryMarkdown({ health, verdict, comparison = null }) {
|
|
172
|
+
function buildAgentSummaryMarkdown({ health, verdict, comparison = null, manifest = null }) {
|
|
132
173
|
const scenarioId = firstString([health?.scenarioId, verdict?.scenarioId], 'unknown-scenario');
|
|
133
174
|
const runId = firstString([health?.runId, verdict?.runId], 'unknown-run');
|
|
134
175
|
const healthStatus = firstString([health?.healthStatus], 'failed');
|
|
@@ -169,6 +210,7 @@ function buildAgentSummaryMarkdown({ health, verdict, comparison = null }) {
|
|
|
169
210
|
if (failedBudgets.length > 0) {
|
|
170
211
|
lines.push('', '## failed budgets', '', ...failedBudgets);
|
|
171
212
|
}
|
|
213
|
+
lines.push(...formatAttempt(manifest));
|
|
172
214
|
if (comparison) {
|
|
173
215
|
lines.push('', '## comparison', '', firstString([comparison.summary], 'No comparison summary provided.'));
|
|
174
216
|
lines.push(...formatComparisonBasis(comparison));
|