Skip to content

Commit 407cc75

Browse files
authored
Update SDK and configuration (#88)
Change-Id: Iccd38f76d6e9c122ae867d3825476f58184896d9 Co-developed-by: Qoder <noreply@qoder.com>
1 parent e776303 commit 407cc75

13 files changed

Lines changed: 330 additions & 43 deletions

File tree

.changeset/pairing-channel.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@openagentpack/sdk": minor
3+
"@openagentpack/cli": minor
4+
"@openagentpack/playground": minor
5+
---
6+
7+
Add `mode: pairing` support for Qoder Channels.
8+
9+
`channels[].mode` now accepts `fixed` (default) or `pairing`. Pairing-mode channels create a transport-only IM connection without binding to an Identity or Template, which is required for Forward Schedule sinks such as scheduled group broadcasts. Fixed-mode channels retain the existing behavior and continue to require `agent` and `identity`.

docs/reference/configuration.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ External references are verified and recorded but never updated or deleted.
7373
channels:
7474
support-dingtalk:
7575
provider: qoder # optional; inherits defaults.provider
76-
agent: support-agent
77-
identity: chen # optional; inherits defaults.identity
76+
agent: support-agent # required for mode: fixed; ignored for mode: pairing
77+
identity: chen # optional; inherits defaults.identity. ignored for mode: pairing
7878
type: dingtalk
79+
mode: fixed # optional; defaults to fixed
7980
name: Support DingTalk # optional; defaults to the YAML key
8081
enabled: true # optional; defaults to true
8182
credentials:
@@ -86,7 +87,34 @@ channels:
8687
include_thinking: false
8788
```
8889

89-
The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.
90+
| Field | Type | Required | Description |
91+
| ----- | ---- | -------- | ----------- |
92+
| `provider` | string | no | Provider name; inherits `defaults.provider`. |
93+
| `agent` | string | conditional | Logical Agent name. Required for `fixed` mode; ignored for `pairing` mode. |
94+
| `identity` | string | conditional | Logical Identity name; inherits `defaults.identity`. Required for `fixed` mode; ignored for `pairing` mode. |
95+
| `type` | string | yes | Provider-specific channel type. Qoder supports `dingtalk`, `feishu`, and `wecom`; `wechat` is QR-only. |
96+
| `mode` | `fixed` \| `pairing` | no | `fixed` (default) binds the channel to one Identity/Template. `pairing` creates a transport-only channel for Schedules/Sinks. |
97+
| `name` | string | no | Display name; defaults to the YAML key. |
98+
| `enabled` | boolean | no | Defaults to `true`. |
99+
| `credentials` | map | conditional | Provider-specific credentials. Required for credential-based channel types. |
100+
| `options` | map | no | Provider-specific response options, e.g. `include_tool_calls`, `include_thinking`. |
101+
102+
The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels in `fixed` mode require the referenced Agent to use Forward delivery. `pairing` mode omits Identity/Template binding and is intended for Schedule sinks such as scheduled group broadcasts. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.
103+
104+
### Managed tool config
105+
106+
`managed_tool_config` declares the provider-operated tools an Agent Harness runs
107+
itself, rather than tools the model calls through the sandbox. Schedule
108+
management is the current use: enabling `create_forward_schedule`,
109+
`list_forward_schedules`, and `delete_forward_schedule` lets an end user create
110+
and cancel Schedules in natural language from a Web or IM Channel conversation.
111+
112+
`enabled_tools` replaces the provider's whole enabled set, so an empty array
113+
turns every managed tool off. Omitting the field entirely sends nothing: because
114+
Qoder Forward Template updates are merge-style, an undeclared field leaves
115+
whatever the remote Template already had. Declare it whenever the tools matter —
116+
a Template recreated from scratch (after a destroy, a manual deletion, or lost
117+
state) otherwise comes back with no managed tools and no error.
90118

91119
## Provider configuration
92120

@@ -253,6 +281,7 @@ agents:
253281
vault: <string>
254282
memory_stores: [ <string> ]
255283
environment_variables: { <key>: <string> } # Qoder only
284+
managed_tool_config: { enabled_tools: [ <string> ] } # Qoder Forward delivery only
256285
resources: [ SessionResource ]
257286
multiagent: { type: "coordinator", agents: [...] }
258287
metadata: { <key>: <string> }
@@ -274,6 +303,7 @@ agents:
274303
| `vault` | string | no | Vault name. |
275304
| `memory_stores` | string[] | no | Bound memory stores. |
276305
| `environment_variables` | map<string,string> | no | Qoder runtime variables. Managed Sessions use Qoder's `KEY=VALUE;...` wire format; Forward Templates store the map as defaults and Forward Sessions send it under `config.environment_variables`. |
306+
| `managed_tool_config.enabled_tools` | string[] | no | Provider-operated tools the Agent Harness exposes, e.g. `create_forward_schedule`, `list_forward_schedules`, `delete_forward_schedule`. Qoder Forward delivery only; declaring it on managed delivery is a validation error. |
277307
| `resources` | SessionResource[] | no | Resources attached to every managed Session created for the Agent. |
278308
| `multiagent.type` | `"coordinator"` | no | Declare a coordinator agent. |
279309
| `multiagent.agents` | string[] | yes (with multiagent) | Agents it orchestrates. |

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

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ export function collectReferenceDiagnostics(config: ProjectConfig, diagnostics:
113113
}
114114

115115
for (const [name, channel] of Object.entries(config.channels ?? {})) {
116-
if (!agentNames.has(channel.agent)) {
116+
if (channel.mode === "pairing") continue;
117+
if (!channel.agent) {
118+
diagnostics.error("config.channel.agent.required", `channel.${name}: fixed-mode channels require agent`);
119+
} else if (!agentNames.has(channel.agent)) {
117120
diagnostics.error("config.channel.agent.unknown", `channel.${name}: references unknown agent '${channel.agent}'`);
118121
}
119122
const identity = channel.identity ?? config.defaults?.identity;
@@ -205,29 +208,31 @@ export function collectProviderCapabilities(
205208
}
206209

207210
if (providerName === "qoder") {
208-
const agent = config.agents?.[channel.agent];
209-
if (agent?.provider && agent.provider !== providerName) {
210-
diagnostics.error(
211-
"config.channel.agent.provider_mismatch",
212-
`channel.${name}: agent '${channel.agent}' is pinned to provider '${agent.provider}'.`,
213-
{ type: "channel", name, provider: providerName },
214-
);
215-
}
216-
const identityName = channel.identity ?? config.defaults?.identity;
217-
const identity = identityName ? config.identities?.[identityName] : undefined;
218-
if (identity?.provider && identity.provider !== providerName) {
219-
diagnostics.error(
220-
"config.channel.identity.provider_mismatch",
221-
`channel.${name}: identity '${identityName}' is pinned to provider '${identity.provider}'.`,
222-
{ type: "channel", name, provider: providerName },
223-
);
224-
}
225-
if (agent && agent.delivery?.qoder?.type !== "forward") {
226-
diagnostics.error(
227-
"qoder.channel.forward_template.required",
228-
`channel.${name}: Qoder Channels require agent '${channel.agent}' to use delivery.qoder.type: forward.`,
229-
{ type: "channel", name, provider: providerName },
230-
);
211+
if (channel.mode !== "pairing" && channel.agent) {
212+
const agent = config.agents?.[channel.agent];
213+
if (agent?.provider && agent.provider !== providerName) {
214+
diagnostics.error(
215+
"config.channel.agent.provider_mismatch",
216+
`channel.${name}: agent '${channel.agent}' is pinned to provider '${agent.provider}'.`,
217+
{ type: "channel", name, provider: providerName },
218+
);
219+
}
220+
const identityName = channel.identity ?? config.defaults?.identity;
221+
const identity = identityName ? config.identities?.[identityName] : undefined;
222+
if (identity?.provider && identity.provider !== providerName) {
223+
diagnostics.error(
224+
"config.channel.identity.provider_mismatch",
225+
`channel.${name}: identity '${identityName}' is pinned to provider '${identity.provider}'.`,
226+
{ type: "channel", name, provider: providerName },
227+
);
228+
}
229+
if (agent && agent.delivery?.qoder?.type !== "forward") {
230+
diagnostics.error(
231+
"qoder.channel.forward_template.required",
232+
`channel.${name}: Qoder Channels require agent '${channel.agent}' to use delivery.qoder.type: forward.`,
233+
{ type: "channel", name, provider: providerName },
234+
);
235+
}
231236
}
232237
const requiredCredentials: Record<string, string[]> = {
233238
dingtalk: ["client_id", "client_secret"],
@@ -306,6 +311,13 @@ export function collectProviderCapabilities(
306311
address,
307312
);
308313
}
314+
if (delivery !== "forward" && agent.managed_tool_config) {
315+
diagnostics.error(
316+
`${providerName}.agent.managed_tool_config.forward_required`,
317+
`agent.${name}: managed_tool_config applies to Forward Templates; set delivery.${providerName}.type: forward or remove it.`,
318+
address,
319+
);
320+
}
309321
if (delivery === "forward" && !isSupported(caps, "template")) {
310322
diagnostics.error(
311323
`${providerName}.agent.delivery.forward.unsupported`,
@@ -463,6 +475,13 @@ export function collectProviderCapabilities(
463475
{ type: "agent", name, provider: providerName },
464476
);
465477
}
478+
if (agent.managed_tool_config && (!agent.provider || agent.provider === providerName)) {
479+
diagnostics.error(
480+
`${providerName}.agent.managed_tool_config.unsupported`,
481+
`agent.${name}: managed_tool_config is supported only by Qoder; remove it or pin this agent to qoder.`,
482+
{ type: "agent", name, provider: providerName },
483+
);
484+
}
466485
if (agent.tunnel && (!agent.provider || agent.provider === providerName)) {
467486
diagnostics.error(
468487
`${providerName}.agent.tunnel.unsupported`,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ export function resolveChannelRefs(
179179
const channel = config.channels?.[channelName];
180180
if (!channel) throw new UserError(`Channel '${channelName}' not found in config`);
181181

182+
if (channel.mode === "pairing") {
183+
return {};
184+
}
185+
186+
if (!channel.agent) {
187+
throw new UserError(`Channel '${channelName}' is fixed mode and must declare agent`);
188+
}
182189
const agent = config.agents?.[channel.agent];
183190
if (!agent) throw new UserError(`Channel '${channelName}' references unknown agent '${channel.agent}'`);
184191
const agentType = agent.delivery?.[provider]?.type === "forward" ? "template" : "agent";

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export function buildDependencyGraph(config: ProjectConfig, targetProviders: str
190190
const channelAddr: ResourceAddress = { type: "channel", name, provider };
191191
addNode(channelAddr);
192192

193+
if (decl.mode === "pairing" || !decl.agent) continue;
194+
193195
const agentDecl = config.agents?.[decl.agent];
194196
const agentType = agentDecl ? resolveAgentMaterialization(provider, agentDecl).resourceType : "agent";
195197
const agentAddr: ResourceAddress = { type: agentType, name: decl.agent, provider };

packages/sdk/src/internal/parser/schema.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ const agentDeliverySchema = z.object({
231231
type: z.enum(["managed", "forward"]),
232232
});
233233

234+
const managedToolConfigSchema = z.object({
235+
enabled_tools: z.array(z.string().min(1)),
236+
});
237+
234238
const sessionGithubRepoResourceSchema = z.object({
235239
type: z.literal("github_repository"),
236240
url: z.string().url(),
@@ -261,15 +265,17 @@ const agentSchema = z.object({
261265
multiagent: multiagentSchema.optional(),
262266
metadata: z.record(z.string(), z.string()).optional(),
263267
environment_variables: z.record(z.string().min(1), z.string()).optional(),
268+
managed_tool_config: managedToolConfigSchema.optional(),
264269
delivery: z.record(z.string(), agentDeliverySchema).optional(),
265270
});
266271

267272
const channelSchema = z.object({
268273
provider: z.string().optional(),
269-
agent: z.string().min(1),
274+
agent: z.string().min(1).optional(),
270275
identity: z.string().min(1).optional(),
271276
type: z.string().min(1),
272277
name: z.string().trim().min(1).optional(),
278+
mode: z.enum(["fixed", "pairing"]).optional().default("fixed"),
273279
enabled: z.boolean().optional(),
274280
credentials: z.record(z.string(), coerceString).optional(),
275281
options: z.record(z.string(), z.unknown()).optional(),

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,18 @@ export function computeReplacementFingerprint(address: ResourceAddress, config:
6363
if (address.type !== "channel") return undefined;
6464
const decl = config.channels?.[address.name];
6565
if (!decl) return undefined;
66-
return contentHash({ channel_type: decl.type, credentials: decl.credentials ?? {} });
66+
return contentHash({ channel_type: decl.type, mode: decl.mode ?? "fixed", credentials: decl.credentials ?? {} });
6767
}
6868

6969
function resolveChannelReferenceIds(
70-
decl: { agent: string; identity?: string },
70+
decl: { agent?: string; identity?: string; mode?: "fixed" | "pairing" },
7171
config: ProjectConfig,
7272
provider: string,
7373
state?: HashStateLookup,
7474
): Record<string, string | null | undefined> {
75+
if (decl.mode === "pairing" || !decl.agent) {
76+
return { mode: "pairing" };
77+
}
7578
const agent = config.agents?.[decl.agent];
7679
const agentType = agent?.delivery?.[provider]?.type === "forward" ? "template" : "agent";
7780
const identity = decl.identity ?? config.defaults?.identity;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export interface ResolvedDeploymentRefs {
9797
}
9898

9999
export interface ResolvedChannelRefs {
100-
identity_id: string;
101-
agent_id: string;
100+
identity_id?: string;
101+
agent_id?: string;
102102
}
103103

104104
export interface DeploymentContext {

packages/sdk/src/internal/providers/qoder/adapter.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -354,16 +354,21 @@ export class QoderAdapter implements ProviderAdapter {
354354
}
355355
if (type === "channel") {
356356
const channelConfig = (raw.channel_config ?? {}) as Record<string, unknown>;
357-
return compactDeep({
358-
identity_id: raw.identity_id,
359-
template_id: raw.template_id,
357+
const mode = (raw.identity_resolution as { mode?: string } | undefined)?.mode ?? "fixed";
358+
const normalized: Record<string, unknown> = {
359+
identity_resolution: { mode },
360360
channel_type: raw.channel_type,
361361
name: raw.name,
362362
enabled: raw.enabled,
363363
channel_config: {
364364
response_options: channelConfig.response_options ?? {},
365365
},
366-
});
366+
};
367+
if (mode === "fixed") {
368+
normalized.identity_id = raw.identity_id;
369+
normalized.template_id = raw.template_id;
370+
}
371+
return compactDeep(normalized);
367372
}
368373

369374
return compactDeep({
@@ -564,12 +569,14 @@ export class QoderAdapter implements ProviderAdapter {
564569

565570
async updateChannel(id: string, name: string, decl: ChannelDecl, refs: ResolvedChannelRefs): Promise<RemoteResource> {
566571
const current = (await this.forwardClient.get(`/channels/${id}`)) as Record<string, unknown>;
567-
if (current.channel_type !== decl.type) {
572+
const currentMode = (current.identity_resolution as { mode?: string } | undefined)?.mode ?? "fixed";
573+
if (current.channel_type !== decl.type || currentMode !== (decl.mode ?? "fixed")) {
568574
await this.deleteChannel(id);
569575
return this.createChannel(name, decl, refs);
570576
}
571577
const body = this.mapChannel(name, decl, refs);
572578
delete body.channel_type;
579+
delete body.identity_resolution;
573580
const res = (await this.forwardClient.post(`/channels/${id}`, body)) as Record<string, unknown>;
574581
return toRemoteResource(res);
575582
}
@@ -579,9 +586,8 @@ export class QoderAdapter implements ProviderAdapter {
579586
}
580587

581588
private mapChannel(name: string, decl: ChannelDecl, refs: ResolvedChannelRefs): Record<string, unknown> {
582-
return {
583-
identity_id: refs.identity_id,
584-
template_id: refs.agent_id,
589+
const mode = decl.mode ?? "fixed";
590+
const body: Record<string, unknown> = {
585591
channel_type: decl.type,
586592
name: decl.name ?? name,
587593
enabled: decl.enabled ?? true,
@@ -594,6 +600,13 @@ export class QoderAdapter implements ProviderAdapter {
594600
},
595601
},
596602
};
603+
if (mode === "pairing") {
604+
body.identity_resolution = { mode: "pairing" };
605+
} else {
606+
body.identity_id = refs.identity_id;
607+
body.template_id = refs.agent_id;
608+
}
609+
return body;
597610
}
598611

599612
private async registerForwardVaults(vaultIds: string[]): Promise<void> {

packages/sdk/src/internal/providers/qoder/mapper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,9 @@ export function mapForwardTemplate(
493493
if (projectName) body.metadata = injectMetadata(decl.metadata, projectName, name);
494494
else body.metadata = decl.metadata ?? {};
495495
if (decl.environment_variables) body.environment_variables = decl.environment_variables;
496+
// Sent on create and on update: Forward updates are merge-style, so omitting
497+
// the field would silently keep whatever the Template already had.
498+
if (decl.managed_tool_config) body.managed_tool_config = decl.managed_tool_config;
496499

497500
if (decl.tools) {
498501
body.tools = [

0 commit comments

Comments
 (0)