Skip to content

Commit bd0fa04

Browse files
committed
fix(qoder): keep forward resource ownership consistent
Change-Id: I611e80304831f8240784d56362f64bcf45a0e641
1 parent 51af416 commit bd0fa04

10 files changed

Lines changed: 285 additions & 52 deletions

File tree

packages/sdk/src/internal/core/validate-config.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,35 @@ export function collectProviderCapabilities(
264264
}
265265
}
266266

267+
if (providerName === "qoder") {
268+
const domains = new Map<string, Set<"managed" | "forward">>();
269+
for (const agent of Object.values(config.agents ?? {})) {
270+
if (agent.provider && agent.provider !== providerName) continue;
271+
const mode = agent.delivery?.qoder?.type === "forward" ? "forward" : "managed";
272+
const refs = [
273+
...(agent.skills ?? []).flatMap((skill) =>
274+
typeof skill === "string" ? [`skill:${skill}`] : skill.type === "custom" ? [`skill:${skill.skill_id}`] : [],
275+
),
276+
...(agent.vault ? [`vault:${agent.vault}`] : []),
277+
...(agent.memory_stores ?? []).map((store) => `memory_store:${store}`),
278+
];
279+
for (const ref of refs) {
280+
const modes = domains.get(ref) ?? new Set<"managed" | "forward">();
281+
modes.add(mode);
282+
domains.set(ref, modes);
283+
}
284+
}
285+
for (const [ref, modes] of domains) {
286+
if (modes.size < 2) continue;
287+
const [type, name] = ref.split(":") as ["skill" | "vault" | "memory_store", string];
288+
diagnostics.error(
289+
`qoder.${type}.delivery_domain.conflict`,
290+
`${type}.${name}: referenced by both Managed and Forward agents; declare separate resources because Qoder uses different API domains.`,
291+
{ type, name, provider: providerName },
292+
);
293+
}
294+
}
295+
267296
for (const [name, agent] of Object.entries(config.agents ?? {})) {
268297
if (agent.provider && agent.provider !== providerName) continue;
269298
const delivery = agent.delivery?.[providerName]?.type ?? "managed";
@@ -364,10 +393,10 @@ export function collectProviderCapabilities(
364393
{ type: "template", name, provider: providerName },
365394
);
366395
}
367-
if (agent.memory_stores?.length) {
396+
if (agent.memory_stores?.length && !config.defaults?.identity) {
368397
diagnostics.error(
369-
"qoder.template.memory_store.unsupported",
370-
`agent.${name}: memory_stores are not yet supported by Qoder Forward Template delivery.`,
398+
"qoder.template.memory_store.identity.required",
399+
`agent.${name}: Forward memory_stores require defaults.identity for the Identity/Template mount.`,
371400
{ type: "template", name, provider: providerName },
372401
);
373402
}

packages/sdk/src/internal/executor/executor.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ async function executeActionInner(
328328
break;
329329
case "memory_store":
330330
if (!provider.deleteMemoryStore) throw memoryStoreUnsupported(address.provider);
331-
await provider.deleteMemoryStore(id);
331+
await provider.deleteMemoryStore(id, apiMode);
332332
break;
333333
case "deployment":
334334
await provider.deleteDeployment(id);
@@ -504,18 +504,22 @@ async function executeActionInner(
504504
throw memoryStoreUnsupported(address.provider);
505505
}
506506
const reconcile = async (storeId: string): Promise<RemoteResource> => {
507-
const store = await provider.updateMemoryStore!(storeId, {
508-
name,
509-
description: decl.description,
510-
metadata: decl.metadata ?? {},
511-
});
507+
const store = await provider.updateMemoryStore!(
508+
storeId,
509+
{
510+
name,
511+
description: decl.description,
512+
metadata: decl.metadata ?? {},
513+
},
514+
apiMode,
515+
);
512516

513517
// Declarative entries are managed seeds. Update/create those paths in place,
514518
// while preserving memories learned by agents at runtime.
515519
const current = new Map<string, { id: string; content_sha256: string }>();
516520
let cursor: string | undefined;
517521
do {
518-
const page = await provider.listMemories!(storeId, { limit: 100, cursor, view: "basic" });
522+
const page = await provider.listMemories!(storeId, { limit: 100, cursor, view: "basic" }, apiMode);
519523
for (const memory of page.data) {
520524
if (memory.type === "memory") current.set(memory.path, memory);
521525
}
@@ -526,22 +530,30 @@ async function executeActionInner(
526530
const existing = current.get(entry.key.replace(/^\/+/, ""));
527531
if (existing) {
528532
if (existing.content_sha256 !== sha256(entry.content)) {
529-
await provider.updateMemory!(storeId, existing.id, {
530-
content: entry.content,
531-
expected_content_sha256: existing.content_sha256,
532-
});
533+
await provider.updateMemory!(
534+
storeId,
535+
existing.id,
536+
{
537+
content: entry.content,
538+
expected_content_sha256: existing.content_sha256,
539+
},
540+
apiMode,
541+
);
533542
}
534543
} else {
535-
await provider.createMemory!(storeId, { path: entry.key, content: entry.content });
544+
await provider.createMemory!(storeId, { path: entry.key, content: entry.content }, apiMode);
536545
}
537546
}
538547
return store;
539548
};
540-
if (isUpdate) {
549+
if (apiModeChanged) {
550+
result = await createMemoryStore(name, decl, apiMode);
551+
if (existingId) await deleteMemoryStore(existingId, priorApiMode).catch(() => undefined);
552+
} else if (isUpdate) {
541553
result = await reconcile(existingId!);
542554
} else {
543555
try {
544-
result = await createMemoryStore(name, decl);
556+
result = await createMemoryStore(name, decl, apiMode);
545557
} catch (err) {
546558
result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
547559
onExisting: async (existing) => reconcile(existing.id!),

packages/sdk/src/internal/executor/resolver.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,18 @@ export function resolveTemplateRefs(
8787
if (!environment) throw new UserError(`Environment '${agent.environment}' is not defined in config.`);
8888

8989
const agentRefs = resolveAgentRefs(agentName, config, provider, state);
90+
const memoryStoreIds = agent.memory_stores?.map((memoryStore) =>
91+
requireRef(state, { type: "memory_store", name: memoryStore, provider }),
92+
);
93+
const identityName = config.defaults?.identity;
9094
return {
9195
...agentRefs,
9296
environment_id:
9397
environment.environment_id ?? requireRef(state, { type: "environment", name: agent.environment, provider }),
9498
...(agent.tunnel ? { tunnel_id: resolveTunnelIdFromConfig(config, agent.tunnel, provider) } : {}),
9599
vault_ids: agent.vault ? [requireRef(state, { type: "vault", name: agent.vault, provider })] : [],
100+
...(memoryStoreIds ? { memory_store_ids: memoryStoreIds } : {}),
101+
...(identityName ? { identity_id: requireRef(state, { type: "identity", name: identityName, provider }) } : {}),
96102
};
97103
}
98104

packages/sdk/src/internal/graph/dependency.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function buildDependencyGraph(config: ProjectConfig, targetProviders: str
8888
const agentAddr: ResourceAddress = { type: materialization.resourceType, name, provider };
8989
addNode(agentAddr);
9090

91-
if (decl.default_memory_store && materialization.resourceType === "template") {
91+
if ((decl.default_memory_store || decl.memory_stores?.length) && materialization.resourceType === "template") {
9292
const identityName = config.defaults?.identity;
9393
if (identityName) {
9494
const identityAddr: ResourceAddress = { type: "identity", name: identityName, provider };

packages/sdk/src/internal/planner/hasher.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ export async function computeResourceHash(
2222

2323
if (address.type === "skill") {
2424
const skillDecl = decl as { source: string };
25+
const apiMode = resolveQoderApiMode(address.type, address.name, address.provider, config);
2526
if (basePath) {
2627
const fileHash = computeSkillContentHash(skillDecl.source, basePath);
27-
return contentHash({ decl, fileHash });
28+
return contentHash({ decl, fileHash, apiMode });
2829
}
30+
return contentHash({ decl, apiMode });
31+
}
32+
33+
if (address.type === "vault" || address.type === "memory_store") {
34+
return contentHash({ decl, apiMode: resolveQoderApiMode(address.type, address.name, address.provider, config) });
2935
}
3036

3137
if (address.type === "file" && basePath) {
@@ -58,6 +64,28 @@ export async function computeResourceHash(
5864
return contentHash(decl);
5965
}
6066

67+
function resolveQoderApiMode(
68+
type: "skill" | "vault" | "memory_store",
69+
name: string,
70+
provider: string,
71+
config: ProjectConfig,
72+
): "managed" | "forward" | undefined {
73+
if (provider !== "qoder") return undefined;
74+
for (const agent of Object.values(config.agents ?? {})) {
75+
if (agent.provider && agent.provider !== provider) continue;
76+
const referenced =
77+
type === "skill"
78+
? agent.skills?.some((skill) =>
79+
typeof skill === "string" ? skill === name : skill.type === "custom" && skill.skill_id === name,
80+
)
81+
: type === "vault"
82+
? agent.vault === name
83+
: agent.memory_stores?.includes(name);
84+
if (referenced && agent.delivery?.qoder?.type === "forward") return "forward";
85+
}
86+
return "managed";
87+
}
88+
6189
/** Stable, non-reversible identity hint for resources whose YAML key may change. */
6290
export function computeReplacementFingerprint(address: ResourceAddress, config: ProjectConfig): string | undefined {
6391
if (address.type !== "channel") return undefined;

packages/sdk/src/internal/providers/base-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export abstract class BaseApiClient {
3434
throw new ApiError(res.status, body, this.errorPrefix);
3535
}
3636

37-
async post(path: string, body: unknown): Promise<unknown> {
37+
async post(path: string, body: unknown, options?: { headers?: Record<string, string> }): Promise<unknown> {
3838
const res = await resolveFetch()(`${this.baseUrl}${path}`, {
3939
method: "POST",
40-
headers: this.headers(),
40+
headers: { ...this.headers(), ...options?.headers },
4141
body: JSON.stringify(body),
4242
});
4343
await this.throwIfError(res);

packages/sdk/src/internal/providers/interface.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface ResolvedTemplateRefs extends ResolvedAgentRefs {
8787
/** Qoder BYOC private-network route used by Forward Templates. */
8888
tunnel_id?: string;
8989
vault_ids: string[];
90+
identity_id?: string;
91+
memory_store_ids?: string[];
9092
}
9193

9294
export interface ResolvedDeploymentRefs {
@@ -231,17 +233,26 @@ export interface ProviderAdapter {
231233
updateChannel?(id: string, name: string, decl: ChannelDecl, refs: ResolvedChannelRefs): Promise<RemoteResource>;
232234
deleteChannel?(id: string): Promise<void>;
233235

234-
createMemoryStore?(name: string, decl: MemoryStoreDecl): Promise<RemoteResource>;
235-
deleteMemoryStore?(id: string): Promise<void>;
236+
createMemoryStore?(name: string, decl: MemoryStoreDecl, mode?: ProviderResourceMode): Promise<RemoteResource>;
237+
deleteMemoryStore?(id: string, mode?: ProviderResourceMode): Promise<void>;
236238
listMemoryStores?(options?: MemoryStoreListOptions): Promise<MemoryPage<MemoryStoreInfo>>;
237239
getMemoryStore?(id: string): Promise<MemoryStoreInfo>;
238-
updateMemoryStore?(id: string, input: UpdateMemoryStoreInput): Promise<MemoryStoreInfo>;
240+
updateMemoryStore?(id: string, input: UpdateMemoryStoreInput, mode?: ProviderResourceMode): Promise<MemoryStoreInfo>;
239241
archiveMemoryStore?(id: string): Promise<MemoryStoreInfo>;
240-
createMemory?(storeId: string, input: CreateMemoryInput): Promise<MemoryInfo>;
242+
createMemory?(storeId: string, input: CreateMemoryInput, mode?: ProviderResourceMode): Promise<MemoryInfo>;
241243
batchCreateMemories?(storeId: string, input: BatchCreateMemoryInput): Promise<BatchCreateMemoryResult>;
242-
listMemories?(storeId: string, options?: MemoryListOptions): Promise<MemoryPage<MemoryListItem>>;
244+
listMemories?(
245+
storeId: string,
246+
options?: MemoryListOptions,
247+
mode?: ProviderResourceMode,
248+
): Promise<MemoryPage<MemoryListItem>>;
243249
getMemory?(storeId: string, memoryId: string): Promise<MemoryInfo>;
244-
updateMemory?(storeId: string, memoryId: string, input: UpdateMemoryInput): Promise<MemoryInfo>;
250+
updateMemory?(
251+
storeId: string,
252+
memoryId: string,
253+
input: UpdateMemoryInput,
254+
mode?: ProviderResourceMode,
255+
): Promise<MemoryInfo>;
245256
deleteMemory?(storeId: string, memoryId: string, expectedContentSha256?: string): Promise<void>;
246257
listMemoryVersions?(storeId: string, options?: MemoryVersionListOptions): Promise<MemoryPage<MemoryVersionInfo>>;
247258
getMemoryVersion?(storeId: string, versionId: string): Promise<MemoryVersionInfo>;

0 commit comments

Comments
 (0)