@hanphone/dsh-a2a 0.3.0 → 0.3.1

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.
@@ -15,7 +15,7 @@ import {
15
15
  type DomainFacility,
16
16
  type KvTable,
17
17
  } from '@deepseek-ai/dsh-storage-domain'
18
- import { TaskState, type AgentSkill, type Part } from '../protocol.ts'
18
+ import { TaskState, type Part } from '../protocol.ts'
19
19
 
20
20
  /** One durable inbound/outbound task record. */
21
21
  export interface TaskRecord {
@@ -59,11 +59,9 @@ export interface InboundServerRecord {
59
59
  readonly description: string
60
60
  readonly version: string
61
61
  readonly endpointPath: string
62
- /** Agent preset id composing inbound sessions; absent = deployment default. */
62
+ /** Agent preset id composing inbound sessions; absent falls back to the deployment default. */
63
63
  readonly preset?: string
64
64
  readonly authTokenEnv?: string
65
- /** Advertised skills (creator-entered declarations, default = preset name). */
66
- readonly skills: readonly AgentSkill[]
67
65
  readonly enabled: boolean
68
66
  }
69
67
 
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Inbound server manager: owns every inbound A2A server instance. Each
3
- * instance binds one agent preset (its session pool is composed from that
4
- * preset), advertises creator-declared skills, and serves its own endpoint and
5
- * AgentCard route. Instances are persisted in the `inbound_servers` domain
6
- * table and assembled on boot; the manager owns route registration and the
7
- * full lifecycle (add/enable/disable/update/remove) driven from the GUI and
8
- * facade, and disposes every instance's environment on teardown.
3
+ * instance binds one concrete agent preset (its session pool is composed from
4
+ * that preset) and serves its own endpoint and AgentCard route. Instances are
5
+ * persisted in the `inbound_servers` domain table and assembled on boot; the
6
+ * manager owns route registration and the full lifecycle
7
+ * (add/enable/disable/update/remove) driven from the GUI and facade, and
8
+ * disposes every instance's environment on teardown.
9
9
  *
10
- * Skill declaration: the stored `skills` list is exactly what the creator
11
- * declared. When a creator leaves it empty at creation time, the default skill
12
- * derives from the bound preset's display name (else the built-in `chat`
13
- * skill), per the v1.0 design — the v0.2 tool white-list derivation is gone.
10
+ * Skill declaration is DERIVED, never typed: "the preset decides its skills;
11
+ * everything is a plugin." An instance's AgentCard skills are the
12
+ * model-invocable entries of its preset's skill directory the preset's
13
+ * standing scope key (`agentPresets.standingKeyFor`) plus
14
+ * `ctx.skills.list({ scope })` — resolved automatically at add/update/boot
15
+ * and cached on the live instance; the creator types no skill text. A missing
16
+ * skills service or standing mount falls back to a built-in `chat` skill so
17
+ * minimal compositions stay exercisable.
14
18
  * @module dsh-a2a/servers/inbound-manager
15
19
  */
16
20
 
@@ -18,7 +22,7 @@ import type { Context } from '@deepseek-ai/cordis'
18
22
  import { buildCard } from '../server/card.ts'
19
23
  import { A2AServer } from '../server/a2a-server.ts'
20
24
  import { A2aRoutes } from '../server/routes.ts'
21
- import { ContextSessionPool, type AgentPresetsLike, type AgentRegistryLike } from '../server/exec/agent-runtime.ts'
25
+ import { ContextSessionPool, type AgentPresetsLike, type AgentRegistryLike, type SkillsLike } from '../server/exec/agent-runtime.ts'
22
26
  import { createSessionExecutor } from '../server/exec/session.ts'
23
27
  import { createSubagentExecutor, type SubagentsLike } from '../server/exec/subagent.ts'
24
28
  import { ExecutorSet } from '../server/executor.ts'
