@zq-silk/yui 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +23 -4
- package/README.md +80 -13
- package/dist/cli/commandCatalog.js +30 -5
- package/dist/cli.js +105 -43
- package/dist/commands/configCommands.js +12 -45
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/projectCommands.js +581 -2
- package/dist/commands/taskCommands.js +159 -172
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +55 -11
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskIntegrationCommands.js +2 -1
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +24 -0
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +2 -2
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/controller.js +8 -12
- package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
- package/dist/controller/runtime.js +3 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +36 -16
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +1 -2
- package/dist/lifecycle/taskRoleSessionReset.js +5 -13
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/repository/checkoutSwap.js +61 -0
- package/dist/repository/gitWorkspace.js +36 -0
- package/dist/repository/project.js +53 -3
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtimeObservation.js +13 -0
- package/dist/runtime/runtimeProjection.js +16 -25
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/scheduler/activeRoleRunDelivery.js +4 -4
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +3 -5
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +0 -14
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +467 -0
- package/dist/storage/sqliteSchema.js +18 -8
- package/dist/storage/sqliteStore.js +61 -21
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +47 -31
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -24
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +4 -0
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +53 -16
- package/skills/yui-operator/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +28 -7
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -22,8 +22,8 @@ export const CONFIG_DEFINITIONS = Object.freeze([
|
|
|
22
22
|
{ key: "provider-retry-max-window-seconds", domain: "runtime", property: "providerRetryMaxWindowSeconds", label: "Provider retry max window", summary: "Positive-integer total retry budget per Run lineage in seconds (default: 600).", takesEffect: "The next retry episode; an active episode keeps its existing deadline." },
|
|
23
23
|
{ key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
|
|
24
24
|
{ key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive yielded Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
|
|
25
|
-
{ key: "context-budget", domain: "workflow", property: "contextBudget", label: "
|
|
26
|
-
{ key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger
|
|
25
|
+
{ key: "context-budget", domain: "workflow", property: "contextBudget", label: "Legacy context budget (inactive)", summary: `Compatibility-only soft and hard values in ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}; they are parsed but never control Session or workflow behavior.`, takesEffect: "No runtime effect; Session Token metrics are read-only observations." },
|
|
26
|
+
{ key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger and optional finding-ledger behavior. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
|
|
27
27
|
{ key: "resources-gc-mode", domain: "resources", property: "resourcesGcMode", label: "Resources GC mode", summary: "Resource GC mode: report or quarantine (default: report).", takesEffect: "The next resource GC operation." },
|
|
28
28
|
{ key: "resources-gc-auto-quarantine", domain: "resources", property: "resourcesGcAutoQuarantine", label: "Resources GC auto-quarantine", summary: "Automatically quarantine eligible terminal Task resources (default: false).", takesEffect: "The next eligible automatic resource GC operation." },
|
|
29
29
|
{ key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -227,13 +227,9 @@ export function resolveTmuxHistoryLimit(value) {
|
|
|
227
227
|
return resolveBoundedPositiveInteger(value, DEFAULT_TMUX_HISTORY_LIMIT, 1_000, 1_000_000, "tmuxHistoryLimit");
|
|
228
228
|
}
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* advisory; when it crosses the hard threshold the scheduler retires the
|
|
234
|
-
* generation and starts a fresh one instead of waiting for provider-side
|
|
235
|
-
* auto-compaction. The fields are additive and optional — Homes without them
|
|
236
|
-
* keep these defaults, so no config migration is required.
|
|
230
|
+
* Compatibility parser for the retired context-budget setting. Existing
|
|
231
|
+
* Homes may retain these values, but no runtime, scheduler, or lifecycle path
|
|
232
|
+
* consumes them. Session Token metrics are an independent read-only view.
|
|
237
233
|
*/
|
|
238
234
|
export const DEFAULT_CONTEXT_SOFT_TOKENS = 100_000;
|
|
239
235
|
export const DEFAULT_CONTEXT_HARD_TOKENS = 120_000;
|
|
@@ -4,17 +4,18 @@ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
|
4
4
|
* Issue 04 (context token budget) — long-term design:
|
|
5
5
|
*
|
|
6
6
|
* A Leader wake is a NOTIFICATION, not a context dump. The wake envelope
|
|
7
|
-
* carries only what the Agent
|
|
8
|
-
*
|
|
9
|
-
* reads
|
|
10
|
-
* the full projection with
|
|
7
|
+
* carries only what the Agent needs for immediate orientation: the wake id,
|
|
8
|
+
* aggregated reason tags, delta window, and a bounded list of active frozen
|
|
9
|
+
* Task Reviews. The Agent reads delta content on demand with
|
|
10
|
+
* `yui task wake show <wake-id>` and the full projection with
|
|
11
|
+
* `yui task context <task>`.
|
|
11
12
|
*
|
|
12
13
|
* The envelope is mode-agnostic: fresh generations and resumed generations
|
|
13
14
|
* receive the same minimal text. The native Session is a disposable cache of
|
|
14
15
|
* working context; Yui's durable Task records (including the wake ledger) are
|
|
15
16
|
* the checkpoint.
|
|
16
17
|
*/
|
|
17
|
-
/** Structural guardrail: the envelope
|
|
18
|
+
/** Structural guardrail: the envelope stays far below a normal model context window. */
|
|
18
19
|
export const WAKE_ENVELOPE_HARD_BYTES = 2_000;
|
|
19
20
|
/** Maximum reason tags rendered before elision to a count. */
|
|
20
21
|
const REASON_DISPLAY_LIMIT = 6;
|
|
@@ -35,11 +36,25 @@ export function buildTaskWakeEnvelope(reader, request) {
|
|
|
35
36
|
runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
|
|
36
37
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length
|
|
37
38
|
};
|
|
39
|
+
const activeReviews = reader.listReviewRounds(request.taskId).filter((round) => ((round.scope ?? "work-item") === "task"
|
|
40
|
+
&& (round.status === "pending" || round.status === "running")));
|
|
41
|
+
const reviewOrientation = activeReviews.slice(0, 3).map((round) => {
|
|
42
|
+
const projects = round.taskCandidate?.projects ?? [];
|
|
43
|
+
const heads = projects.slice(0, 2).map(({ projectId, commit }) => (`${projectId}@${commit.slice(0, 12)}`)).join("+");
|
|
44
|
+
return `${round.id}/${round.reviewerRoleName}`
|
|
45
|
+
+ `/${round.deltaRecheck === undefined ? "full" : "delta"}`
|
|
46
|
+
+ `[${round.status}]`
|
|
47
|
+
+ `@${heads || round.reviewBaseCommit.slice(0, 12)}`
|
|
48
|
+
+ `${projects.length > 2 ? `+${projects.length - 2}` : ""}`;
|
|
49
|
+
}).join(", ");
|
|
38
50
|
const lines = [
|
|
39
51
|
`Wake: ${request.wakeId} — delta since ${request.fromCursor}`,
|
|
40
52
|
` Reasons: ${renderReasons(request.reasons)}`,
|
|
41
53
|
` Changed: ${counts.events} events, ${counts.messages} messages, ${counts.runs} runs`
|
|
42
54
|
+ ` → yui task wake show ${request.taskId} ${request.wakeId}`,
|
|
55
|
+
` Active Task Reviews: ${activeReviews.length === 0
|
|
56
|
+
? "none"
|
|
57
|
+
: `${reviewOrientation}${activeReviews.length > 3 ? `, … (+${activeReviews.length - 3})` : ""}`}`,
|
|
43
58
|
`Full context: yui task context ${request.taskId}`
|
|
44
59
|
];
|
|
45
60
|
const body = lines.join("\n");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
3
|
-
import { createRuntimeObservation,
|
|
3
|
+
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
4
|
const DEFAULT_MAX_CONCURRENT_SAMPLES = 8;
|
|
5
5
|
const MAX_CONCURRENT_SAMPLES = 64;
|
|
6
6
|
/**
|
|
@@ -31,20 +31,30 @@ export class AgentRuntimeObserver {
|
|
|
31
31
|
const dirty = new Set();
|
|
32
32
|
const sequenceBase = this.#sequence;
|
|
33
33
|
this.#sequence += active.length;
|
|
34
|
-
await forEachConcurrent(active, this.#maxConcurrentSamples, async ({ key, fence, source,
|
|
34
|
+
await forEachConcurrent(active, this.#maxConcurrentSamples, async ({ key, fence, source, persistedState }, index) => {
|
|
35
35
|
const existingState = this.#states.get(key);
|
|
36
36
|
// Cursor state is intentionally process-local, but the latest canonical
|
|
37
37
|
// usage/activity baseline is durable. Rehydrate it after Controller
|
|
38
38
|
// restart so rereading the bounded transcript tail cannot manufacture a
|
|
39
39
|
// fresh activity edge from tokens that were already observed.
|
|
40
40
|
const state = existingState ?? { ...persistedState };
|
|
41
|
+
const restoredUsageEventId = existingState === undefined
|
|
42
|
+
? persistedState.usageEventId
|
|
43
|
+
: undefined;
|
|
41
44
|
const driver = this.drivers.require(fence.driverId);
|
|
42
45
|
const observer = driver.runtime.observer;
|
|
43
46
|
if (observer === undefined)
|
|
44
47
|
return;
|
|
45
48
|
let sample;
|
|
46
49
|
try {
|
|
47
|
-
sample = await observer.sample(source, state.cursor
|
|
50
|
+
sample = await observer.sample(source, state.cursor, {
|
|
51
|
+
...(state.usageOccurrenceId === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { latestOccurrenceId: state.usageOccurrenceId }),
|
|
54
|
+
...(state.usageOccurrenceCheckpoint === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { latestCheckpoint: state.usageOccurrenceCheckpoint })
|
|
57
|
+
});
|
|
48
58
|
}
|
|
49
59
|
catch (error) {
|
|
50
60
|
sample = Object.freeze({
|
|
@@ -59,37 +69,70 @@ export class AgentRuntimeObserver {
|
|
|
59
69
|
// latency can change completion order without changing observation
|
|
60
70
|
// identity or the canonical sequence assigned to a source.
|
|
61
71
|
const sequence = sequenceBase + index;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
let usages = sample.usages ?? [];
|
|
73
|
+
if (state.usage === undefined) {
|
|
74
|
+
// Cumulative counters need a lower bound. An exact initial sample
|
|
75
|
+
// proves the complete Session-generation history even when the
|
|
76
|
+
// current Run resumed that Session, so preserve every occurrence and
|
|
77
|
+
// anchor it at zero. A partial sample cannot prove the omitted request
|
|
78
|
+
// boundaries; retain its latest total as a partial baseline, plus the
|
|
79
|
+
// smallest ordered witness if the visible counter already rolled back.
|
|
80
|
+
// Request snapshots already describe one complete occurrence and must
|
|
81
|
+
// not be mixed with a synthetic cumulative fact.
|
|
82
|
+
const usageOccurrences = usages.filter(hasUsageSnapshot);
|
|
83
|
+
const latestOccurrence = usageOccurrences.at(-1);
|
|
84
|
+
const completeHistory = latestOccurrence?.usage.semantics === "cumulative-session"
|
|
85
|
+
&& usages.every(({ observationQuality }) => observationQuality !== "partial");
|
|
86
|
+
const rollbackWitness = latestOccurrence?.usage.semantics === "cumulative-session"
|
|
87
|
+
&& !completeHistory
|
|
88
|
+
? cumulativeRollbackWitness(usageOccurrences)
|
|
89
|
+
: [];
|
|
90
|
+
const baseline = latestOccurrence?.usage.semantics !== "cumulative-session"
|
|
91
|
+
? undefined
|
|
92
|
+
: completeHistory
|
|
93
|
+
? Object.freeze({
|
|
94
|
+
semantics: "cumulative-session",
|
|
95
|
+
inputTokens: 0,
|
|
96
|
+
outputTokens: 0
|
|
97
|
+
})
|
|
98
|
+
: latestOccurrence.usage;
|
|
99
|
+
if (baseline !== undefined && rollbackWitness.length === 0) {
|
|
100
|
+
const baselineKey = completeHistory ? "zero" : latestOccurrence.occurrenceId;
|
|
101
|
+
const identity = completeHistory
|
|
102
|
+
? tokenObservationIdentity("baseline", fence, source.sourceId, baselineKey)
|
|
103
|
+
: usageObservationIdentity(fence, source.sourceId, latestOccurrence);
|
|
78
104
|
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
79
105
|
schemaVersion: 2,
|
|
80
|
-
eventId:
|
|
81
|
-
semanticKey:
|
|
106
|
+
eventId: identity.eventId,
|
|
107
|
+
semanticKey: identity.semanticKey,
|
|
82
108
|
kind: "activity.observed",
|
|
83
|
-
authority:
|
|
109
|
+
authority: completeHistory ? "controller" : "driver-inferred",
|
|
84
110
|
receivedAt: at,
|
|
85
111
|
sequence,
|
|
86
112
|
ordinal: 1,
|
|
87
113
|
fence,
|
|
88
|
-
payload: {
|
|
114
|
+
payload: {
|
|
115
|
+
activity: "model",
|
|
116
|
+
sourceId: source.sourceId,
|
|
117
|
+
usage: baseline,
|
|
118
|
+
...(completeHistory
|
|
119
|
+
? {}
|
|
120
|
+
: { observationQuality: "partial" })
|
|
121
|
+
}
|
|
89
122
|
}));
|
|
90
123
|
state.usage = baseline;
|
|
91
|
-
|
|
124
|
+
state.usageEventId = identity.eventId;
|
|
125
|
+
state.usageOccurrenceId = completeHistory
|
|
126
|
+
? undefined
|
|
127
|
+
: latestOccurrence.occurrenceId;
|
|
128
|
+
state.usageOccurrenceCheckpoint = completeHistory
|
|
129
|
+
? undefined
|
|
130
|
+
: latestOccurrence.resumeCheckpoint;
|
|
131
|
+
if (!completeHistory)
|
|
132
|
+
usages = [];
|
|
92
133
|
}
|
|
134
|
+
if (rollbackWitness.length > 0)
|
|
135
|
+
usages = rollbackWitness;
|
|
93
136
|
}
|
|
94
137
|
const health = JSON.stringify([sample.status, sample.detail ?? null]);
|
|
95
138
|
if (state.health !== health) {
|
|
@@ -112,32 +155,81 @@ export class AgentRuntimeObserver {
|
|
|
112
155
|
state.health = health;
|
|
113
156
|
dirty.add(`role:${fence.taskId}/${fence.roleName}`);
|
|
114
157
|
}
|
|
115
|
-
const usageChanged = sample.usage !== undefined
|
|
116
|
-
&& !sameUsage(state.usage, sample.usage);
|
|
117
158
|
const activityChanged = sample.activityId !== undefined
|
|
118
159
|
&& sample.activityId !== state.activityId;
|
|
119
|
-
if (
|
|
120
|
-
|
|
160
|
+
if (restoredUsageEventId !== undefined) {
|
|
161
|
+
let persistedIndex = -1;
|
|
162
|
+
for (let usageIndex = usages.length - 1; usageIndex >= 0; usageIndex -= 1) {
|
|
163
|
+
if (usageObservationIdentity(fence, source.sourceId, usages[usageIndex]).eventId
|
|
164
|
+
=== restoredUsageEventId) {
|
|
165
|
+
persistedIndex = usageIndex;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (persistedIndex >= 0) {
|
|
170
|
+
usages = usages.slice(persistedIndex + 1);
|
|
171
|
+
}
|
|
172
|
+
else if (state.usage?.semantics === "cumulative-session") {
|
|
173
|
+
// The durable counter proves the latest cumulative total, but an
|
|
174
|
+
// initial bounded read that cannot recover that occurrence may
|
|
175
|
+
// have skipped intermediate request boundaries while the
|
|
176
|
+
// Controller was stopped. Preserve the total and fail closed only
|
|
177
|
+
// for the derived maximum-request metric.
|
|
178
|
+
usages = usages.map((occurrence) => (occurrence.usage?.semantics !== "cumulative-session"
|
|
179
|
+
? occurrence
|
|
180
|
+
: Object.freeze({ ...occurrence, observationQuality: "partial" })));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
usages.forEach((occurrence, usageIndex) => {
|
|
184
|
+
const identity = usageObservationIdentity(fence, source.sourceId, occurrence);
|
|
121
185
|
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
122
186
|
schemaVersion: 2,
|
|
123
|
-
eventId:
|
|
124
|
-
semanticKey:
|
|
187
|
+
eventId: identity.eventId,
|
|
188
|
+
semanticKey: identity.semanticKey,
|
|
125
189
|
kind: "activity.observed",
|
|
126
190
|
authority: "driver-inferred",
|
|
127
191
|
receivedAt: at,
|
|
128
192
|
sequence,
|
|
129
|
-
ordinal: 2,
|
|
193
|
+
ordinal: 2 + usageIndex,
|
|
130
194
|
fence,
|
|
131
195
|
payload: {
|
|
132
196
|
activity: sample.activity ?? "model",
|
|
133
|
-
|
|
134
|
-
...(usage === undefined
|
|
197
|
+
sourceId: source.sourceId,
|
|
198
|
+
...(occurrence.usage === undefined || occurrence.activityId === undefined
|
|
199
|
+
? {}
|
|
200
|
+
: { activityId: occurrence.activityId }),
|
|
201
|
+
...(occurrence.observationQuality === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: { observationQuality: occurrence.observationQuality }),
|
|
204
|
+
...(occurrence.usage === undefined ? {} : { usage: occurrence.usage })
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
if (occurrence.usage !== undefined) {
|
|
208
|
+
state.usage = occurrence.usage;
|
|
209
|
+
state.usageEventId = identity.eventId;
|
|
210
|
+
state.usageOccurrenceId = occurrence.occurrenceId;
|
|
211
|
+
state.usageOccurrenceCheckpoint = occurrence.resumeCheckpoint;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (activityChanged && state.cursor !== undefined) {
|
|
215
|
+
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
216
|
+
schemaVersion: 2,
|
|
217
|
+
eventId: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
218
|
+
semanticKey: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
219
|
+
kind: "activity.observed",
|
|
220
|
+
authority: "driver-inferred",
|
|
221
|
+
receivedAt: at,
|
|
222
|
+
sequence,
|
|
223
|
+
ordinal: 2 + usages.length,
|
|
224
|
+
fence,
|
|
225
|
+
payload: {
|
|
226
|
+
activity: sample.activity ?? "model",
|
|
227
|
+
sourceId: source.sourceId,
|
|
228
|
+
activityId: sample.activityId
|
|
135
229
|
}
|
|
136
230
|
}));
|
|
137
231
|
dirty.add(`role:${fence.taskId}/${fence.roleName}`);
|
|
138
232
|
}
|
|
139
|
-
if (sample.usage !== undefined)
|
|
140
|
-
state.usage = sample.usage;
|
|
141
233
|
if (sample.activityId !== undefined)
|
|
142
234
|
state.activityId = sample.activityId;
|
|
143
235
|
this.#states.set(key, state);
|
|
@@ -160,11 +252,13 @@ export class AgentRuntimeObserver {
|
|
|
160
252
|
// by Run and sort each group once so every active Run can reuse the same
|
|
161
253
|
// ordered slice instead of repeatedly filtering/sorting all E events.
|
|
162
254
|
const observationsByRunId = new Map();
|
|
255
|
+
const taskObservations = [];
|
|
163
256
|
for (const event of this.store.listEvents(task.id)) {
|
|
164
257
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
165
258
|
const runId = observation?.fence.runId;
|
|
166
259
|
if (observation === null || runId === undefined)
|
|
167
260
|
continue;
|
|
261
|
+
taskObservations.push(observation);
|
|
168
262
|
const grouped = observationsByRunId.get(runId);
|
|
169
263
|
if (grouped === undefined) {
|
|
170
264
|
observationsByRunId.set(runId, [observation]);
|
|
@@ -176,6 +270,7 @@ export class AgentRuntimeObserver {
|
|
|
176
270
|
for (const observations of observationsByRunId.values()) {
|
|
177
271
|
observations.sort(compareObservations);
|
|
178
272
|
}
|
|
273
|
+
taskObservations.sort(compareObservations);
|
|
179
274
|
for (const run of this.store.listAgentRuns(task.id)) {
|
|
180
275
|
if (run.status !== "active"
|
|
181
276
|
|| this.store.getActiveAgentRun(task.id, run.roleName)?.id !== run.id)
|
|
@@ -200,12 +295,18 @@ export class AgentRuntimeObserver {
|
|
|
200
295
|
continue;
|
|
201
296
|
}
|
|
202
297
|
const fence = accepted.fence;
|
|
203
|
-
const
|
|
204
|
-
.
|
|
205
|
-
const persistedUsage =
|
|
298
|
+
const generation = taskObservations.filter((observation) => (sessionGenerationFenceMatches(fence, observation.fence)
|
|
299
|
+
&& observation.payload.sourceId === source.sourceId));
|
|
300
|
+
const persistedUsage = generation.filter((observation) => (observation.kind === "activity.observed"
|
|
206
301
|
&& observation.payload.usage !== undefined)).at(-1);
|
|
207
|
-
const
|
|
302
|
+
const persistedActivity = generation.filter((observation) => (observation.kind === "activity.observed"
|
|
303
|
+
&& observation.payload.activityId !== undefined
|
|
304
|
+
&& observation.payload.usage === undefined)).at(-1);
|
|
305
|
+
const persistedHealth = generation.filter((observation) => (observation.kind === "observer.health"
|
|
208
306
|
&& observation.payload.sourceId === source.sourceId)).at(-1);
|
|
307
|
+
const persistedUsageResume = persistedUsage === undefined
|
|
308
|
+
? undefined
|
|
309
|
+
: usageObservationResume(persistedUsage.semanticKey);
|
|
209
310
|
result.push(Object.freeze({
|
|
210
311
|
key: JSON.stringify([
|
|
211
312
|
fence.taskId,
|
|
@@ -216,20 +317,30 @@ export class AgentRuntimeObserver {
|
|
|
216
317
|
fence.launchId,
|
|
217
318
|
fence.sessionGenerationId,
|
|
218
319
|
fence.nativeSessionId,
|
|
219
|
-
fence.nativeTurnId,
|
|
220
|
-
fence.receiptId,
|
|
221
320
|
source.sourceId
|
|
222
321
|
]),
|
|
223
322
|
fence,
|
|
224
323
|
source,
|
|
225
|
-
freshSession: run.mode === "new",
|
|
226
324
|
persistedState: Object.freeze({
|
|
227
325
|
...(persistedUsage?.payload.usage === undefined
|
|
228
326
|
? {}
|
|
229
327
|
: { usage: persistedUsage.payload.usage }),
|
|
230
|
-
...(persistedUsage
|
|
328
|
+
...(persistedUsage === undefined
|
|
231
329
|
? {}
|
|
232
|
-
: {
|
|
330
|
+
: {
|
|
331
|
+
usageEventId: persistedUsage.eventId,
|
|
332
|
+
...(persistedUsageResume === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: {
|
|
335
|
+
usageOccurrenceId: persistedUsageResume.occurrenceId,
|
|
336
|
+
...(persistedUsageResume.checkpoint === undefined
|
|
337
|
+
? {}
|
|
338
|
+
: { usageOccurrenceCheckpoint: persistedUsageResume.checkpoint })
|
|
339
|
+
})
|
|
340
|
+
}),
|
|
341
|
+
...(persistedActivity?.payload.activityId === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { activityId: persistedActivity.payload.activityId }),
|
|
233
344
|
...(persistedHealth === undefined
|
|
234
345
|
? {}
|
|
235
346
|
: {
|
|
@@ -265,6 +376,28 @@ function sampleConcurrency(value) {
|
|
|
265
376
|
}
|
|
266
377
|
return resolved;
|
|
267
378
|
}
|
|
379
|
+
function hasUsageSnapshot(occurrence) {
|
|
380
|
+
return occurrence.usage !== undefined;
|
|
381
|
+
}
|
|
382
|
+
function cumulativeRollbackWitness(occurrences) {
|
|
383
|
+
const cumulative = occurrences.filter(({ usage }) => (usage.semantics === "cumulative-session"));
|
|
384
|
+
const latest = cumulative.at(-1);
|
|
385
|
+
for (let index = 1; index < cumulative.length; index += 1) {
|
|
386
|
+
const previous = cumulative[index - 1];
|
|
387
|
+
const current = cumulative[index];
|
|
388
|
+
if (current.usage.inputTokens >= previous.usage.inputTokens
|
|
389
|
+
&& current.usage.outputTokens >= previous.usage.outputTokens)
|
|
390
|
+
continue;
|
|
391
|
+
const witness = latest === current
|
|
392
|
+
? [previous, current]
|
|
393
|
+
: [previous, current, latest];
|
|
394
|
+
return Object.freeze(witness.map((occurrence) => Object.freeze({
|
|
395
|
+
...occurrence,
|
|
396
|
+
observationQuality: "partial"
|
|
397
|
+
})));
|
|
398
|
+
}
|
|
399
|
+
return Object.freeze([]);
|
|
400
|
+
}
|
|
268
401
|
function numericCompare(left, right) {
|
|
269
402
|
return left.localeCompare(right, undefined, { numeric: true });
|
|
270
403
|
}
|
|
@@ -279,11 +412,74 @@ function observationId(kind, fence, sourceId, value) {
|
|
|
279
412
|
.update(JSON.stringify([kind, fence, sourceId, value]))
|
|
280
413
|
.digest("hex")}`;
|
|
281
414
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
415
|
+
function usageObservationIdentity(fence, sourceId, occurrence) {
|
|
416
|
+
const eventId = tokenObservationId("usage", fence, sourceId, occurrence.occurrenceId);
|
|
417
|
+
const encodedOccurrenceId = Buffer.from(occurrence.occurrenceId, "utf8").toString("base64url");
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
eventId,
|
|
420
|
+
semanticKey: `${eventId}:occurrence:${encodedOccurrenceId}${occurrence.resumeCheckpoint === undefined
|
|
421
|
+
? ""
|
|
422
|
+
: `:checkpoint:${Buffer.from(occurrence.resumeCheckpoint, "utf8").toString("base64url")}`}`
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function usageObservationResume(semanticKey) {
|
|
426
|
+
const occurrenceMarker = ":occurrence:";
|
|
427
|
+
const checkpointMarker = ":checkpoint:";
|
|
428
|
+
const occurrenceIndex = semanticKey.lastIndexOf(occurrenceMarker);
|
|
429
|
+
if (occurrenceIndex < 0)
|
|
430
|
+
return undefined;
|
|
431
|
+
const checkpointIndex = semanticKey.indexOf(checkpointMarker, occurrenceIndex + occurrenceMarker.length);
|
|
432
|
+
const encodedOccurrence = semanticKey.slice(occurrenceIndex + occurrenceMarker.length, checkpointIndex < 0 ? undefined : checkpointIndex);
|
|
433
|
+
if (encodedOccurrence.length === 0)
|
|
434
|
+
return undefined;
|
|
435
|
+
try {
|
|
436
|
+
const occurrenceId = Buffer.from(encodedOccurrence, "base64url").toString("utf8");
|
|
437
|
+
if (occurrenceId.length === 0)
|
|
438
|
+
return undefined;
|
|
439
|
+
if (checkpointIndex < 0)
|
|
440
|
+
return Object.freeze({ occurrenceId });
|
|
441
|
+
const encodedCheckpoint = semanticKey.slice(checkpointIndex + checkpointMarker.length);
|
|
442
|
+
if (encodedCheckpoint.length === 0)
|
|
443
|
+
return undefined;
|
|
444
|
+
const checkpoint = Buffer.from(encodedCheckpoint, "base64url").toString("utf8");
|
|
445
|
+
return checkpoint.length === 0
|
|
446
|
+
? undefined
|
|
447
|
+
: Object.freeze({ occurrenceId, checkpoint });
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function tokenObservationIdentity(kind, fence, sourceId, value) {
|
|
454
|
+
const eventId = tokenObservationId(kind, fence, sourceId, value);
|
|
455
|
+
return Object.freeze({ eventId, semanticKey: eventId });
|
|
456
|
+
}
|
|
457
|
+
function tokenObservationId(kind, fence, sourceId, value) {
|
|
458
|
+
return `runtime-observer-${createHash("sha256").update(JSON.stringify([
|
|
459
|
+
kind,
|
|
460
|
+
fence.taskId ?? null,
|
|
461
|
+
fence.roleName,
|
|
462
|
+
fence.agentId,
|
|
463
|
+
fence.driverId,
|
|
464
|
+
fence.launchId,
|
|
465
|
+
fence.sessionGenerationId,
|
|
466
|
+
fence.nativeSessionId ?? null,
|
|
467
|
+
sourceId,
|
|
468
|
+
value
|
|
469
|
+
])).digest("hex")}`;
|
|
470
|
+
}
|
|
471
|
+
function sessionGenerationFenceMatches(expected, actual) {
|
|
472
|
+
for (const field of [
|
|
473
|
+
"taskId",
|
|
474
|
+
"roleName",
|
|
475
|
+
"agentId",
|
|
476
|
+
"driverId",
|
|
477
|
+
"launchId",
|
|
478
|
+
"sessionGenerationId",
|
|
479
|
+
"nativeSessionId"
|
|
480
|
+
]) {
|
|
481
|
+
if (expected[field] !== actual[field])
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
289
485
|
}
|
|
@@ -582,7 +582,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
582
582
|
async #prepareAndScan(taskId) {
|
|
583
583
|
const task = this.store.getTask(taskId);
|
|
584
584
|
if (task !== null
|
|
585
|
-
&&
|
|
585
|
+
&& task.status === "active"
|
|
586
586
|
&& this.workspacePreparer !== undefined) {
|
|
587
587
|
await this.workspacePreparer.prepareTaskWorkspace(taskId);
|
|
588
588
|
}
|
|
@@ -1184,11 +1184,12 @@ export class FileTaskController {
|
|
|
1184
1184
|
this.#clearDeliveryRetry(key);
|
|
1185
1185
|
this.#scheduleTaskPassRetry({ taskId, keys: [key] });
|
|
1186
1186
|
}
|
|
1187
|
+
const runtimeAdvanced = (firstRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0
|
|
1188
|
+
|| (secondRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0;
|
|
1187
1189
|
if (scope.kind === "full"
|
|
1188
1190
|
|| scope.keys.some((key) => key !== "operator")
|
|
1189
|
-
||
|
|
1190
|
-
|
|
1191
|
-
this.#signalOperatorMailbox();
|
|
1191
|
+
|| runtimeAdvanced) {
|
|
1192
|
+
this.#signalOperatorMailbox(runtimeAdvanced);
|
|
1192
1193
|
}
|
|
1193
1194
|
}
|
|
1194
1195
|
catch (error) {
|
|
@@ -1311,7 +1312,7 @@ export class FileTaskController {
|
|
|
1311
1312
|
this.#clearEmptyOperatorRetry();
|
|
1312
1313
|
this.#scheduleNextInputDeadline();
|
|
1313
1314
|
}
|
|
1314
|
-
#signalOperatorMailbox() {
|
|
1315
|
+
#signalOperatorMailbox(retryReady = false) {
|
|
1315
1316
|
if (this.#stopped)
|
|
1316
1317
|
return;
|
|
1317
1318
|
const mailbox = this.store.getWorkMailbox({ kind: "operator" });
|
|
@@ -1320,7 +1321,8 @@ export class FileTaskController {
|
|
|
1320
1321
|
this.#lastOperatorSignalIdentity = undefined;
|
|
1321
1322
|
return;
|
|
1322
1323
|
}
|
|
1323
|
-
if (identity !== this.#lastOperatorSignalIdentity
|
|
1324
|
+
if (identity !== this.#lastOperatorSignalIdentity
|
|
1325
|
+
|| (retryReady && this.store.getOperatorDeliveryTarget() !== null)) {
|
|
1324
1326
|
this.#operatorSignalScheduler.signal("operator");
|
|
1325
1327
|
this.#lastOperatorSignalIdentity = identity;
|
|
1326
1328
|
}
|
|
@@ -1516,13 +1518,7 @@ export class FileTaskController {
|
|
|
1516
1518
|
&& this.#operatorStartupRetryArmed));
|
|
1517
1519
|
if (operatorRetries.length > 0) {
|
|
1518
1520
|
retry.set("operator", {
|
|
1519
|
-
identity: operatorRetries.map((notification) => ("
|
|
1520
|
-
? `input:${notification.inputRequestId}`
|
|
1521
|
-
: "recoveryTaskId" in notification
|
|
1522
|
-
? `recovery:${notification.recoveryTaskId}`
|
|
1523
|
-
: "stallTaskId" in notification
|
|
1524
|
-
? `stall:${notification.stallTaskId}`
|
|
1525
|
-
: `terminal:${notification.terminalTaskId}`)).join("|")
|
|
1521
|
+
identity: operatorRetries.map((notification) => (`${notification.sourceKind}:${notification.taskId}:${notification.sourceId}`)).join("|")
|
|
1526
1522
|
});
|
|
1527
1523
|
}
|
|
1528
1524
|
else if (result.inputNotifications.some((notification) => notification.status === "sent"
|