@omercnet/paseo-gas-city 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bun.lock +1217 -0
- package/client/city-operations.tsx +1505 -0
- package/client/contribute.tsx +96 -0
- package/client/dispatch-intent.ts +53 -0
- package/client/factory-panel.tsx +373 -0
- package/client/gas-city-surface.tsx +343 -0
- package/client/settings-screen.tsx +286 -0
- package/client/view-model.ts +318 -0
- package/docs/images/paseo-gas-city-compact-overview.webp +0 -0
- package/docs/images/paseo-gas-city-dispatch-confirmation.webp +0 -0
- package/docs/images/paseo-gas-city-wide-events.webp +0 -0
- package/docs/images/paseo-gas-city-wide-overview.webp +0 -0
- package/icon.svg +5 -0
- package/index.client.tsx +1 -0
- package/index.server.ts +42 -0
- package/package.json +73 -0
- package/paseo-plugin.json +4 -0
- package/server/gas-city-client.ts +668 -0
- package/server/handlers.ts +790 -0
- package/server/workspace-mapping.ts +170 -0
- package/shared/index.ts +4 -0
- package/shared/limits.ts +28 -0
- package/shared/rpc.ts +99 -0
- package/shared/schemas.ts +430 -0
- package/shared/settings.ts +89 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import type { RpcInput, RpcOutput } from "@getpaseo/plugin";
|
|
2
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
3
|
+
import {
|
|
4
|
+
AttentionListSchema,
|
|
5
|
+
CityRigSnapshotSchema,
|
|
6
|
+
ConvoyListSchema,
|
|
7
|
+
DispatchResultSchema,
|
|
8
|
+
type discoverSupervisor,
|
|
9
|
+
type dispatchWork,
|
|
10
|
+
EventListSchema,
|
|
11
|
+
GAS_CITY_LIMITS,
|
|
12
|
+
type GasCityDiagnostic,
|
|
13
|
+
type GasCityRpcSettings,
|
|
14
|
+
GasCityRpcSettingsSchema,
|
|
15
|
+
type getCityRigSnapshot,
|
|
16
|
+
type listAttention,
|
|
17
|
+
type listConvoys,
|
|
18
|
+
type listEvents,
|
|
19
|
+
type listSessions,
|
|
20
|
+
type listWork,
|
|
21
|
+
type performSessionAction,
|
|
22
|
+
type resolveWorkspaceRig,
|
|
23
|
+
SessionActionResultSchema,
|
|
24
|
+
SessionListSchema,
|
|
25
|
+
SupervisorDiscoverySchema,
|
|
26
|
+
WorkListSchema,
|
|
27
|
+
} from "../shared";
|
|
28
|
+
import {
|
|
29
|
+
GasCityClient,
|
|
30
|
+
GasCityClientError,
|
|
31
|
+
type UpstreamCity,
|
|
32
|
+
type UpstreamConvoy,
|
|
33
|
+
type UpstreamEvent,
|
|
34
|
+
UpstreamEventSchema,
|
|
35
|
+
type UpstreamRig,
|
|
36
|
+
type UpstreamSession,
|
|
37
|
+
type UpstreamWorkItem,
|
|
38
|
+
} from "./gas-city-client";
|
|
39
|
+
import { mapWorkspaceToRig } from "./workspace-mapping";
|
|
40
|
+
|
|
41
|
+
export interface GasCityHandlerDependencies {
|
|
42
|
+
createClient?: (settings: GasCityRpcSettings) => GasCityClient;
|
|
43
|
+
now?: () => Date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GasCityHandlers {
|
|
47
|
+
discoverSupervisor(
|
|
48
|
+
input: RpcInput<typeof discoverSupervisor>,
|
|
49
|
+
context: PluginHandlerContext,
|
|
50
|
+
): Promise<RpcOutput<typeof discoverSupervisor>>;
|
|
51
|
+
resolveWorkspaceRig(
|
|
52
|
+
input: RpcInput<typeof resolveWorkspaceRig>,
|
|
53
|
+
context: PluginHandlerContext,
|
|
54
|
+
): Promise<RpcOutput<typeof resolveWorkspaceRig>>;
|
|
55
|
+
getCityRigSnapshot(
|
|
56
|
+
input: RpcInput<typeof getCityRigSnapshot>,
|
|
57
|
+
context: PluginHandlerContext,
|
|
58
|
+
): Promise<RpcOutput<typeof getCityRigSnapshot>>;
|
|
59
|
+
listSessions(
|
|
60
|
+
input: RpcInput<typeof listSessions>,
|
|
61
|
+
context: PluginHandlerContext,
|
|
62
|
+
): Promise<RpcOutput<typeof listSessions>>;
|
|
63
|
+
listConvoys(
|
|
64
|
+
input: RpcInput<typeof listConvoys>,
|
|
65
|
+
context: PluginHandlerContext,
|
|
66
|
+
): Promise<RpcOutput<typeof listConvoys>>;
|
|
67
|
+
listWork(
|
|
68
|
+
input: RpcInput<typeof listWork>,
|
|
69
|
+
context: PluginHandlerContext,
|
|
70
|
+
): Promise<RpcOutput<typeof listWork>>;
|
|
71
|
+
listEvents(
|
|
72
|
+
input: RpcInput<typeof listEvents>,
|
|
73
|
+
context: PluginHandlerContext,
|
|
74
|
+
): Promise<RpcOutput<typeof listEvents>>;
|
|
75
|
+
listAttention(
|
|
76
|
+
input: RpcInput<typeof listAttention>,
|
|
77
|
+
context: PluginHandlerContext,
|
|
78
|
+
): Promise<RpcOutput<typeof listAttention>>;
|
|
79
|
+
dispatchWork(
|
|
80
|
+
input: RpcInput<typeof dispatchWork>,
|
|
81
|
+
context: PluginHandlerContext,
|
|
82
|
+
): Promise<RpcOutput<typeof dispatchWork>>;
|
|
83
|
+
performSessionAction(
|
|
84
|
+
input: RpcInput<typeof performSessionAction>,
|
|
85
|
+
context: PluginHandlerContext,
|
|
86
|
+
): Promise<RpcOutput<typeof performSessionAction>>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function diagnostic(code: string, message: string, retryable: boolean): GasCityDiagnostic {
|
|
90
|
+
return { code, message, retryable };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function nullable(value: string | undefined): string | null {
|
|
94
|
+
return value && value.length > 0 ? value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function refreshedAt(now: () => Date): string {
|
|
98
|
+
return now().toISOString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function safeError(action: string, error: unknown): Error {
|
|
102
|
+
if (error instanceof GasCityClientError) {
|
|
103
|
+
console.error("[paseo-gas-city] RPC failed", {
|
|
104
|
+
action,
|
|
105
|
+
code: error.code,
|
|
106
|
+
correlationId: error.correlationId,
|
|
107
|
+
status: error.status,
|
|
108
|
+
});
|
|
109
|
+
if (error.code === "invalid-endpoint" || error.code === "endpoint-not-allowed") {
|
|
110
|
+
return new Error("Gas City endpoint configuration is invalid.");
|
|
111
|
+
}
|
|
112
|
+
if (error.code === "invalid-response" || error.code === "response-too-large") {
|
|
113
|
+
return new Error("Gas City returned an invalid response.");
|
|
114
|
+
}
|
|
115
|
+
if (error.code === "timeout") return new Error("Gas City request timed out.");
|
|
116
|
+
if (error.code === "unreachable") return new Error("Gas City supervisor is unreachable.");
|
|
117
|
+
if (error.code === "canceled") return new Error("Gas City request was canceled.");
|
|
118
|
+
return new Error("Gas City request failed.");
|
|
119
|
+
}
|
|
120
|
+
console.error("[paseo-gas-city] RPC failed", { action, error });
|
|
121
|
+
return new Error(`Unable to ${action}. Check Paseo plugin logs for details.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function discoveryFailure(error: unknown, now: () => Date): RpcOutput<typeof discoverSupervisor> {
|
|
125
|
+
const invalid =
|
|
126
|
+
error instanceof GasCityClientError &&
|
|
127
|
+
(error.code === "invalid-response" || error.code === "response-too-large");
|
|
128
|
+
const configuration =
|
|
129
|
+
error instanceof GasCityClientError &&
|
|
130
|
+
(error.code === "invalid-endpoint" || error.code === "endpoint-not-allowed");
|
|
131
|
+
const code = configuration ? "invalid-endpoint" : invalid ? "invalid-response" : "unreachable";
|
|
132
|
+
const message = configuration
|
|
133
|
+
? "The Gas City endpoint configuration is invalid."
|
|
134
|
+
: invalid
|
|
135
|
+
? "The Gas City supervisor returned an invalid response."
|
|
136
|
+
: "The Gas City supervisor is unreachable.";
|
|
137
|
+
if (error instanceof GasCityClientError) {
|
|
138
|
+
console.error("[paseo-gas-city] Supervisor discovery failed", {
|
|
139
|
+
code: error.code,
|
|
140
|
+
correlationId: error.correlationId,
|
|
141
|
+
status: error.status,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return SupervisorDiscoverySchema.parse({
|
|
145
|
+
state: configuration ? "not-configured" : invalid ? "invalid-response" : "unreachable",
|
|
146
|
+
supervisor: null,
|
|
147
|
+
cities: [],
|
|
148
|
+
diagnostics: [diagnostic(code, message, !configuration)],
|
|
149
|
+
refreshedAt: refreshedAt(now),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function citySummary(city: UpstreamCity) {
|
|
154
|
+
return {
|
|
155
|
+
name: city.name,
|
|
156
|
+
path: city.path,
|
|
157
|
+
running: city.running,
|
|
158
|
+
status: nullable(city.status),
|
|
159
|
+
error: nullable(city.error),
|
|
160
|
+
completedPhases: city.phases_completed ?? [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function rigSummary(rig: UpstreamRig) {
|
|
165
|
+
return {
|
|
166
|
+
name: rig.name,
|
|
167
|
+
path: rig.path,
|
|
168
|
+
prefix: nullable(rig.prefix),
|
|
169
|
+
suspended: rig.suspended,
|
|
170
|
+
agentCount: rig.agent_count,
|
|
171
|
+
runningAgentCount: rig.running_count,
|
|
172
|
+
defaultBranch: nullable(rig.default_branch),
|
|
173
|
+
lastActivityAt: nullable(rig.last_activity),
|
|
174
|
+
git: rig.git
|
|
175
|
+
? {
|
|
176
|
+
branch: rig.git.branch,
|
|
177
|
+
clean: rig.git.clean,
|
|
178
|
+
changedFiles: rig.git.changed_files,
|
|
179
|
+
ahead: rig.git.ahead,
|
|
180
|
+
behind: rig.git.behind,
|
|
181
|
+
}
|
|
182
|
+
: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function submissionKinds(session: UpstreamSession): string[] {
|
|
187
|
+
const capabilities = session.submission_capabilities;
|
|
188
|
+
if (!capabilities) return [];
|
|
189
|
+
return Object.entries(capabilities)
|
|
190
|
+
.filter((entry): entry is [string, true] => entry[1] === true)
|
|
191
|
+
.map(([name]) => name)
|
|
192
|
+
.slice(0, GAS_CITY_LIMITS.diagnostics);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sessionItem(cityName: string, session: UpstreamSession) {
|
|
196
|
+
return {
|
|
197
|
+
id: session.id,
|
|
198
|
+
cityName,
|
|
199
|
+
rigName: nullable(session.rig),
|
|
200
|
+
template: session.template,
|
|
201
|
+
state: session.state,
|
|
202
|
+
title: session.title,
|
|
203
|
+
provider: session.provider,
|
|
204
|
+
sessionName: session.session_name,
|
|
205
|
+
createdAt: session.created_at,
|
|
206
|
+
lastActiveAt: nullable(session.last_active),
|
|
207
|
+
attached: session.attached,
|
|
208
|
+
running: session.running,
|
|
209
|
+
configuredNamedSession: session.configured_named_session ?? false,
|
|
210
|
+
activity: nullable(session.activity),
|
|
211
|
+
activeBeadId: nullable(session.active_bead),
|
|
212
|
+
model: nullable(session.model),
|
|
213
|
+
kind: nullable(session.kind ?? session.agent_kind),
|
|
214
|
+
submissionKinds: submissionKinds(session),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function convoyItem(cityName: string, convoy: UpstreamConvoy, rigs: readonly UpstreamRig[]) {
|
|
219
|
+
const explicitRig = convoy.metadata?.rig;
|
|
220
|
+
const rigName = explicitRig && rigs.some(({ name }) => name === explicitRig) ? explicitRig : null;
|
|
221
|
+
return {
|
|
222
|
+
id: convoy.id,
|
|
223
|
+
cityName,
|
|
224
|
+
rigName,
|
|
225
|
+
title: convoy.title,
|
|
226
|
+
status: convoy.status,
|
|
227
|
+
priority: convoy.priority ?? null,
|
|
228
|
+
assignee: nullable(convoy.assignee),
|
|
229
|
+
createdAt: convoy.created_at,
|
|
230
|
+
updatedAt: nullable(convoy.updated_at),
|
|
231
|
+
totalWork: null,
|
|
232
|
+
closedWork: null,
|
|
233
|
+
blocked: convoy.is_blocked ?? false,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function workItem(cityName: string, rigName: string | null, item: UpstreamWorkItem) {
|
|
238
|
+
return {
|
|
239
|
+
id: item.id,
|
|
240
|
+
cityName,
|
|
241
|
+
rigName,
|
|
242
|
+
title: item.title,
|
|
243
|
+
status: item.status,
|
|
244
|
+
type: item.issue_type,
|
|
245
|
+
priority: item.priority ?? null,
|
|
246
|
+
assignee: nullable(item.assignee),
|
|
247
|
+
createdAt: item.created_at,
|
|
248
|
+
updatedAt: nullable(item.updated_at),
|
|
249
|
+
blocked: item.is_blocked ?? null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function upstreamListTruncated(response: {
|
|
254
|
+
items: readonly unknown[] | null;
|
|
255
|
+
total: number;
|
|
256
|
+
next_cursor?: string;
|
|
257
|
+
partial?: boolean;
|
|
258
|
+
}): boolean {
|
|
259
|
+
return (
|
|
260
|
+
Boolean(response.next_cursor) ||
|
|
261
|
+
Boolean(response.partial) ||
|
|
262
|
+
response.total > (response.items?.length ?? 0)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function eventMetadata(event: UpstreamEvent) {
|
|
267
|
+
const metadata: Record<string, string | number | boolean | null> = {};
|
|
268
|
+
if (
|
|
269
|
+
typeof event.payload === "object" &&
|
|
270
|
+
event.payload !== null &&
|
|
271
|
+
!Array.isArray(event.payload)
|
|
272
|
+
) {
|
|
273
|
+
for (const [key, value] of Object.entries(event.payload)) {
|
|
274
|
+
if (
|
|
275
|
+
Object.keys(metadata).length < GAS_CITY_LIMITS.metadataEntries &&
|
|
276
|
+
(typeof value === "string" ||
|
|
277
|
+
typeof value === "number" ||
|
|
278
|
+
typeof value === "boolean" ||
|
|
279
|
+
value === null)
|
|
280
|
+
) {
|
|
281
|
+
metadata[key] = value;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [key, value] of [
|
|
286
|
+
["session_id", event.session_id],
|
|
287
|
+
["run_id", event.run_id],
|
|
288
|
+
["step_id", event.step_id],
|
|
289
|
+
] as const) {
|
|
290
|
+
if (value && Object.keys(metadata).length < GAS_CITY_LIMITS.metadataEntries)
|
|
291
|
+
metadata[key] = value;
|
|
292
|
+
}
|
|
293
|
+
return metadata;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function eventItem(event: UpstreamEvent, fallbackCity: string | null) {
|
|
297
|
+
return {
|
|
298
|
+
cityName: nullable(event.city) ?? fallbackCity,
|
|
299
|
+
sequence: event.seq,
|
|
300
|
+
type: event.type,
|
|
301
|
+
actor: nullable(event.actor),
|
|
302
|
+
subject: nullable(event.subject ?? event.session_id ?? event.run_id),
|
|
303
|
+
message: nullable(event.message),
|
|
304
|
+
timestamp: event.ts,
|
|
305
|
+
metadata: eventMetadata(event),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseEventItems(items: readonly unknown[] | null, fallbackCity: string | null) {
|
|
310
|
+
const parsed = [];
|
|
311
|
+
let dropped = 0;
|
|
312
|
+
for (const item of items ?? []) {
|
|
313
|
+
const result = UpstreamEventSchema.safeParse(item);
|
|
314
|
+
if (result.success) parsed.push(eventItem(result.data, fallbackCity));
|
|
315
|
+
else dropped += 1;
|
|
316
|
+
}
|
|
317
|
+
return { items: parsed, dropped };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function requireMutations(settings: GasCityRpcSettings, confirmed: boolean) {
|
|
321
|
+
if (!settings.mutationsEnabled) {
|
|
322
|
+
throw new Error("Gas City mutations are disabled by the interactive safety interlock.");
|
|
323
|
+
}
|
|
324
|
+
if (!confirmed) throw new Error("Gas City mutation requires explicit confirmation.");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function workspacePath(
|
|
328
|
+
workspaceId: string,
|
|
329
|
+
context: PluginHandlerContext,
|
|
330
|
+
): Promise<string | null> {
|
|
331
|
+
const workspace = await context.paseo.workspaces.ref(workspaceId).refresh();
|
|
332
|
+
return workspace?.workspaceDirectory ?? null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function createGasCityHandlers(
|
|
336
|
+
dependencies: GasCityHandlerDependencies = {},
|
|
337
|
+
): GasCityHandlers {
|
|
338
|
+
const createClient =
|
|
339
|
+
dependencies.createClient ??
|
|
340
|
+
((settings) =>
|
|
341
|
+
new GasCityClient({
|
|
342
|
+
endpointUrl: settings.endpointUrl,
|
|
343
|
+
allowRemoteEndpoint: settings.allowRemoteEndpoint,
|
|
344
|
+
}));
|
|
345
|
+
const now = dependencies.now ?? (() => new Date());
|
|
346
|
+
|
|
347
|
+
const resources = (input: { settings: GasCityRpcSettings }) => {
|
|
348
|
+
const settings = GasCityRpcSettingsSchema.parse(input.settings);
|
|
349
|
+
return { settings, client: createClient(settings) };
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
async discoverSupervisor(input) {
|
|
354
|
+
try {
|
|
355
|
+
const { client } = resources(input);
|
|
356
|
+
const [health, cities] = await Promise.all([client.health(), client.cities()]);
|
|
357
|
+
return SupervisorDiscoverySchema.parse({
|
|
358
|
+
state: "available",
|
|
359
|
+
supervisor: {
|
|
360
|
+
endpointUrl: client.endpoint.href.replace(/\/$/, ""),
|
|
361
|
+
version: nullable(health.version),
|
|
362
|
+
buildId: nullable(health.build_id),
|
|
363
|
+
uptimeSeconds: health.uptime_sec,
|
|
364
|
+
cityCount: health.cities_total,
|
|
365
|
+
runningCityCount: health.cities_running,
|
|
366
|
+
},
|
|
367
|
+
cities: (cities.items ?? []).map(citySummary),
|
|
368
|
+
diagnostics: [],
|
|
369
|
+
refreshedAt: refreshedAt(now),
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return discoveryFailure(error, now);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async resolveWorkspaceRig(input, context) {
|
|
377
|
+
const { settings, client } = resources(input);
|
|
378
|
+
try {
|
|
379
|
+
const [path, cityResponse] = await Promise.all([
|
|
380
|
+
workspacePath(input.workspaceId, context),
|
|
381
|
+
client.cities(),
|
|
382
|
+
]);
|
|
383
|
+
const cities = cityResponse.items ?? [];
|
|
384
|
+
const rigPairs = await Promise.all(
|
|
385
|
+
cities
|
|
386
|
+
.filter(({ running }) => running)
|
|
387
|
+
.map(async (city) => [city.name, await client.rigs(city.name)] as const),
|
|
388
|
+
);
|
|
389
|
+
const rigsByCity = new Map(
|
|
390
|
+
rigPairs.map(([cityName, response]) => [cityName, response.items ?? []] as const),
|
|
391
|
+
);
|
|
392
|
+
return await mapWorkspaceToRig({
|
|
393
|
+
workspaceId: input.workspaceId,
|
|
394
|
+
workspacePath: path,
|
|
395
|
+
cities,
|
|
396
|
+
rigsByCity,
|
|
397
|
+
overrides: settings.workspaceMappings,
|
|
398
|
+
});
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("[paseo-gas-city] Workspace mapping failed", {
|
|
401
|
+
code: error instanceof GasCityClientError ? error.code : "internal",
|
|
402
|
+
correlationId: error instanceof GasCityClientError ? error.correlationId : null,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
state: "unavailable",
|
|
406
|
+
workspaceId: input.workspaceId,
|
|
407
|
+
workspacePath: null,
|
|
408
|
+
cityName: null,
|
|
409
|
+
rigName: null,
|
|
410
|
+
rigPath: null,
|
|
411
|
+
source: null,
|
|
412
|
+
candidates: [],
|
|
413
|
+
diagnostics: [
|
|
414
|
+
diagnostic(
|
|
415
|
+
"mapping-unavailable",
|
|
416
|
+
"Gas City rig mapping is temporarily unavailable.",
|
|
417
|
+
true,
|
|
418
|
+
),
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
async getCityRigSnapshot(input, _context) {
|
|
424
|
+
const { client } = resources(input);
|
|
425
|
+
try {
|
|
426
|
+
const [cities, status, rigs] = await Promise.all([
|
|
427
|
+
client.cities(),
|
|
428
|
+
client.cityStatus(input.cityName),
|
|
429
|
+
client.rigs(input.cityName),
|
|
430
|
+
]);
|
|
431
|
+
const city = (cities.items ?? []).find(({ name }) => name === input.cityName);
|
|
432
|
+
const diagnostics: GasCityDiagnostic[] = [];
|
|
433
|
+
if (status.partial) {
|
|
434
|
+
diagnostics.push(
|
|
435
|
+
diagnostic("partial-status", "Gas City reported an incomplete city snapshot.", true),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const rigItems = rigs.items ?? [];
|
|
439
|
+
const selectedRig = input.rigName
|
|
440
|
+
? (rigItems.find(({ name }) => name === input.rigName) ?? null)
|
|
441
|
+
: null;
|
|
442
|
+
if (input.rigName && !selectedRig) {
|
|
443
|
+
diagnostics.push(
|
|
444
|
+
diagnostic("rig-not-found", "The requested Gas City rig was not found.", false),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return CityRigSnapshotSchema.parse({
|
|
448
|
+
city: {
|
|
449
|
+
name: city?.name ?? status.name,
|
|
450
|
+
path: city?.path ?? status.path,
|
|
451
|
+
running: city?.running ?? true,
|
|
452
|
+
status: nullable(city?.status),
|
|
453
|
+
error: nullable(city?.error),
|
|
454
|
+
completedPhases: city?.phases_completed ?? [],
|
|
455
|
+
suspended: status.suspended,
|
|
456
|
+
uptimeSeconds: status.uptime_sec,
|
|
457
|
+
agents: {
|
|
458
|
+
total: status.agents.total,
|
|
459
|
+
running: status.agents.running,
|
|
460
|
+
suspended: status.agents.suspended,
|
|
461
|
+
quarantined: status.agents.quarantined,
|
|
462
|
+
},
|
|
463
|
+
sessions: status.session_counts_detail ?? { active: 0, suspended: 0 },
|
|
464
|
+
work: {
|
|
465
|
+
open: status.work.open,
|
|
466
|
+
ready: status.work.ready,
|
|
467
|
+
inProgress: status.work.in_progress,
|
|
468
|
+
},
|
|
469
|
+
totalsScope: "city",
|
|
470
|
+
},
|
|
471
|
+
rig: selectedRig ? rigSummary(selectedRig) : null,
|
|
472
|
+
rigs: rigItems.map(rigSummary),
|
|
473
|
+
partial: Boolean(status.partial) || (input.rigName !== null && selectedRig === null),
|
|
474
|
+
diagnostics,
|
|
475
|
+
refreshedAt: refreshedAt(now),
|
|
476
|
+
});
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw safeError("load the Gas City snapshot", error);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
async listSessions(input) {
|
|
483
|
+
const { client } = resources(input);
|
|
484
|
+
try {
|
|
485
|
+
const response = await client.sessions(input.cityName);
|
|
486
|
+
const items = (response.items ?? [])
|
|
487
|
+
.filter((session) => input.rigName === null || session.rig === input.rigName)
|
|
488
|
+
.map((session) => sessionItem(input.cityName, session));
|
|
489
|
+
return SessionListSchema.parse({
|
|
490
|
+
scope: input.rigName === null ? "city" : "rig",
|
|
491
|
+
items,
|
|
492
|
+
truncated: upstreamListTruncated(response),
|
|
493
|
+
refreshedAt: refreshedAt(now),
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
throw safeError("list Gas City sessions", error);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
async listConvoys(input) {
|
|
501
|
+
const { client } = resources(input);
|
|
502
|
+
try {
|
|
503
|
+
const [response, rigs] = await Promise.all([
|
|
504
|
+
client.convoys(input.cityName),
|
|
505
|
+
client.rigs(input.cityName),
|
|
506
|
+
]);
|
|
507
|
+
const rigItems = rigs.items ?? [];
|
|
508
|
+
const items = (response.items ?? [])
|
|
509
|
+
.map((convoy) => convoyItem(input.cityName, convoy, rigItems))
|
|
510
|
+
.filter(
|
|
511
|
+
(convoy) =>
|
|
512
|
+
input.rigName === null || convoy.rigName === null || convoy.rigName === input.rigName,
|
|
513
|
+
);
|
|
514
|
+
return ConvoyListSchema.parse({
|
|
515
|
+
scope: input.rigName === null ? "city" : "rig-and-unattributed",
|
|
516
|
+
items,
|
|
517
|
+
truncated: upstreamListTruncated(response),
|
|
518
|
+
refreshedAt: refreshedAt(now),
|
|
519
|
+
});
|
|
520
|
+
} catch (error) {
|
|
521
|
+
throw safeError("list Gas City convoys", error);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
async listWork(input) {
|
|
526
|
+
const { settings, client } = resources(input);
|
|
527
|
+
try {
|
|
528
|
+
const response = await client.work(input.cityName, input.rigName, settings.eventLimit);
|
|
529
|
+
return WorkListSchema.parse({
|
|
530
|
+
scope: input.rigName === null ? "city" : "rig",
|
|
531
|
+
items: (response.items ?? []).map((item) =>
|
|
532
|
+
workItem(input.cityName, input.rigName, item),
|
|
533
|
+
),
|
|
534
|
+
truncated: upstreamListTruncated(response),
|
|
535
|
+
partial: Boolean(response.partial),
|
|
536
|
+
refreshedAt: refreshedAt(now),
|
|
537
|
+
});
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw safeError("list Gas City work", error);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
async listEvents(input) {
|
|
544
|
+
const { settings, client } = resources(input);
|
|
545
|
+
try {
|
|
546
|
+
if (input.scope === "supervisor") {
|
|
547
|
+
const response = await client.supervisorEvents(settings.eventLimit);
|
|
548
|
+
const events = parseEventItems(response.items, null);
|
|
549
|
+
return EventListSchema.parse({
|
|
550
|
+
scope: "supervisor-head",
|
|
551
|
+
items: events.items,
|
|
552
|
+
cursor: null,
|
|
553
|
+
truncated: events.dropped > 0 || response.total > events.items.length,
|
|
554
|
+
refreshedAt: refreshedAt(now),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const response = await client.cityEvents(input.cityName, input.cursor, settings.eventLimit);
|
|
558
|
+
const events = parseEventItems(response.items, input.cityName);
|
|
559
|
+
return EventListSchema.parse({
|
|
560
|
+
scope: "city",
|
|
561
|
+
items: events.items,
|
|
562
|
+
cursor: nullable(response.next_cursor),
|
|
563
|
+
truncated: events.dropped > 0 || upstreamListTruncated(response),
|
|
564
|
+
refreshedAt: refreshedAt(now),
|
|
565
|
+
});
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw safeError("list Gas City events", error);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
async listAttention(input) {
|
|
572
|
+
const { client } = resources(input);
|
|
573
|
+
try {
|
|
574
|
+
const [status, sessions, convoys, pending, rigs] = await Promise.all([
|
|
575
|
+
client.cityStatus(input.cityName),
|
|
576
|
+
client.sessions(input.cityName),
|
|
577
|
+
client.convoys(input.cityName),
|
|
578
|
+
client.pending(input.cityName),
|
|
579
|
+
client.rigs(input.cityName),
|
|
580
|
+
]);
|
|
581
|
+
const observedAt = refreshedAt(now);
|
|
582
|
+
const sessionById = new Map((sessions.items ?? []).map((session) => [session.id, session]));
|
|
583
|
+
const rigItems = rigs.items ?? [];
|
|
584
|
+
const items: Array<{
|
|
585
|
+
id: string;
|
|
586
|
+
cityName: string;
|
|
587
|
+
rigName: string | null;
|
|
588
|
+
kind: "city" | "rig" | "session" | "convoy" | "work";
|
|
589
|
+
severity: "info" | "warning" | "critical";
|
|
590
|
+
code: string;
|
|
591
|
+
title: string;
|
|
592
|
+
message: string;
|
|
593
|
+
requestId: string | null;
|
|
594
|
+
resourceId: string | null;
|
|
595
|
+
observedAt: string;
|
|
596
|
+
}> = [];
|
|
597
|
+
if (status.suspended) {
|
|
598
|
+
items.push({
|
|
599
|
+
id: `city:${input.cityName}:suspended`,
|
|
600
|
+
cityName: input.cityName,
|
|
601
|
+
rigName: null,
|
|
602
|
+
kind: "city",
|
|
603
|
+
severity: "warning",
|
|
604
|
+
code: "city-suspended",
|
|
605
|
+
title: `${input.cityName} is suspended`,
|
|
606
|
+
message: "The city will not reconcile work until it is resumed.",
|
|
607
|
+
requestId: null,
|
|
608
|
+
resourceId: input.cityName,
|
|
609
|
+
observedAt,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (status.agents.quarantined > 0) {
|
|
613
|
+
items.push({
|
|
614
|
+
id: `city:${input.cityName}:quarantined`,
|
|
615
|
+
cityName: input.cityName,
|
|
616
|
+
rigName: null,
|
|
617
|
+
kind: "city",
|
|
618
|
+
severity: "critical",
|
|
619
|
+
code: "agents-quarantined",
|
|
620
|
+
title: "Agents are quarantined",
|
|
621
|
+
message: `${status.agents.quarantined} agent(s) require operator attention.`,
|
|
622
|
+
requestId: null,
|
|
623
|
+
resourceId: input.cityName,
|
|
624
|
+
observedAt,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (status.partial) {
|
|
628
|
+
items.push({
|
|
629
|
+
id: `city:${input.cityName}:partial`,
|
|
630
|
+
cityName: input.cityName,
|
|
631
|
+
rigName: null,
|
|
632
|
+
kind: "city",
|
|
633
|
+
severity: "warning",
|
|
634
|
+
code: "partial-status",
|
|
635
|
+
title: "City status is incomplete",
|
|
636
|
+
message: "One or more Gas City status backends did not respond.",
|
|
637
|
+
requestId: null,
|
|
638
|
+
resourceId: input.cityName,
|
|
639
|
+
observedAt,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
for (const entry of pending.items ?? []) {
|
|
643
|
+
const session = sessionById.get(entry.session_id);
|
|
644
|
+
if (input.rigName !== null && session?.rig !== input.rigName) continue;
|
|
645
|
+
items.push({
|
|
646
|
+
id: `session:${entry.session_id}:pending:${entry.request_id}`,
|
|
647
|
+
cityName: input.cityName,
|
|
648
|
+
rigName: nullable(session?.rig),
|
|
649
|
+
kind: "session",
|
|
650
|
+
severity: "warning",
|
|
651
|
+
code: "interaction-pending",
|
|
652
|
+
title: `${session?.title ?? entry.session_id} needs input`,
|
|
653
|
+
message: `The session is waiting for an operator response (${entry.kind}).`,
|
|
654
|
+
requestId: entry.request_id,
|
|
655
|
+
resourceId: entry.session_id,
|
|
656
|
+
observedAt,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
for (const convoy of convoys.items ?? []) {
|
|
660
|
+
if (!convoy.is_blocked) continue;
|
|
661
|
+
const rigName = convoyItem(input.cityName, convoy, rigItems).rigName;
|
|
662
|
+
if (input.rigName !== null && rigName !== null && rigName !== input.rigName) continue;
|
|
663
|
+
items.push({
|
|
664
|
+
id: `convoy:${convoy.id}:blocked`,
|
|
665
|
+
cityName: input.cityName,
|
|
666
|
+
rigName,
|
|
667
|
+
kind: "convoy",
|
|
668
|
+
severity: "warning",
|
|
669
|
+
code: "convoy-blocked",
|
|
670
|
+
title: `${convoy.title} is blocked`,
|
|
671
|
+
message: "The convoy has unresolved dependencies.",
|
|
672
|
+
requestId: null,
|
|
673
|
+
resourceId: convoy.id,
|
|
674
|
+
observedAt,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const upstreamTruncated = [sessions, convoys, pending, rigs].some(upstreamListTruncated);
|
|
678
|
+
const truncated = items.length > GAS_CITY_LIMITS.attentionItems || upstreamTruncated;
|
|
679
|
+
return AttentionListSchema.parse({
|
|
680
|
+
items: items.slice(0, GAS_CITY_LIMITS.attentionItems),
|
|
681
|
+
scope: input.rigName === null ? "city" : "city-and-rig",
|
|
682
|
+
truncated,
|
|
683
|
+
refreshedAt: observedAt,
|
|
684
|
+
});
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw safeError("derive Gas City attention", error);
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
async dispatchWork(input) {
|
|
691
|
+
const { settings, client } = resources(input);
|
|
692
|
+
const request = input.request;
|
|
693
|
+
requireMutations(settings, request.confirmed);
|
|
694
|
+
const target = request.target;
|
|
695
|
+
const scope = target.rigName
|
|
696
|
+
? { rig: target.rigName, scope_kind: "rig", scope_ref: target.rigName }
|
|
697
|
+
: {};
|
|
698
|
+
const body =
|
|
699
|
+
request.kind === "bead"
|
|
700
|
+
? {
|
|
701
|
+
...scope,
|
|
702
|
+
target: target.agent,
|
|
703
|
+
bead: request.beadId,
|
|
704
|
+
reassign: request.reassign,
|
|
705
|
+
owned: request.owned,
|
|
706
|
+
force: request.force,
|
|
707
|
+
no_formula: request.noFormula,
|
|
708
|
+
no_convoy: request.noConvoy,
|
|
709
|
+
merge: request.merge,
|
|
710
|
+
}
|
|
711
|
+
: {
|
|
712
|
+
...scope,
|
|
713
|
+
target: target.agent,
|
|
714
|
+
formula: request.formula,
|
|
715
|
+
title: request.title,
|
|
716
|
+
attached_bead_id: request.attachedBeadId ?? undefined,
|
|
717
|
+
vars: Object.fromEntries(
|
|
718
|
+
Object.entries(request.variables).map(([key, value]) => [key, String(value)]),
|
|
719
|
+
),
|
|
720
|
+
force: request.force,
|
|
721
|
+
merge: request.merge,
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
const result = await client.sling(target.cityName, body);
|
|
725
|
+
return DispatchResultSchema.parse({
|
|
726
|
+
status: result.status,
|
|
727
|
+
target: result.target,
|
|
728
|
+
beadId: nullable(result.bead),
|
|
729
|
+
formula: nullable(result.formula),
|
|
730
|
+
workflowId: nullable(result.workflow_id),
|
|
731
|
+
rootBeadId: nullable(result.root_bead_id),
|
|
732
|
+
dashboardUrl: nullable(result.dashboard_url),
|
|
733
|
+
warnings: result.warnings ?? [],
|
|
734
|
+
});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw safeError("dispatch Gas City work", error);
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
async performSessionAction(input) {
|
|
741
|
+
const { settings, client } = resources(input);
|
|
742
|
+
const request = input.request;
|
|
743
|
+
requireMutations(settings, request.confirmed);
|
|
744
|
+
let action: string = request.action;
|
|
745
|
+
let body: unknown;
|
|
746
|
+
if (request.action === "message") body = { message: request.message };
|
|
747
|
+
if (request.action === "submit") body = { message: request.message, intent: request.intent };
|
|
748
|
+
if (request.action === "respond") {
|
|
749
|
+
body = {
|
|
750
|
+
request_id: request.requestId,
|
|
751
|
+
action: request.response,
|
|
752
|
+
text: request.text ?? undefined,
|
|
753
|
+
metadata: Object.fromEntries(
|
|
754
|
+
Object.entries(request.metadata).map(([key, value]) => [key, String(value)]),
|
|
755
|
+
),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (request.action === "message") action = "messages";
|
|
759
|
+
try {
|
|
760
|
+
const result = await client.sessionAction(
|
|
761
|
+
request.cityName,
|
|
762
|
+
request.sessionId,
|
|
763
|
+
action,
|
|
764
|
+
body,
|
|
765
|
+
);
|
|
766
|
+
return SessionActionResultSchema.parse({
|
|
767
|
+
status: result.status,
|
|
768
|
+
sessionId: result.id ?? request.sessionId,
|
|
769
|
+
requestId: nullable(result.request_id),
|
|
770
|
+
eventCursor: result.event_cursor === undefined ? null : String(result.event_cursor),
|
|
771
|
+
});
|
|
772
|
+
} catch (error) {
|
|
773
|
+
throw safeError("perform the Gas City session action", error);
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const defaultHandlers = createGasCityHandlers();
|
|
780
|
+
|
|
781
|
+
export const handleDiscoverSupervisor = defaultHandlers.discoverSupervisor;
|
|
782
|
+
export const handleResolveWorkspaceRig = defaultHandlers.resolveWorkspaceRig;
|
|
783
|
+
export const handleGetCityRigSnapshot = defaultHandlers.getCityRigSnapshot;
|
|
784
|
+
export const handleListSessions = defaultHandlers.listSessions;
|
|
785
|
+
export const handleListConvoys = defaultHandlers.listConvoys;
|
|
786
|
+
export const handleListWork = defaultHandlers.listWork;
|
|
787
|
+
export const handleListEvents = defaultHandlers.listEvents;
|
|
788
|
+
export const handleListAttention = defaultHandlers.listAttention;
|
|
789
|
+
export const handleDispatchWork = defaultHandlers.dispatchWork;
|
|
790
|
+
export const handlePerformSessionAction = defaultHandlers.performSessionAction;
|