@@ -45,10 +49,13 @@ export interface InboundManagerHost {
45
49
  readonly logger: { info(message: string): void; warn(message: string): void; error(message: string): void }
46
50
  readonly agents?: AgentRegistryLike
47
51
  readonly agentPresets?: AgentPresetsLike
52
+ readonly skills?: SkillsLike
48
53
  readonly subagents?: SubagentsLike
49
54
  readonly sessionCwd: string
50
55
  readonly defaultBaseUrl: string
51
56
  readonly subagentProvider: string
57
+ /** Deployment default preset id (from `agentPresets.defaultId`), when known. */
58
+ readonly defaultPresetId?: string
52
59
  /** Resolve the deployment's default model options (may be undefined). */
53
60
  readonly resolveDefaultModel?: () => { readonly provider?: string; readonly model?: string } | undefined
54
61
  /** Allocate a fresh stable instance id. */
@@ -59,6 +66,10 @@ export interface InboundManagerHost {
59
66
  export interface LiveInboundServer {
60
67
  readonly id: string
61
68
  readonly record: InboundServerRecord
69
+ /** Effective preset id (record's or the deployment default). */
70
+ readonly preset: string | undefined
71
+ /** Derived skill declarations (cached at assemble time). */
72
+ readonly skills: readonly AgentSkill[]
62
73
  readonly card: ReturnType<typeof buildCard>
63
74
  readonly server: A2AServer
64
75
  readonly routes: A2aRoutes
@@ -75,10 +86,9 @@ export interface InboundCreateInput {
75
86
  readonly description: string
76
87
  readonly version: string
77
88
  readonly endpointPath?: string
89
+ /** Preset id; absent = the deployment default preset. */
78
90
  readonly preset?: string
79
91
  readonly authTokenEnv?: string
80
- /** Declared skills; empty defaults to the preset name (or `chat`). */
81
- readonly skills?: readonly AgentSkill[]
82
92
  readonly enabled?: boolean
83
93
  }
84
94
 
@@ -96,23 +106,52 @@ function refuseExecutor(reason: string) {
96
106
  }
97
107
  }
98
108
 
99
- /** Default skkill when a creator declares none: the preset display name. */
100
- export function defaultSkillFor(
101
- skills: readonly AgentSkill[] | undefined,
102
- presetName: string | undefined,
103
- ): readonly AgentSkill[] {
104
- if (skills !== undefined && skills.length > 0) return skills
105
- if (presetName !== undefined && presetName.length > 0) {
106
- return [{ id: 'chat', name: presetName, description: `Compose inbound sessions from the "${presetName}" agent preset.`, tags: ['preset'] }]
107
- }
109
+ /** Built-in fallback skill when no skills service / standing mount exists. */
110
+ export function chatFallbackSkills(): readonly AgentSkill[] {
108
111
  return [{ id: 'chat', name: 'chat', description: 'Conversational assistance over a DSH agent session.', tags: ['chat'] }]
109
112
  }
110
113
 
111
- /** Assemble one live instance from a persisted record. */
112
- function assemble(record: InboundServerRecord, host: InboundManagerHost): LiveInboundServer {
113
- const skills: readonly AgentSkill[] = record.skills
114
+ /**
115
+ * Derive an instance's AgentCard skills from its preset's skill directory.
116
+ *
117
+ * `agentPresets.standingKeyFor(preset)` yields the preset's standing scope
118
+ * key (no agent required); `ctx.skills.list({ scope })` returns the catalogue
119
+ * that preset agent actually sees (its layer + the deployment global). Only
120
+ * model-invocable entries are advertised, mapped to `AgentSkill` (id = name,
121
+ * name = name). Any missing service, failed mount, or empty catalogue falls
122
+ * back to the built-in `chat` skill.
123
+ *
124
+ * @param agentPresets - the preset roster, or undefined.
125
+ * @param skills - the skill registry, or undefined.
126
+ * @param preset - the instance's preset id; undefined = deployment default.
127
+ */
128
+ export async function derivePresetSkills(
129
+ agentPresets: AgentPresetsLike | undefined,
130
+ skills: SkillsLike | undefined,
131
+ preset: string | undefined,
132
+ ): Promise<readonly AgentSkill[]> {
133
+ if (agentPresets?.standingKeyFor === undefined || skills === undefined) return chatFallbackSkills()
134
+ try {
135
+ const scope = await agentPresets.standingKeyFor(preset)
136
+ const rows = await skills.list({ scope })
137
+ const advertised: AgentSkill[] = rows
138
+ .filter((row) => row.invocation?.modelInvocable !== false)
139
+ .map((row) => ({
140
+ id: row.name,
141
+ name: row.name,
142
+ ...(row.description !== undefined && row.description.length > 0 ? { description: row.description } : {}),
143
+ }))
144
+ return advertised.length > 0 ? advertised : chatFallbackSkills()
145
+ } catch {
146
+ return chatFallbackSkills()
147
+ }
148
+ }
149
+
150
+ /** Assemble one live instance (skills already derived from the preset). */
151
+ function assemble(record: InboundServerRecord, skills: readonly AgentSkill[], host: InboundManagerHost): LiveInboundServer {
114
152
  const endpointPath = record.endpointPath
115
153
  const cardPath = `${endpointPath.replace(/\/$/, '')}/agent-card.json`
154
+ const preset = record.preset ?? host.defaultPresetId
116
155
  const card = buildCard({
117
156
  baseUrl: host.defaultBaseUrl,
118
157
  endpointPath,
@@ -130,7 +169,7 @@ function assemble(record: InboundServerRecord, host: InboundManagerHost): LiveIn
130
169
  ? new ContextSessionPool(host.agents, {
131
170
  cwd: host.sessionCwd,
132
171
  ...(host.agentPresets !== undefined ? { agentPresets: host.agentPresets } : {}),
133
- ...(record.preset !== undefined ? { presetId: () => record.preset } : {}),
172
+ ...(preset !== undefined ? { presetId: () => preset } : {}),
134
173
  // `?.()` yields `{ provider?, model? } | undefined`, matching the
135
174
  // AgentRuntimeOptions.resolveAgentOptions signature under
136
175
  // exactOptionalPropertyTypes.
@@ -184,6 +223,8 @@ function assemble(record: InboundServerRecord, host: InboundManagerHost): LiveIn
184
223
  return {
185
224
  id: record.id,
186
225
  record,
226
+ preset,
227
+ skills,
187
228
  card,
188
229
  server,
189
230
  routes,
@@ -242,7 +283,7 @@ export class DomainInboundStore {
242
283
  function safeParse(raw: string): InboundServerRecord | undefined {
243
284
  try {
244
285
  const parsed = JSON.parse(raw) as InboundServerRecord
245
- if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string' || !Array.isArray(parsed.skills)) return undefined
286
+ if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string') return undefined
246
287
  return parsed
247
288
  } catch {
248
289
  return undefined
@@ -258,11 +299,12 @@ export class InboundServerManager {
258
299
  private readonly store: DomainInboundStore,
259
300
  ) {}
260
301
 
261
- /** Assemble every persisted enabled instance (boot). */
262
- boot(): void {
302
+ /** Assemble every persisted instance (skills derived per preset). */
303
+ async boot(): Promise<void> {
263
304
  for (const record of this.store.list()) {
264
305
  try {
265
- const live = assemble(record, this.host)
306
+ const skills = await derivePresetSkills(this.host.agentPresets, this.host.skills, record.preset ?? this.host.defaultPresetId)
307
+ const live = assemble(record, skills, this.host)
266
308
  this.live.set(record.id, live)
267
309
  if (record.enabled) live.routes.enable()
268
310
  } catch (err) {
@@ -279,35 +321,24 @@ export class InboundServerManager {
279
321
  return this.live.get(id)
280
322
  }
281
323
 
282
- /** Resolve the display name of a preset id (undefined when unnamed/absent). */
283
- private async presetDisplayName(preset: string | undefined): Promise<string | undefined> {
284
- if (preset === undefined || this.host.agentPresets === undefined) return undefined
285
- try {
286
- const resolved = await this.host.agentPresets.resolve(preset)
287
- return resolved.name ?? resolved.id
288
- } catch {
289
- return undefined
290
- }
291
- }
292
-
293
324
  /** Create, persist, and assemble a new inbound instance. */
294
325
  async add(input: InboundCreateInput): Promise<OpResult & { readonly id?: string }> {
295
326
  const id = this.newId(input.name)
296
327
  if (this.live.has(id)) return { ok: false, message: `inbound server ${id} already exists` }
297
- const presetName = await this.presetDisplayName(input.preset)
328
+ const preset = input.preset ?? this.host.defaultPresetId
298
329
  const record: InboundServerRecord = {
299
330
  id,
300
331
  name: input.name,
301
332
  description: input.description,
302
333
  version: input.version,
303
334
  endpointPath: input.endpointPath ?? `/a2a/${id}`,
304
- ...(input.preset !== undefined ? { preset: input.preset } : {}),
335
+ ...(preset !== undefined ? { preset } : {}),
305
336
  ...(input.authTokenEnv !== undefined ? { authTokenEnv: input.authTokenEnv } : {}),
306
- skills: [...defaultSkillFor(input.skills, presetName)],
307
337
  enabled: input.enabled ?? true,
308
338
  }
309
339
  try {
310
- const live = assemble(record, this.host)
340
+ const skills = await derivePresetSkills(this.host.agentPresets, this.host.skills, preset)
341
+ const live = assemble(record, skills, this.host)
311
342
  await this.store.save(record)
312
343
  this.live.set(id, live)
313
344
  if (record.enabled) live.routes.enable()
@@ -317,8 +348,8 @@ export class InboundServerManager {
317
348
  }
318
349
  }
319
350
 
320
- /** Persist, rebuild, and apply a patch onto a live instance. */
321
- async update(id: string, patch: Partial<Pick<InboundServerRecord, 'name' | 'description' | 'version' | 'endpointPath' | 'preset' | 'authTokenEnv' | 'skills'>>): Promise<OpResult> {
351
+ /** Persist, rebuild (re-deriving skills), and apply a patch onto a live instance. */
352
+ async update(id: string, patch: Partial<Pick<InboundServerRecord, 'name' | 'description' | 'version' | 'endpointPath' | 'preset' | 'authTokenEnv'>>): Promise<OpResult> {
322
353
  const live = this.live.get(id)
323
354
  const stored = this.store.get(id)
324
355
  if (live === undefined || stored === undefined) return { ok: false, message: `inbound server ${id} not found` }
@@ -330,12 +361,12 @@ export class InboundServerManager {
330
361
  endpointPath: patch.endpointPath ?? stored.endpointPath,
331
362
  ...(patch.preset !== undefined ? { preset: patch.preset } : {}),
332
363
  ...(patch.authTokenEnv !== undefined ? { authTokenEnv: patch.authTokenEnv } : {}),
333
- skills: patch.skills ?? stored.skills,
334
364
  enabled: stored.enabled,
335
365
  }
336
366
  try {
337
367
  await this.store.save(next)
338
- const replaced = assemble(next, this.host)
368
+ const skills = await derivePresetSkills(this.host.agentPresets, this.host.skills, next.preset ?? this.host.defaultPresetId)
369
+ const replaced = assemble(next, skills, this.host)
339
370
  // Preserve the route registration state while swapping runtime halves.
340
371
  const wasEnabled = live.routes.active
341
372
  live.dispose()
@@ -382,4 +413,4 @@ export class InboundServerManager {
382
413
  }
383
414
  }
384
415
 
385
- export type { AgentSkill }
416
+ export type { AgentSkill }
package/src/service.ts CHANGED
@@ -64,9 +64,9 @@ export interface InboundCreateInput {
64
64
  readonly description: string
65
65
  readonly version: string
66
66
  readonly endpointPath?: string
67
+ /** Preset id; absent = the deployment default. Skills derive from it. */
67
68
  readonly preset?: string
68
69
  readonly authTokenEnv?: string
69
- readonly skills?: readonly SkillView[]
70
70
  readonly enabled?: boolean
71
71
  }
72
72
 
@@ -91,7 +91,7 @@ export interface A2AServiceImpl {
91
91
  createInboundServer(input: InboundCreateInput): Promise<OpResult>
92
92
  removeInboundServer(id: string): Promise<OpResult>
93
93
  setInboundServerEnabled(id: string, enabled: boolean): Promise<OpResult>
94
- updateInboundServer(id: string, patch: { name?: string; description?: string; version?: string; endpointPath?: string; preset?: string; authTokenEnv?: string; skills?: readonly SkillView[] }): Promise<OpResult>
94
+ updateInboundServer(id: string, patch: { name?: string; description?: string; version?: string; endpointPath?: string; preset?: string; authTokenEnv?: string }): Promise<OpResult>
95
95
  // ── outbound server instances ───────────────────────────────────────
96
96
  listOutboundServers(): OutboundServerView[]
97
97
  createOutboundServer(input: OutboundCreateInput): Promise<OpResult>
@@ -140,7 +140,7 @@ export class A2AService extends Service {
140
140
  return this.impl.setInboundServerEnabled(id, enabled)
141
141
  }
142
142
 
143
- async updateInboundServer(id: string, patch: { name?: string; description?: string; version?: string; endpointPath?: string; preset?: string; authTokenEnv?: string; skills?: readonly SkillView[] }): Promise<OpResult> {
143
+ async updateInboundServer(id: string, patch: { name?: string; description?: string; version?: string; endpointPath?: string; preset?: string; authTokenEnv?: string }): Promise<OpResult> {
144
144
  return this.impl.updateInboundServer(id, patch)
145
145
  }
146
146