Build workspace agent, capability graph, and versioned cutovers

This commit is contained in:
2026-09-05 12:33:52 -07:00
parent 2de080c09f
commit fa9e77e75c
19 changed files with 321 additions and 54 deletions
+59 -11
View File
@@ -139,8 +139,10 @@ export type EdgePort = {
connect(targetObjectId: string): Promise<void>;
};
export type InterfacePort = {
objectId: string;
interfaceRevisionId: string;
invoke(operationId: string, input?: Record<string, unknown>): Promise<unknown>;
live(operationId: string, input?: Record<string, unknown>): Promise<ReturnType<typeof liveValue>>;
};
export type ConstructorPort = {
atomId: string;
@@ -212,18 +214,44 @@ export const createRuntimeContext = (
ports.set(dependency.portId, edge);
break;
}
case "receiverInterfaceRevisionId": {
case "interfaceRevisionId": {
const interfaceRevisionId = dependency.binding.value;
const dependencyObjectId = dependency.objectId || request.objectId;
const invoke = async (operationId: string, input: Record<string, unknown>) => {
const response = await orch.invokeCapability({
capability: create(CapabilityRefSchema, { interfaceRevisionId, operationId }),
objectId: dependencyObjectId,
input: Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsToProtoValue(value)])),
});
if (!response.ok) throw new Error(response.error || `Capability ${operationId} failed`);
for (const dependency of response.dependencies) {
if (dependency.kind === "state") {
await recordDependency({
kind: "state",
objectId: dependency.objectId,
attachmentId: dependency.attachmentId,
});
} else if (dependency.kind === "edge") {
await recordDependency({
kind: "edge",
objectId: dependency.objectId,
attachmentId: dependency.attachmentId,
projectionId: dependency.projectionId,
});
}
}
return response.result;
};
const capability: InterfacePort = {
objectId: dependencyObjectId,
interfaceRevisionId,
async invoke(operationId, input = {}) {
const response = await orch.invokeCapability({
capability: create(CapabilityRefSchema, { interfaceRevisionId, operationId }),
objectId: request.objectId,
input: Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsToProtoValue(value)])),
});
if (!response.ok) throw new Error(response.error || `Capability ${operationId} failed`);
return protoValueToJs(response.result);
return protoValueToJs(await invoke(operationId, input));
},
async live(operationId, input = {}) {
const value = await invoke(operationId, input);
if (!value) throw new Error(`Capability ${operationId} returned no value`);
return liveValue(value);
},
};
ports.set(dependency.portId, capability);
@@ -329,7 +357,11 @@ export const createPackageRuntimeRoutes = (config: {
if (!handler) throw new ConnectError(`Unknown export ${exportId ?? ""}`, Code.NotFound);
try {
const result = await evaluate(handler, createRuntimeContext(camino, orch, request));
return create(InvokeResponseSchema, { ok: true, result: result.value });
return create(InvokeResponseSchema, {
ok: true,
result: result.value,
dependencies: protoDependencies(result.dependencies),
});
} catch (error) {
return create(InvokeResponseSchema, {
ok: false,
@@ -354,6 +386,7 @@ export const createPackageRuntimeRoutes = (config: {
};
const subscriptions = new Map<string, Subscription>();
const establishing = new Map<string, Promise<void>>();
let subscriptionEpoch = 0;
const ensureSubscription = async (dependency: RuntimeDependency) => {
const key = dependencyKey(dependency);
@@ -383,6 +416,7 @@ export const createPackageRuntimeRoutes = (config: {
};
subscription.next = waitNext();
subscriptions.set(key, subscription);
subscriptionEpoch += 1;
} catch (error) {
controller.abort();
throw error;
@@ -403,7 +437,21 @@ export const createPackageRuntimeRoutes = (config: {
};
context.signal.addEventListener("abort", abortAll, { once: true });
let current = await evaluate(handler, runtimeContext, ensureSubscription);
const evaluateWithStableSubscriptions = async () => {
// A direct state/edge port records its dependency before reading it,
// but a nested interface invocation can only report its transitive
// dependencies after that invocation returns. Once a new dependency
// stream is established, evaluate again so every read contributing to
// the emitted value happened after its stream became live.
for (let pass = 0; pass < 32; pass += 1) {
const before = subscriptionEpoch;
const result = await evaluate(handler, runtimeContext, ensureSubscription);
if (subscriptionEpoch === before) return result;
}
throw new Error("Derived dependency discovery did not stabilize after 32 passes");
};
let current = await evaluateWithStableSubscriptions();
yield create(WatchEventSchema, {
watchId,
value: current.value,
@@ -432,7 +480,7 @@ export const createPackageRuntimeRoutes = (config: {
if (outcome.done) throw new Error(`Dependency stream ${outcome.key} ended unexpectedly`);
subscription.next = subscription.waitNext();
const updated = await evaluate(handler, runtimeContext, ensureSubscription);
const updated = await evaluateWithStableSubscriptions();
const active = new Set(updated.dependencies.map(dependencyKey));
for (const [key, entry] of subscriptions) {
if (!active.has(key)) {