@clawcipes/recipes 0.1.2 → 0.1.4
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/README.md +2 -0
- package/index.ts +295 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<img src="./clawcipes_cook.jpg" alt="Clawcipes logo" width="240" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
> **Experimental:** We’re in active development. Installing should not have any negative impacts, but it’s always good to be safe and copy your `~/.openclaw` folder to a backup.
|
|
8
|
+
|
|
7
9
|
Clawcipes is an OpenClaw plugin that provides **CLI-first recipes** for scaffolding specialist agents and teams from Markdown.
|
|
8
10
|
|
|
9
11
|
If you like durable workflows: Clawcipes is built around a **file-first team workspace** (inbox/backlog/in-progress/done) that plays nicely with git.
|
package/index.ts
CHANGED
|
@@ -180,6 +180,20 @@ type AgentConfigSnippet = {
|
|
|
180
180
|
tools?: { profile?: string; allow?: string[]; deny?: string[] };
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
type BindingMatch = {
|
|
184
|
+
channel: string;
|
|
185
|
+
accountId?: string;
|
|
186
|
+
// OpenClaw config schema uses: dm | group | channel
|
|
187
|
+
peer?: { kind: "dm" | "group" | "channel"; id: string };
|
|
188
|
+
guildId?: string;
|
|
189
|
+
teamId?: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type BindingSnippet = {
|
|
193
|
+
agentId: string;
|
|
194
|
+
match: BindingMatch;
|
|
195
|
+
};
|
|
196
|
+
|
|
183
197
|
function upsertAgentInConfig(cfgObj: any, snippet: AgentConfigSnippet) {
|
|
184
198
|
if (!cfgObj.agents) cfgObj.agents = {};
|
|
185
199
|
if (!Array.isArray(cfgObj.agents.list)) cfgObj.agents.list = [];
|
|
@@ -242,6 +256,43 @@ function ensureMainFirstInAgentsList(cfgObj: any, api: OpenClawPluginApi) {
|
|
|
242
256
|
list.unshift(main);
|
|
243
257
|
}
|
|
244
258
|
|
|
259
|
+
function stableStringify(x: any) {
|
|
260
|
+
const seen = new WeakSet();
|
|
261
|
+
const sortObj = (v: any): any => {
|
|
262
|
+
if (v && typeof v === "object") {
|
|
263
|
+
if (seen.has(v)) return "[Circular]";
|
|
264
|
+
seen.add(v);
|
|
265
|
+
if (Array.isArray(v)) return v.map(sortObj);
|
|
266
|
+
const out: any = {};
|
|
267
|
+
for (const k of Object.keys(v).sort()) out[k] = sortObj(v[k]);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
return v;
|
|
271
|
+
};
|
|
272
|
+
return JSON.stringify(sortObj(x));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function upsertBindingInConfig(cfgObj: any, binding: BindingSnippet) {
|
|
276
|
+
if (!Array.isArray(cfgObj.bindings)) cfgObj.bindings = [];
|
|
277
|
+
const list: any[] = cfgObj.bindings;
|
|
278
|
+
|
|
279
|
+
const sig = stableStringify({ agentId: binding.agentId, match: binding.match });
|
|
280
|
+
const idx = list.findIndex((b) => stableStringify({ agentId: b?.agentId, match: b?.match }) === sig);
|
|
281
|
+
|
|
282
|
+
if (idx >= 0) {
|
|
283
|
+
// Update in place (preserve ordering)
|
|
284
|
+
list[idx] = { ...list[idx], ...binding };
|
|
285
|
+
return { changed: false, note: "already-present" as const };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Most-specific-first: if a peer match is specified, insert at front so it wins.
|
|
289
|
+
// Otherwise append.
|
|
290
|
+
if (binding.match?.peer) list.unshift(binding);
|
|
291
|
+
else list.push(binding);
|
|
292
|
+
|
|
293
|
+
return { changed: true, note: "added" as const };
|
|
294
|
+
}
|
|
295
|
+
|
|
245
296
|
async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: AgentConfigSnippet[]) {
|
|
246
297
|
// Load the latest config from disk (not the snapshot in api.config).
|
|
247
298
|
const current = (api.runtime as any).config?.loadConfig?.();
|
|
@@ -262,16 +313,37 @@ async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippe
|
|
|
262
313
|
return { updatedAgents: snippets.map((s) => s.id) };
|
|
263
314
|
}
|
|
264
315
|
|
|
265
|
-
async function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
vars?: Record<string, string>;
|
|
270
|
-
}) {
|
|
271
|
-
const cfg = getCfg(api);
|
|
316
|
+
async function applyBindingSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: BindingSnippet[]) {
|
|
317
|
+
const current = (api.runtime as any).config?.loadConfig?.();
|
|
318
|
+
if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
|
|
319
|
+
const cfgObj = (current.cfg ?? current) as any;
|
|
272
320
|
|
|
273
|
-
const
|
|
274
|
-
|
|
321
|
+
const results: any[] = [];
|
|
322
|
+
for (const s of snippets) {
|
|
323
|
+
results.push({ ...s, result: upsertBindingInConfig(cfgObj, s) });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await (api.runtime as any).config?.writeConfigFile?.(cfgObj);
|
|
327
|
+
return { updatedBindings: results };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function scaffoldAgentFromRecipe(
|
|
331
|
+
api: OpenClawPluginApi,
|
|
332
|
+
recipe: RecipeFrontmatter,
|
|
333
|
+
opts: {
|
|
334
|
+
agentId: string;
|
|
335
|
+
agentName?: string;
|
|
336
|
+
update?: boolean;
|
|
337
|
+
vars?: Record<string, string>;
|
|
338
|
+
|
|
339
|
+
// Where to write the scaffolded files (may be a shared team workspace role folder)
|
|
340
|
+
filesRootDir: string;
|
|
341
|
+
|
|
342
|
+
// What to set in agents.list[].workspace (may be shared team workspace root)
|
|
343
|
+
workspaceRootDir: string;
|
|
344
|
+
},
|
|
345
|
+
) {
|
|
346
|
+
await ensureDir(opts.filesRootDir);
|
|
275
347
|
|
|
276
348
|
const templates = recipe.templates ?? {};
|
|
277
349
|
const files = recipe.files ?? [];
|
|
@@ -282,7 +354,7 @@ async function scaffoldAgentFromRecipe(api: OpenClawPluginApi, recipe: RecipeFro
|
|
|
282
354
|
const raw = templates[f.template];
|
|
283
355
|
if (typeof raw !== "string") throw new Error(`Missing template: ${f.template}`);
|
|
284
356
|
const rendered = renderTemplate(raw, vars);
|
|
285
|
-
const target = path.join(
|
|
357
|
+
const target = path.join(opts.filesRootDir, f.path);
|
|
286
358
|
const mode = opts.update ? (f.mode ?? "overwrite") : (f.mode ?? "createOnly");
|
|
287
359
|
const r = await writeFileSafely(target, rendered, mode);
|
|
288
360
|
fileResults.push({ path: target, wrote: r.wrote, reason: r.reason });
|
|
@@ -290,13 +362,14 @@ async function scaffoldAgentFromRecipe(api: OpenClawPluginApi, recipe: RecipeFro
|
|
|
290
362
|
|
|
291
363
|
const configSnippet: AgentConfigSnippet = {
|
|
292
364
|
id: opts.agentId,
|
|
293
|
-
workspace:
|
|
365
|
+
workspace: opts.workspaceRootDir,
|
|
294
366
|
identity: { name: opts.agentName ?? recipe.name ?? opts.agentId },
|
|
295
367
|
tools: recipe.tools ?? {},
|
|
296
368
|
};
|
|
297
369
|
|
|
298
370
|
return {
|
|
299
|
-
|
|
371
|
+
filesRootDir: opts.filesRootDir,
|
|
372
|
+
workspaceRootDir: opts.workspaceRootDir,
|
|
300
373
|
fileResults,
|
|
301
374
|
next: {
|
|
302
375
|
configSnippet,
|
|
@@ -393,6 +466,194 @@ const recipesPlugin = {
|
|
|
393
466
|
console.log(JSON.stringify(out, null, 2));
|
|
394
467
|
});
|
|
395
468
|
|
|
469
|
+
cmd
|
|
470
|
+
.command("bind")
|
|
471
|
+
.description("Add/update a multi-agent routing binding (writes openclaw.json bindings[])")
|
|
472
|
+
.requiredOption("--agent-id <agentId>", "Target agent id")
|
|
473
|
+
.requiredOption("--channel <channel>", "Channel name (telegram|whatsapp|discord|slack|...) ")
|
|
474
|
+
.option("--account-id <accountId>", "Channel accountId (if applicable)")
|
|
475
|
+
.option("--peer-kind <kind>", "Peer kind (dm|group|channel) (aliases: direct->dm)")
|
|
476
|
+
.option("--peer-id <id>", "Peer id (DM number/id, group id, or channel id)")
|
|
477
|
+
.option("--guild-id <guildId>", "Discord guildId")
|
|
478
|
+
.option("--team-id <teamId>", "Slack teamId")
|
|
479
|
+
.option("--match <json>", "Full match object as JSON/JSON5 (overrides flags)")
|
|
480
|
+
.action(async (options: any) => {
|
|
481
|
+
const agentId = String(options.agentId);
|
|
482
|
+
let match: BindingMatch;
|
|
483
|
+
|
|
484
|
+
if (options.match) {
|
|
485
|
+
match = JSON5.parse(String(options.match)) as BindingMatch;
|
|
486
|
+
} else {
|
|
487
|
+
match = {
|
|
488
|
+
channel: String(options.channel),
|
|
489
|
+
};
|
|
490
|
+
if (options.accountId) match.accountId = String(options.accountId);
|
|
491
|
+
if (options.guildId) match.guildId = String(options.guildId);
|
|
492
|
+
if (options.teamId) match.teamId = String(options.teamId);
|
|
493
|
+
|
|
494
|
+
if (options.peerKind || options.peerId) {
|
|
495
|
+
if (!options.peerKind || !options.peerId) {
|
|
496
|
+
throw new Error("--peer-kind and --peer-id must be provided together");
|
|
497
|
+
}
|
|
498
|
+
let kind = String(options.peerKind);
|
|
499
|
+
// Back-compat alias
|
|
500
|
+
if (kind === "direct") kind = "dm";
|
|
501
|
+
if (kind !== "dm" && kind !== "group" && kind !== "channel") {
|
|
502
|
+
throw new Error("--peer-kind must be dm|group|channel (or direct as alias for dm)");
|
|
503
|
+
}
|
|
504
|
+
match.peer = { kind, id: String(options.peerId) };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!match?.channel) throw new Error("match.channel is required");
|
|
509
|
+
|
|
510
|
+
const res = await applyBindingSnippetsToOpenClawConfig(api, [{ agentId, match }]);
|
|
511
|
+
console.log(JSON.stringify(res, null, 2));
|
|
512
|
+
console.error("Binding written. Restart gateway if required for changes to take effect.");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
cmd
|
|
516
|
+
.command("bindings")
|
|
517
|
+
.description("Show current bindings from openclaw config")
|
|
518
|
+
.action(async () => {
|
|
519
|
+
const current = (api.runtime as any).config?.loadConfig?.();
|
|
520
|
+
if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
|
|
521
|
+
const cfgObj = (current.cfg ?? current) as any;
|
|
522
|
+
console.log(JSON.stringify(cfgObj.bindings ?? [], null, 2));
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
cmd
|
|
526
|
+
.command("migrate-team")
|
|
527
|
+
.description("Migrate a legacy team scaffold into the new workspace-<teamId> layout")
|
|
528
|
+
.requiredOption("--team-id <teamId>", "Team id (must end with -team)")
|
|
529
|
+
.option("--mode <mode>", "move|copy", "move")
|
|
530
|
+
.option("--dry-run", "Print the plan without writing anything", false)
|
|
531
|
+
.option("--overwrite", "Allow merging into an existing destination (dangerous)", false)
|
|
532
|
+
.action(async (options: any) => {
|
|
533
|
+
const teamId = String(options.teamId);
|
|
534
|
+
if (!teamId.endsWith("-team")) throw new Error("teamId must end with -team");
|
|
535
|
+
|
|
536
|
+
const mode = String(options.mode ?? "move");
|
|
537
|
+
if (mode !== "move" && mode !== "copy") throw new Error("--mode must be move|copy");
|
|
538
|
+
|
|
539
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
540
|
+
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
541
|
+
|
|
542
|
+
const legacyTeamDir = path.resolve(baseWorkspace, "teams", teamId);
|
|
543
|
+
const legacyAgentsDir = path.resolve(baseWorkspace, "agents");
|
|
544
|
+
|
|
545
|
+
const destTeamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
|
|
546
|
+
const destRolesDir = path.join(destTeamDir, "roles");
|
|
547
|
+
|
|
548
|
+
const exists = async (p: string) => fileExists(p);
|
|
549
|
+
|
|
550
|
+
// Build migration plan
|
|
551
|
+
const plan: any = {
|
|
552
|
+
teamId,
|
|
553
|
+
mode,
|
|
554
|
+
legacy: { teamDir: legacyTeamDir, agentsDir: legacyAgentsDir },
|
|
555
|
+
dest: { teamDir: destTeamDir, rolesDir: destRolesDir },
|
|
556
|
+
steps: [] as any[],
|
|
557
|
+
agentIds: [] as string[],
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const legacyTeamExists = await exists(legacyTeamDir);
|
|
561
|
+
if (!legacyTeamExists) {
|
|
562
|
+
throw new Error(`Legacy team directory not found: ${legacyTeamDir}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const destExists = await exists(destTeamDir);
|
|
566
|
+
if (destExists && !options.overwrite) {
|
|
567
|
+
throw new Error(`Destination already exists: ${destTeamDir} (re-run with --overwrite to merge)`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 1) Move/copy team shared workspace
|
|
571
|
+
plan.steps.push({ kind: "teamDir", from: legacyTeamDir, to: destTeamDir });
|
|
572
|
+
|
|
573
|
+
// 2) Move/copy each role agent directory into roles/<role>/
|
|
574
|
+
const legacyAgentsExist = await exists(legacyAgentsDir);
|
|
575
|
+
let legacyAgentFolders: string[] = [];
|
|
576
|
+
if (legacyAgentsExist) {
|
|
577
|
+
legacyAgentFolders = (await fs.readdir(legacyAgentsDir)).filter((x) => x.startsWith(`${teamId}-`));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const folder of legacyAgentFolders) {
|
|
581
|
+
const agentId = folder;
|
|
582
|
+
const role = folder.slice((teamId + "-").length);
|
|
583
|
+
const from = path.join(legacyAgentsDir, folder);
|
|
584
|
+
const to = path.join(destRolesDir, role);
|
|
585
|
+
plan.agentIds.push(agentId);
|
|
586
|
+
plan.steps.push({ kind: "roleDir", agentId, role, from, to });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const dryRun = !!options.dryRun;
|
|
590
|
+
if (dryRun) {
|
|
591
|
+
console.log(JSON.stringify({ ok: true, dryRun: true, plan }, null, 2));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Helpers
|
|
596
|
+
const copyDirRecursive = async (src: string, dst: string) => {
|
|
597
|
+
await ensureDir(dst);
|
|
598
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
599
|
+
for (const ent of entries) {
|
|
600
|
+
const s = path.join(src, ent.name);
|
|
601
|
+
const d = path.join(dst, ent.name);
|
|
602
|
+
if (ent.isDirectory()) await copyDirRecursive(s, d);
|
|
603
|
+
else if (ent.isSymbolicLink()) {
|
|
604
|
+
const link = await fs.readlink(s);
|
|
605
|
+
await fs.symlink(link, d);
|
|
606
|
+
} else {
|
|
607
|
+
await ensureDir(path.dirname(d));
|
|
608
|
+
await fs.copyFile(s, d);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const removeDirRecursive = async (p: string) => {
|
|
614
|
+
// node 25 supports fs.rm
|
|
615
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const moveDir = async (src: string, dst: string) => {
|
|
619
|
+
await ensureDir(path.dirname(dst));
|
|
620
|
+
try {
|
|
621
|
+
await fs.rename(src, dst);
|
|
622
|
+
} catch {
|
|
623
|
+
// cross-device or existing: fallback to copy+remove
|
|
624
|
+
await copyDirRecursive(src, dst);
|
|
625
|
+
await removeDirRecursive(src);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Execute plan
|
|
630
|
+
if (mode === "copy") {
|
|
631
|
+
await copyDirRecursive(legacyTeamDir, destTeamDir);
|
|
632
|
+
} else {
|
|
633
|
+
await moveDir(legacyTeamDir, destTeamDir);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Ensure roles dir exists
|
|
637
|
+
await ensureDir(destRolesDir);
|
|
638
|
+
|
|
639
|
+
for (const step of plan.steps.filter((s: any) => s.kind === "roleDir")) {
|
|
640
|
+
if (!(await exists(step.from))) continue;
|
|
641
|
+
if (mode === "copy") await copyDirRecursive(step.from, step.to);
|
|
642
|
+
else await moveDir(step.from, step.to);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Update config: set each team agent's workspace to destTeamDir (shared)
|
|
646
|
+
const agentSnippets: AgentConfigSnippet[] = plan.agentIds.map((agentId: string) => ({
|
|
647
|
+
id: agentId,
|
|
648
|
+
workspace: destTeamDir,
|
|
649
|
+
}));
|
|
650
|
+
if (agentSnippets.length) {
|
|
651
|
+
await applyAgentSnippetsToOpenClawConfig(api, agentSnippets);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(JSON.stringify({ ok: true, migrated: teamId, destTeamDir, agentIds: plan.agentIds }, null, 2));
|
|
655
|
+
});
|
|
656
|
+
|
|
396
657
|
cmd
|
|
397
658
|
.command("install")
|
|
398
659
|
.description("Install a ClawHub skill into this OpenClaw workspace (confirmation-gated)")
|
|
@@ -509,7 +770,8 @@ const recipesPlugin = {
|
|
|
509
770
|
if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
|
|
510
771
|
|
|
511
772
|
const teamId = String(options.teamId);
|
|
512
|
-
|
|
773
|
+
// Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
|
|
774
|
+
const teamDir = path.resolve(workspaceRoot, "..", `workspace-${teamId}`);
|
|
513
775
|
|
|
514
776
|
const inboxDir = path.join(teamDir, "inbox");
|
|
515
777
|
const backlogDir = path.join(teamDir, "work", "backlog");
|
|
@@ -658,10 +920,16 @@ const recipesPlugin = {
|
|
|
658
920
|
return;
|
|
659
921
|
}
|
|
660
922
|
|
|
923
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace ?? "~/.openclaw/workspace";
|
|
924
|
+
// Put standalone agent workspaces alongside the default workspace (same parent dir).
|
|
925
|
+
const resolvedWorkspaceRoot = path.resolve(baseWorkspace, "..", `workspace-${options.agentId}`);
|
|
926
|
+
|
|
661
927
|
const result = await scaffoldAgentFromRecipe(api, recipe, {
|
|
662
928
|
agentId: options.agentId,
|
|
663
929
|
agentName: options.name,
|
|
664
930
|
update: !!options.overwrite,
|
|
931
|
+
filesRootDir: resolvedWorkspaceRoot,
|
|
932
|
+
workspaceRootDir: resolvedWorkspaceRoot,
|
|
665
933
|
vars: {
|
|
666
934
|
agentId: options.agentId,
|
|
667
935
|
agentName: options.name ?? recipe.name ?? options.agentId,
|
|
@@ -702,8 +970,15 @@ const recipesPlugin = {
|
|
|
702
970
|
return;
|
|
703
971
|
}
|
|
704
972
|
|
|
705
|
-
const
|
|
973
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
974
|
+
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
975
|
+
|
|
976
|
+
// Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
|
|
977
|
+
const teamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
|
|
706
978
|
await ensureDir(teamDir);
|
|
979
|
+
|
|
980
|
+
const rolesDir = path.join(teamDir, "roles");
|
|
981
|
+
await ensureDir(rolesDir);
|
|
707
982
|
const notesDir = path.join(teamDir, "notes");
|
|
708
983
|
const workDir = path.join(teamDir, "work");
|
|
709
984
|
const backlogDir = path.join(workDir, "backlog");
|
|
@@ -761,16 +1036,22 @@ const recipesPlugin = {
|
|
|
761
1036
|
tools: a.tools ?? recipe.tools,
|
|
762
1037
|
};
|
|
763
1038
|
|
|
1039
|
+
const roleDir = path.join(rolesDir, role);
|
|
764
1040
|
const r = await scaffoldAgentFromRecipe(api, scopedRecipe, {
|
|
765
1041
|
agentId,
|
|
766
1042
|
agentName,
|
|
767
1043
|
update: !!options.overwrite,
|
|
1044
|
+
// Write role-specific files under roles/<role>/
|
|
1045
|
+
filesRootDir: roleDir,
|
|
1046
|
+
// But set the agent workspace root to the shared team workspace
|
|
1047
|
+
workspaceRootDir: teamDir,
|
|
768
1048
|
vars: {
|
|
769
1049
|
teamId,
|
|
770
1050
|
teamDir,
|
|
771
1051
|
role,
|
|
772
1052
|
agentId,
|
|
773
1053
|
agentName,
|
|
1054
|
+
roleDir,
|
|
774
1055
|
},
|
|
775
1056
|
});
|
|
776
1057
|
results.push({ role, agentId, ...r });
|