assistme 0.8.12 → 0.9.0

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/PLAN.md CHANGED
@@ -143,4 +143,4 @@ New page: `/agent` - Agent chat interface
143
143
 
144
144
  ### Phase 4: Edge Function
145
145
 
146
- 1. Create agent-submit-task edge function for mobile clients
146
+ 1. Create main-agent-submit-task edge function for mobile clients
@@ -8,7 +8,7 @@ import {
8
8
  readAuthStore,
9
9
  safeParse,
10
10
  writeAuthStore
11
- } from "./chunk-5ML5YMCM.js";
11
+ } from "./chunk-RRMI6RDG.js";
12
12
  import {
13
13
  log,
14
14
  newCorrelationId,
@@ -2833,19 +2833,51 @@ var TaskPoller = class {
2833
2833
  // src/agent/memory/manager.ts
2834
2834
  var MemoryManager = class {
2835
2835
  /**
2836
- * Store a new memory. Called by the agent after completing tasks
2837
- * to remember important facts about the user.
2836
+ * When set, all memory operations use this agent's users.id as the
2837
+ * effective user, giving the agent its own memory space.
2838
+ */
2839
+ agentUserId = null;
2840
+ /** The human owner's user ID — used to load owner memories as fallback. */
2841
+ ownerUserId = null;
2842
+ /**
2843
+ * Configure agent-scoped memory.
2844
+ * @param agentUserId The agent's users.id — memories are stored/read here
2845
+ * @param ownerUserId The human owner's users.id — owner memories loaded as fallback
2846
+ */
2847
+ setAgentContext(agentUserId, ownerUserId) {
2848
+ this.agentUserId = agentUserId;
2849
+ this.ownerUserId = ownerUserId;
2850
+ }
2851
+ /** Clear agent context (revert to normal user-scoped mode). */
2852
+ clearAgentContext() {
2853
+ this.agentUserId = null;
2854
+ this.ownerUserId = null;
2855
+ }
2856
+ /** Inject effective_user_id + owner_user_id into params. */
2857
+ agentParams(extra = {}) {
2858
+ const params = { ...extra };
2859
+ if (this.agentUserId) {
2860
+ params.effective_user_id = this.agentUserId;
2861
+ if (this.ownerUserId) {
2862
+ params.owner_user_id = this.ownerUserId;
2863
+ }
2864
+ }
2865
+ return params;
2866
+ }
2867
+ /**
2868
+ * Store a new memory. Called by the agent after completing tasks.
2869
+ * When agent context is set, stores under the agent's user_id.
2838
2870
  */
2839
2871
  async remember(content, category = "general", options) {
2840
2872
  const expiresAt = options?.expiresInDays ? new Date(Date.now() + options.expiresInDays * 864e5).toISOString() : null;
2841
- const data = await callMcpHandler("memory.store", {
2873
+ const data = await callMcpHandler("memory.store", this.agentParams({
2842
2874
  category,
2843
2875
  content,
2844
2876
  importance: options?.importance ?? 5,
2845
2877
  tags: options?.tags ?? [],
2846
2878
  source_message_id: options?.sourceMessageId ?? null,
2847
2879
  expires_at: expiresAt
2848
- });
2880
+ }));
2849
2881
  log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
2850
2882
  return data;
2851
2883
  }
@@ -2854,10 +2886,10 @@ var MemoryManager = class {
2854
2886
  */
2855
2887
  async search(query9, limit = 10) {
2856
2888
  try {
2857
- return await callMcpHandler("memory.search", {
2889
+ return await callMcpHandler("memory.search", this.agentParams({
2858
2890
  query: query9,
2859
2891
  limit
2860
- });
2892
+ }));
2861
2893
  } catch (err) {
2862
2894
  log.warn(`Memory search failed: ${err instanceof Error ? err.message : err}`);
2863
2895
  return [];
@@ -2865,13 +2897,13 @@ var MemoryManager = class {
2865
2897
  }
2866
2898
  /**
2867
2899
  * Get the most important/recent memories to include in context.
2868
- * Called before each task to build the agent's "working memory".
2869
- * Automatically filters out expired memories.
2900
+ * When agent context is set, loads agent's memories first,
2901
+ * then supplements with the owner's memories as fallback.
2870
2902
  */
2871
2903
  async getContext(maxItems = 20) {
2872
- const all = await callMcpHandler("memory.get_context", {
2904
+ const all = await callMcpHandler("memory.get_context", this.agentParams({
2873
2905
  max_items: maxItems
2874
- });
2906
+ }));
2875
2907
  const seen = /* @__PURE__ */ new Set();
2876
2908
  return (all || []).filter((m) => {
2877
2909
  if (seen.has(m.id)) return false;
@@ -2885,48 +2917,63 @@ var MemoryManager = class {
2885
2917
  async buildMemoryPrompt() {
2886
2918
  const memories = await this.getContext();
2887
2919
  if (memories.length === 0) return "";
2888
- const sections = {};
2889
- for (const m of memories) {
2890
- const key = m.category;
2891
- if (!sections[key]) sections[key] = [];
2892
- sections[key].push(`- ${m.content}`);
2893
- }
2920
+ const hasOwnerFlag = memories.some((m) => m._from_owner);
2921
+ const agentMemories = hasOwnerFlag ? memories.filter((m) => !m._from_owner) : memories;
2922
+ const ownerMemories = hasOwnerFlag ? memories.filter((m) => m._from_owner) : [];
2894
2923
  const categoryLabels = {
2895
2924
  instruction: "Standing Instructions",
2896
- preference: "User Preferences",
2925
+ preference: "Preferences",
2897
2926
  general: "Known Facts",
2898
2927
  context: "Context",
2899
2928
  skill_learned: "Learned Skills",
2900
2929
  fact: "Facts"
2901
2930
  };
2902
- let prompt = "\n\n## What You Know About The User\n";
2903
- for (const [cat, items] of Object.entries(sections)) {
2904
- prompt += `
2931
+ function formatSection(mems) {
2932
+ const sections = {};
2933
+ for (const m of mems) {
2934
+ const key = m.category;
2935
+ if (!sections[key]) sections[key] = [];
2936
+ sections[key].push(`- ${m.content}`);
2937
+ }
2938
+ let out = "";
2939
+ for (const [cat, items] of Object.entries(sections)) {
2940
+ out += `
2905
2941
  ### ${categoryLabels[cat] || cat}
2906
2942
  `;
2907
- prompt += `${items.join("\n")}
2943
+ out += `${items.join("\n")}
2908
2944
  `;
2945
+ }
2946
+ return out;
2947
+ }
2948
+ let prompt = "";
2949
+ if (agentMemories.length > 0) {
2950
+ prompt += this.agentUserId ? "\n\n## Your Memories\n" : "\n\n## What You Know About The User\n";
2951
+ prompt += formatSection(agentMemories);
2952
+ }
2953
+ if (ownerMemories.length > 0) {
2954
+ prompt += "\n\n## Owner's Context\n";
2955
+ prompt += formatSection(ownerMemories);
2909
2956
  }
2910
2957
  return prompt;
2911
2958
  }
2912
2959
  // ── CRUD for CLI ────────────────────────────────────────────
2913
2960
  async list(category, limit = 20) {
2914
- const data = await callMcpHandler("memory.list", {
2961
+ const data = await callMcpHandler("memory.list", this.agentParams({
2915
2962
  category: category || null,
2916
2963
  limit
2917
- });
2964
+ }));
2918
2965
  return data || [];
2919
2966
  }
2920
2967
  async add(content, category = "general", importance = 5, tags = []) {
2921
2968
  return this.remember(content, category, { importance, tags });
2922
2969
  }
2923
2970
  async remove(memoryId) {
2924
- await callMcpHandler("memory.remove", { memory_id: memoryId });
2971
+ await callMcpHandler("memory.remove", this.agentParams({ memory_id: memoryId }));
2925
2972
  }
2926
2973
  async clear(category) {
2927
- const result = await callMcpHandler("memory.clear", {
2974
+ const result = await callMcpHandler("memory.clear", this.agentParams({
2928
2975
  category: category || null
2929
- });
2976
+ }));
2930
2977
  return result.count;
2931
2978
  }
2932
2979
  };
@@ -5528,7 +5575,7 @@ function createJobTools(sessionId, taskId) {
5528
5575
  const job = await runner.loadJob(args.job_name);
5529
5576
  if (!job) {
5530
5577
  const jobs = await runner.listJobs();
5531
- const available = jobs.length > 0 ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills)`).join(", ")}` : "No jobs defined yet. Use skill_generate to create a job from a job description.";
5578
+ const available = jobs.length > 0 ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills, ${j.agentCount} agents)`).join(", ")}` : "No jobs defined yet. Use skill_generate to create a job from a job description.";
5532
5579
  return textResponse(`Job "${args.job_name}" not found. ${available}`);
5533
5580
  }
5534
5581
  if (job.skills.length === 0) {
@@ -5569,11 +5616,38 @@ function createJobTools(sessionId, taskId) {
5569
5616
  }
5570
5617
  }
5571
5618
  }
5572
- const prompt = runner.buildJobPrompt(job, runId || "untracked");
5619
+ const effectiveRunId = runId || "untracked";
5620
+ const prompt = runner.buildJobPrompt(job, effectiveRunId);
5621
+ const primaryAgentForFilter = job.agents.length > 0 ? job.agents.find((p) => p.role === "primary") || job.agents[0] : null;
5622
+ const parallelAgents = job.agents.filter(
5623
+ (a) => a.role === "parallel" && a.agentId !== primaryAgentForFilter?.agentId
5624
+ );
5625
+ let fullResponse = prompt;
5626
+ if (parallelAgents.length > 0) {
5627
+ fullResponse += `
5628
+
5629
+ ---
5630
+
5631
+ ## Parallel Agent Dispatch
5632
+ `;
5633
+ fullResponse += `The following parallel agents are assigned to this job. `;
5634
+ fullResponse += `After you complete your primary tasks, review their responsibilities and execute any that haven't been covered:
5635
+
5636
+ `;
5637
+ for (const pa of parallelAgents) {
5638
+ fullResponse += `### ${pa.agentName} (@${pa.agentUsername})
5639
+ `;
5640
+ if (pa.agentBio) fullResponse += `*${pa.agentBio}*
5641
+ `;
5642
+ fullResponse += `**Instructions:** ${pa.agentPrompt}
5643
+
5644
+ `;
5645
+ }
5646
+ }
5573
5647
  log.info(
5574
- `Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
5648
+ `Job "${args.job_name}" started with ${job.skills.length} skills, ${job.agents.length} agents (run: ${effectiveRunId.slice(0, 8)})`
5575
5649
  );
5576
- return textResponse(prompt);
5650
+ return textResponse(fullResponse);
5577
5651
  })
5578
5652
  ),
5579
5653
  tool4(
@@ -5639,6 +5713,80 @@ function createJobTools(sessionId, taskId) {
5639
5713
  }
5640
5714
  )
5641
5715
  ),
5716
+ tool4(
5717
+ "job_assign_agent",
5718
+ "Create or update an agent for a job. The agent is a user with a specific prompt and identity that will execute this job.",
5719
+ {
5720
+ job_name: z4.string().describe("Name of the job to assign the agent to"),
5721
+ agent_name: z4.string().describe("Display name for the agent (e.g. 'Daily Reporter')"),
5722
+ agent_prompt: z4.string().describe("The agent's system prompt that defines its behavior and personality"),
5723
+ agent_bio: z4.string().optional().describe("Short bio describing the agent's role"),
5724
+ role: z4.enum(["primary", "parallel"]).optional().describe("Agent role: 'primary' (default) or 'parallel' for concurrent execution")
5725
+ },
5726
+ typedHandler(
5727
+ async (args) => {
5728
+ try {
5729
+ const result = await callMcpHandler("job.save_agent", {
5730
+ job_name: args.job_name,
5731
+ agent_name: args.agent_name,
5732
+ agent_prompt: args.agent_prompt,
5733
+ agent_bio: args.agent_bio || null,
5734
+ role: args.role || "primary"
5735
+ });
5736
+ if (!result) {
5737
+ return textResponse(`Failed to create agent for job "${args.job_name}".`);
5738
+ }
5739
+ log.info(
5740
+ `Agent "${result.username}" assigned to job "${args.job_name}" (${args.role || "primary"})`
5741
+ );
5742
+ let response = `## Agent Assigned to Job: ${args.job_name}
5743
+
5744
+ `;
5745
+ response += `- **Agent:** ${args.agent_name} (@${result.username})
5746
+ `;
5747
+ response += `- **Role:** ${args.role || "primary"}
5748
+ `;
5749
+ if (args.agent_bio) {
5750
+ response += `- **Bio:** ${args.agent_bio}
5751
+ `;
5752
+ }
5753
+ response += `
5754
+ The agent is now ready to execute this job.`;
5755
+ return textResponse(response);
5756
+ } catch (err) {
5757
+ return textResponse(
5758
+ `Failed to assign agent: ${err instanceof Error ? err.message : err}`
5759
+ );
5760
+ }
5761
+ }
5762
+ )
5763
+ ),
5764
+ tool4(
5765
+ "job_remove_agent",
5766
+ "Remove (deactivate) an agent from a job.",
5767
+ {
5768
+ job_name: z4.string().describe("Name of the job"),
5769
+ agent_name: z4.string().describe("Display name of the agent to remove")
5770
+ },
5771
+ typedHandler(
5772
+ async (args) => {
5773
+ try {
5774
+ await callMcpHandler("job.remove_agent", {
5775
+ job_name: args.job_name,
5776
+ agent_name: args.agent_name
5777
+ });
5778
+ log.info(`Agent "${args.agent_name}" removed from job "${args.job_name}"`);
5779
+ return textResponse(
5780
+ `Agent "${args.agent_name}" has been removed from job "${args.job_name}".`
5781
+ );
5782
+ } catch (err) {
5783
+ return textResponse(
5784
+ `Failed to remove agent: ${err instanceof Error ? err.message : err}`
5785
+ );
5786
+ }
5787
+ }
5788
+ )
5789
+ ),
5642
5790
  tool4(
5643
5791
  "job_status",
5644
5792
  "Check the status and run history of a job.",
@@ -5657,7 +5805,7 @@ function createJobTools(sessionId, taskId) {
5657
5805
  }
5658
5806
  let response2 = "## Your Jobs\n\n";
5659
5807
  for (const job of jobs) {
5660
- response2 += `- **${job.name}** (${job.skillCount} skills): ${job.description.slice(0, 100)}
5808
+ response2 += `- **${job.name}** (${job.skillCount} skills, ${job.agentCount} agents): ${job.description.slice(0, 100)}
5661
5809
  `;
5662
5810
  }
5663
5811
  response2 += "\nUse `job_run` to execute a job, or `job_schedule` to automate it.";
@@ -8420,6 +8568,11 @@ var TaskProcessor = class {
8420
8568
  resetEventSequence();
8421
8569
  newCorrelationId();
8422
8570
  log.info(`Processing task ${task.id.slice(0, 8)}...`);
8571
+ const taskAgentId = task.metadata?.agent_id || (task.sender_id !== this.userId ? task.sender_id : null);
8572
+ if (taskAgentId && this.memoryManager && this.userId) {
8573
+ this.memoryManager.setAgentContext(taskAgentId, this.userId);
8574
+ log.debug(`Memory context set to agent ${taskAgentId.slice(0, 8)}`);
8575
+ }
8423
8576
  const toolCallRecords = [];
8424
8577
  const toolFailures = [];
8425
8578
  try {
@@ -8479,6 +8632,9 @@ var TaskProcessor = class {
8479
8632
  await emitEvent(task.id, "status_change", { status: "failed" });
8480
8633
  } finally {
8481
8634
  setCorrelationId(null);
8635
+ if (taskAgentId && this.memoryManager) {
8636
+ this.memoryManager.clearAgentContext();
8637
+ }
8482
8638
  try {
8483
8639
  const browser = getBrowser();
8484
8640
  if (browser.isConnected()) {
@@ -43,7 +43,7 @@ function getRawToken() {
43
43
  async function callMcpHandler(action, params = {}, overrideToken) {
44
44
  const config = getConfig();
45
45
  const token = overrideToken || getRawToken();
46
- const url = `${config.supabaseUrl}/functions/v1/mcp-handler`;
46
+ const url = `${config.supabaseUrl}/functions/v1/main-mcp-handler`;
47
47
  const response = await fetch(url, {
48
48
  method: "POST",
49
49
  headers: {
@@ -91,6 +91,14 @@ var SkillRowSchema = z.object({
91
91
  source_skill_id: z.string().optional().nullable(),
92
92
  invocation_count: z.number().optional().default(0)
93
93
  });
94
+ var JobAgentSchema = z.object({
95
+ agent_id: z.string(),
96
+ agent_username: z.string(),
97
+ agent_name: z.string().optional().default(""),
98
+ agent_bio: z.string().optional().nullable(),
99
+ agent_prompt: z.string(),
100
+ role: z.string().optional().default("primary")
101
+ });
94
102
  var JobRowSchema = z.object({
95
103
  job_id: z.string(),
96
104
  job_name: z.string(),
@@ -100,14 +108,16 @@ var JobRowSchema = z.object({
100
108
  skill_name: z.string().optional().nullable(),
101
109
  skill_description: z.string().optional().default(""),
102
110
  skill_emoji: z.string().optional().default(""),
103
- skill_content: z.string().optional().default("")
111
+ skill_content: z.string().optional().default(""),
112
+ agents: z.array(JobAgentSchema).optional().default([])
104
113
  });
105
114
  var JobListRowSchema = z.object({
106
115
  id: z.string(),
107
116
  name: z.string(),
108
117
  description: z.string().optional().default(""),
109
118
  prompt: z.string().optional().nullable(),
110
- skill_count: z.number().optional().default(0)
119
+ skill_count: z.number().optional().default(0),
120
+ agent_count: z.number().optional().default(0)
111
121
  });
112
122
  var JobRunRowSchema = z.object({
113
123
  run_id: z.string(),
@@ -193,6 +203,15 @@ var JobRunner = class {
193
203
  const rows = data.map((row) => safeParse(JobRowSchema, row)).filter((r) => r != null);
194
204
  if (rows.length === 0) return null;
195
205
  const first = rows[0];
206
+ const rawAgents = first.agents || [];
207
+ const agents = rawAgents.map((a) => safeParse(JobAgentSchema, a)).filter((a) => a != null).map((a) => ({
208
+ agentId: a.agent_id,
209
+ agentUsername: a.agent_username,
210
+ agentName: a.agent_name || "",
211
+ agentBio: a.agent_bio ?? null,
212
+ agentPrompt: a.agent_prompt,
213
+ role: a.role
214
+ }));
196
215
  return {
197
216
  jobId: first.job_id,
198
217
  jobName: first.job_name,
@@ -204,7 +223,8 @@ var JobRunner = class {
204
223
  skillDescription: row.skill_description,
205
224
  skillEmoji: row.skill_emoji,
206
225
  skillContent: row.skill_content
207
- }))
226
+ })),
227
+ agents
208
228
  };
209
229
  } catch (err) {
210
230
  log.debug(`Failed to load job "${jobName}": ${errorMessage(err)}`);
@@ -221,7 +241,8 @@ var JobRunner = class {
221
241
  id: row.id,
222
242
  name: row.name,
223
243
  description: row.description,
224
- skillCount: row.skill_count
244
+ skillCount: row.skill_count,
245
+ agentCount: row.agent_count
225
246
  }));
226
247
  } catch (err) {
227
248
  log.debug(`Failed to list jobs: ${errorMessage(err)}`);
@@ -313,6 +334,9 @@ var JobRunner = class {
313
334
  `;
314
335
  }
315
336
  prompt += `## Instructions
337
+
338
+ `;
339
+ prompt += `### Step 1: Analyze and set up skills
316
340
  `;
317
341
  prompt += `For each capability the job needs:
318
342
  `;
@@ -326,8 +350,24 @@ var JobRunner = class {
326
350
 
327
351
  `;
328
352
  prompt += `Be practical \u2014 only create skills that would genuinely help automate this job.
353
+
354
+ `;
355
+ prompt += `### Step 2: Create the job's agent
356
+ `;
357
+ prompt += `After setting up skills, create an agent for this job using \`job_assign_agent\`.
358
+ `;
359
+ prompt += `The agent is the identity that will execute this job. Design it with:
329
360
  `;
330
- prompt += `When done, summarize what skills were created, updated, or already existed.
361
+ prompt += `- **agent_name**: A concise, role-based name (e.g. "Daily Reporter", "Code Reviewer")
362
+ `;
363
+ prompt += `- **agent_prompt**: A system prompt that defines the agent's behavior, expertise, and personality. `;
364
+ prompt += `Include the job's goals, preferred approach, and any domain knowledge the agent should have. `;
365
+ prompt += `Reference the linked skills as capabilities the agent can use.
366
+ `;
367
+ prompt += `- **agent_bio**: A one-line description of what this agent does.
368
+
369
+ `;
370
+ prompt += `When done, summarize what skills were created/updated and the agent that was assigned.
331
371
  `;
332
372
  return prompt;
333
373
  }
@@ -340,6 +380,10 @@ var JobRunner = class {
340
380
  * chain them based on what it discovers at runtime.
341
381
  */
342
382
  buildJobPrompt(job, runId) {
383
+ const primaryAgent = job.agents.length > 0 ? job.agents.find((a) => a.role === "primary") || job.agents[0] : null;
384
+ const parallelAgents = job.agents.filter(
385
+ (a) => a.role === "parallel" && a.agentId !== primaryAgent?.agentId
386
+ );
343
387
  const effectiveDescription = job.jobPrompt || job.jobDescription;
344
388
  let prompt = `## Job: ${job.jobName}
345
389
  `;
@@ -349,10 +393,43 @@ var JobRunner = class {
349
393
  prompt += `**Run ID:** ${runId}
350
394
 
351
395
  `;
352
- prompt += `You are now acting as "${job.jobName}". `;
353
- prompt += `Your goal is to accomplish the objectives described above using the skills and tools available to you.
396
+ if (primaryAgent) {
397
+ prompt += `## Agent Identity
398
+ `;
399
+ prompt += `You are **${primaryAgent.agentName}** (@${primaryAgent.agentUsername}).
400
+ `;
401
+ if (primaryAgent.agentBio) {
402
+ prompt += `*${primaryAgent.agentBio}*
403
+ `;
404
+ }
405
+ prompt += `
406
+ ### Agent Instructions
407
+ `;
408
+ prompt += `${primaryAgent.agentPrompt}
409
+
410
+ `;
411
+ } else {
412
+ prompt += `You are now acting as "${job.jobName}". `;
413
+ prompt += `Your goal is to accomplish the objectives described above using the skills and tools available to you.
414
+
415
+ `;
416
+ }
417
+ if (parallelAgents.length > 0) {
418
+ prompt += `### Parallel Agents
419
+ `;
420
+ prompt += `The following agents are running concurrently on this job. `;
421
+ prompt += `They handle their own tasks independently \u2014 coordinate to avoid duplicate work.
354
422
 
355
423
  `;
424
+ for (const pa of parallelAgents) {
425
+ prompt += `- **${pa.agentName}** (@${pa.agentUsername})`;
426
+ if (pa.agentBio) prompt += ` \u2014 ${pa.agentBio}`;
427
+ prompt += `
428
+ `;
429
+ }
430
+ prompt += `
431
+ `;
432
+ }
356
433
  prompt += `### Available Skills
357
434
  `;
358
435
  prompt += `These skills are your capabilities for this job. `;
package/dist/index.js CHANGED
@@ -34,11 +34,11 @@ import {
34
34
  setSessionBusy,
35
35
  toggleScheduledTask,
36
36
  updateHeartbeat
37
- } from "./chunk-MP4E522X.js";
37
+ } from "./chunk-E27P57PM.js";
38
38
  import {
39
39
  JobRunner,
40
40
  callMcpHandler
41
- } from "./chunk-5ML5YMCM.js";
41
+ } from "./chunk-RRMI6RDG.js";
42
42
  import {
43
43
  log,
44
44
  setLogConversationId,
@@ -382,7 +382,7 @@ function registerJobCommands(program2) {
382
382
  jobCmd.command("list").description("List your defined jobs").action(async () => {
383
383
  try {
384
384
  await getCurrentUserId();
385
- const { JobRunner: JobRunner2 } = await import("./job-runner-NA3KJCKA.js");
385
+ const { JobRunner: JobRunner2 } = await import("./job-runner-4KTCVIQP.js");
386
386
  const runner = new JobRunner2();
387
387
  const jobs = await runner.listJobs();
388
388
  if (jobs.length === 0) {
@@ -406,7 +406,7 @@ function registerJobCommands(program2) {
406
406
  jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
407
407
  try {
408
408
  await getCurrentUserId();
409
- const { JobRunner: JobRunner2 } = await import("./job-runner-NA3KJCKA.js");
409
+ const { JobRunner: JobRunner2 } = await import("./job-runner-4KTCVIQP.js");
410
410
  const runner = new JobRunner2();
411
411
  const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
412
412
  if (runs.length === 0) {
@@ -454,7 +454,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
454
454
  process.exit(1);
455
455
  }
456
456
  await getCurrentUserId();
457
- const { JobRunner: JobRunner2 } = await import("./job-runner-NA3KJCKA.js");
457
+ const { JobRunner: JobRunner2 } = await import("./job-runner-4KTCVIQP.js");
458
458
  const runner = new JobRunner2();
459
459
  const job = await runner.loadJob(name);
460
460
  if (!job) {
@@ -2002,7 +2002,31 @@ var Orchestrator = class {
2002
2002
  } catch (linkErr) {
2003
2003
  log.debug(`Failed to link session to job run: ${linkErr}`);
2004
2004
  }
2005
- const prompt = runner.buildJobPrompt(job, jobRun.id);
2005
+ let prompt = runner.buildJobPrompt(job, jobRun.id);
2006
+ const primaryAgent = job.agents.length > 0 ? job.agents.find((a) => a.role === "primary") || job.agents[0] : null;
2007
+ const parallelAgents = job.agents.filter(
2008
+ (a) => a.role === "parallel" && a.agentId !== primaryAgent?.agentId
2009
+ );
2010
+ if (parallelAgents.length > 0) {
2011
+ prompt += `
2012
+
2013
+ ---
2014
+
2015
+ ## Parallel Agent Responsibilities
2016
+ `;
2017
+ prompt += `The following agents are also assigned. Execute their responsibilities as part of this run:
2018
+
2019
+ `;
2020
+ for (const pa of parallelAgents) {
2021
+ prompt += `### ${pa.agentName}
2022
+ `;
2023
+ if (pa.agentBio) prompt += `*${pa.agentBio}*
2024
+ `;
2025
+ prompt += `**Instructions:** ${pa.agentPrompt}
2026
+
2027
+ `;
2028
+ }
2029
+ }
2006
2030
  try {
2007
2031
  await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
2008
2032
  await runner.completeRun(jobRun.id, "completed", "Job executed via web trigger");
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  JobRunner
3
- } from "./chunk-5ML5YMCM.js";
3
+ } from "./chunk-RRMI6RDG.js";
4
4
  import "./chunk-YHUARPU7.js";
5
5
  import "./chunk-ZUUASYPZ.js";
6
6
  export {
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  TaskProcessor,
4
4
  getBrowser
5
- } from "../chunk-MP4E522X.js";
6
- import "../chunk-5ML5YMCM.js";
5
+ } from "../chunk-E27P57PM.js";
6
+ import "../chunk-RRMI6RDG.js";
7
7
  import {
8
8
  setLogTransport
9
9
  } from "../chunk-YHUARPU7.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.8.12",
3
+ "version": "0.9.0",
4
4
  "description": "AssistMe CLI Agent - AI-powered agentic assistant for code, browser, and automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -194,6 +194,15 @@ export class TaskProcessor {
194
194
  newCorrelationId();
195
195
  log.info(`Processing task ${task.id.slice(0, 8)}...`);
196
196
 
197
+ // Set agent memory context if this task is from a job agent
198
+ const taskAgentId =
199
+ (task.metadata?.agent_id as string) ||
200
+ (task.sender_id !== this.userId ? task.sender_id : null);
201
+ if (taskAgentId && this.memoryManager && this.userId) {
202
+ this.memoryManager.setAgentContext(taskAgentId, this.userId);
203
+ log.debug(`Memory context set to agent ${taskAgentId.slice(0, 8)}`);
204
+ }
205
+
197
206
  const toolCallRecords: ToolCallRecord[] = [];
198
207
  const toolFailures: ToolFailureRecord[] = [];
199
208
 
@@ -266,6 +275,11 @@ export class TaskProcessor {
266
275
  } finally {
267
276
  setCorrelationId(null);
268
277
 
278
+ // Clear agent memory context so next task uses default user scope
279
+ if (taskAgentId && this.memoryManager) {
280
+ this.memoryManager.clearAgentContext();
281
+ }
282
+
269
283
  try {
270
284
  const browser = getBrowser();
271
285
  if (browser.isConnected()) {
@@ -21,14 +21,53 @@ export interface Memory {
21
21
  last_accessed_at: string | null;
22
22
  expires_at: string | null;
23
23
  created_at: string;
24
+ /** Set by the backend when this memory comes from the owner (human user)
25
+ * rather than the agent itself. Only present in agent-scoped get_context. */
26
+ _from_owner?: boolean;
24
27
  }
25
28
 
26
29
  // ── Memory Manager ──────────────────────────────────────────────────
27
30
 
28
31
  export class MemoryManager {
29
32
  /**
30
- * Store a new memory. Called by the agent after completing tasks
31
- * to remember important facts about the user.
33
+ * When set, all memory operations use this agent's users.id as the
34
+ * effective user, giving the agent its own memory space.
35
+ */
36
+ private agentUserId: string | null = null;
37
+ /** The human owner's user ID — used to load owner memories as fallback. */
38
+ private ownerUserId: string | null = null;
39
+
40
+ /**
41
+ * Configure agent-scoped memory.
42
+ * @param agentUserId The agent's users.id — memories are stored/read here
43
+ * @param ownerUserId The human owner's users.id — owner memories loaded as fallback
44
+ */
45
+ setAgentContext(agentUserId: string, ownerUserId: string): void {
46
+ this.agentUserId = agentUserId;
47
+ this.ownerUserId = ownerUserId;
48
+ }
49
+
50
+ /** Clear agent context (revert to normal user-scoped mode). */
51
+ clearAgentContext(): void {
52
+ this.agentUserId = null;
53
+ this.ownerUserId = null;
54
+ }
55
+
56
+ /** Inject effective_user_id + owner_user_id into params. */
57
+ private agentParams(extra: Record<string, unknown> = {}): Record<string, unknown> {
58
+ const params: Record<string, unknown> = { ...extra };
59
+ if (this.agentUserId) {
60
+ params.effective_user_id = this.agentUserId;
61
+ if (this.ownerUserId) {
62
+ params.owner_user_id = this.ownerUserId;
63
+ }
64
+ }
65
+ return params;
66
+ }
67
+
68
+ /**
69
+ * Store a new memory. Called by the agent after completing tasks.
70
+ * When agent context is set, stores under the agent's user_id.
32
71
  */
33
72
  async remember(
34
73
  content: string,
@@ -44,14 +83,14 @@ export class MemoryManager {
44
83
  ? new Date(Date.now() + options.expiresInDays * 86400_000).toISOString()
45
84
  : null;
46
85
 
47
- const data = await callMcpHandler<Memory>("memory.store", {
86
+ const data = await callMcpHandler<Memory>("memory.store", this.agentParams({
48
87
  category,
49
88
  content,
50
89
  importance: options?.importance ?? 5,
51
90
  tags: options?.tags ?? [],
52
91
  source_message_id: options?.sourceMessageId ?? null,
53
92
  expires_at: expiresAt,
54
- });
93
+ }));
55
94
 
56
95
  log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
57
96
  return data;
@@ -62,10 +101,10 @@ export class MemoryManager {
62
101
  */
63
102
  async search(query: string, limit = 10): Promise<Memory[]> {
64
103
  try {
65
- return await callMcpHandler<Memory[]>("memory.search", {
104
+ return await callMcpHandler<Memory[]>("memory.search", this.agentParams({
66
105
  query,
67
106
  limit,
68
- });
107
+ }));
69
108
  } catch (err) {
70
109
  log.warn(`Memory search failed: ${err instanceof Error ? err.message : err}`);
71
110
  return [];
@@ -74,13 +113,13 @@ export class MemoryManager {
74
113
 
75
114
  /**
76
115
  * Get the most important/recent memories to include in context.
77
- * Called before each task to build the agent's "working memory".
78
- * Automatically filters out expired memories.
116
+ * When agent context is set, loads agent's memories first,
117
+ * then supplements with the owner's memories as fallback.
79
118
  */
80
119
  async getContext(maxItems = 20): Promise<Memory[]> {
81
- const all = await callMcpHandler<Memory[]>("memory.get_context", {
120
+ const all = await callMcpHandler<Memory[]>("memory.get_context", this.agentParams({
82
121
  max_items: maxItems,
83
- });
122
+ }));
84
123
 
85
124
  // Deduplicate by id
86
125
  const seen = new Set<string>();
@@ -98,39 +137,65 @@ export class MemoryManager {
98
137
  const memories = await this.getContext();
99
138
  if (memories.length === 0) return "";
100
139
 
101
- const sections: Record<string, string[]> = {};
102
-
103
- for (const m of memories) {
104
- const key = m.category;
105
- if (!sections[key]) sections[key] = [];
106
- sections[key].push(`- ${m.content}`);
107
- }
140
+ // Separate agent's own memories from owner's (only relevant in agent mode)
141
+ const hasOwnerFlag = memories.some((m) => m._from_owner);
142
+ const agentMemories = hasOwnerFlag
143
+ ? memories.filter((m) => !m._from_owner)
144
+ : memories;
145
+ const ownerMemories = hasOwnerFlag
146
+ ? memories.filter((m) => m._from_owner)
147
+ : [];
108
148
 
109
149
  const categoryLabels: Record<string, string> = {
110
150
  instruction: "Standing Instructions",
111
- preference: "User Preferences",
151
+ preference: "Preferences",
112
152
  general: "Known Facts",
113
153
  context: "Context",
114
154
  skill_learned: "Learned Skills",
115
155
  fact: "Facts",
116
156
  };
117
157
 
118
- let prompt = "\n\n## What You Know About The User\n";
119
- for (const [cat, items] of Object.entries(sections)) {
120
- prompt += `\n### ${categoryLabels[cat] || cat}\n`;
121
- prompt += `${items.join("\n") }\n`;
158
+ function formatSection(mems: Memory[]): string {
159
+ const sections: Record<string, string[]> = {};
160
+ for (const m of mems) {
161
+ const key = m.category;
162
+ if (!sections[key]) sections[key] = [];
163
+ sections[key].push(`- ${m.content}`);
164
+ }
165
+ let out = "";
166
+ for (const [cat, items] of Object.entries(sections)) {
167
+ out += `\n### ${categoryLabels[cat] || cat}\n`;
168
+ out += `${items.join("\n")}\n`;
169
+ }
170
+ return out;
171
+ }
172
+
173
+ let prompt = "";
174
+
175
+ if (agentMemories.length > 0) {
176
+ prompt += this.agentUserId
177
+ ? "\n\n## Your Memories\n"
178
+ : "\n\n## What You Know About The User\n";
179
+ prompt += formatSection(agentMemories);
122
180
  }
123
181
 
182
+ if (ownerMemories.length > 0) {
183
+ prompt += "\n\n## Owner's Context\n";
184
+ prompt += formatSection(ownerMemories);
185
+ }
186
+
187
+
188
+
124
189
  return prompt;
125
190
  }
126
191
 
127
192
  // ── CRUD for CLI ────────────────────────────────────────────
128
193
 
129
194
  async list(category?: MemoryCategory, limit = 20): Promise<Memory[]> {
130
- const data = await callMcpHandler<Memory[]>("memory.list", {
195
+ const data = await callMcpHandler<Memory[]>("memory.list", this.agentParams({
131
196
  category: category || null,
132
197
  limit,
133
- });
198
+ }));
134
199
  return data || [];
135
200
  }
136
201
 
@@ -144,13 +209,13 @@ export class MemoryManager {
144
209
  }
145
210
 
146
211
  async remove(memoryId: string): Promise<void> {
147
- await callMcpHandler("memory.remove", { memory_id: memoryId });
212
+ await callMcpHandler("memory.remove", this.agentParams({ memory_id: memoryId }));
148
213
  }
149
214
 
150
215
  async clear(category?: MemoryCategory): Promise<number> {
151
- const result = await callMcpHandler<{ count: number }>("memory.clear", {
216
+ const result = await callMcpHandler<{ count: number }>("memory.clear", this.agentParams({
152
217
  category: category || null,
153
- });
218
+ }));
154
219
  return result.count;
155
220
  }
156
221
 
@@ -2,7 +2,7 @@
2
2
  * Scheduling — orchestration, job lifecycle, task polling, and cron scheduling.
3
3
  */
4
4
  export { JobAnalysisPoller } from "./job-analysis-poller.js";
5
- export { type JobInfo, type JobRunInfo,JobRunner, type JobSkillInfo } from "./job-runner.js";
5
+ export { type JobAgentInfo, type JobInfo, type JobRunInfo, JobRunner, type JobSkillInfo } from "./job-runner.js";
6
6
  export {
7
7
  createScheduledTask,
8
8
  deleteScheduledTask,
@@ -1,7 +1,7 @@
1
1
  import { callMcpHandler } from "../../db/api-client.js";
2
2
  import { errorMessage } from "../../utils/errors.js";
3
3
  import { log } from "../../utils/logger.js";
4
- import { JobListRowSchema, JobRowSchema, JobRunRowSchema, safeParse } from "../../utils/schemas.js";
4
+ import { JobAgentSchema, JobListRowSchema, JobRowSchema, JobRunRowSchema, safeParse } from "../../utils/schemas.js";
5
5
 
6
6
  // ── Interfaces ─────────────────────────────────────────────────────
7
7
 
@@ -13,12 +13,22 @@ export interface JobSkillInfo {
13
13
  skillContent: string;
14
14
  }
15
15
 
16
+ export interface JobAgentInfo {
17
+ agentId: string;
18
+ agentUsername: string;
19
+ agentName: string;
20
+ agentBio: string | null;
21
+ agentPrompt: string;
22
+ role: string;
23
+ }
24
+
16
25
  export interface JobInfo {
17
26
  jobId: string;
18
27
  jobName: string;
19
28
  jobDescription: string;
20
29
  jobPrompt: string | null;
21
30
  skills: JobSkillInfo[];
31
+ agents: JobAgentInfo[];
22
32
  }
23
33
 
24
34
  export interface JobRunInfo {
@@ -55,6 +65,20 @@ export class JobRunner {
55
65
 
56
66
  const first = rows[0];
57
67
 
68
+ // Agents are attached to the first row only
69
+ const rawAgents = (first as Record<string, unknown>).agents as unknown[] || [];
70
+ const agents: JobAgentInfo[] = rawAgents
71
+ .map((a) => safeParse(JobAgentSchema, a))
72
+ .filter((a): a is NonNullable<typeof a> => a != null)
73
+ .map((a) => ({
74
+ agentId: a.agent_id,
75
+ agentUsername: a.agent_username,
76
+ agentName: a.agent_name || "",
77
+ agentBio: a.agent_bio ?? null,
78
+ agentPrompt: a.agent_prompt,
79
+ role: a.role,
80
+ }));
81
+
58
82
  return {
59
83
  jobId: first.job_id,
60
84
  jobName: first.job_name,
@@ -69,6 +93,7 @@ export class JobRunner {
69
93
  skillEmoji: row.skill_emoji,
70
94
  skillContent: row.skill_content,
71
95
  })),
96
+ agents,
72
97
  };
73
98
  } catch (err) {
74
99
  log.debug(`Failed to load job "${jobName}": ${errorMessage(err)}`);
@@ -80,7 +105,7 @@ export class JobRunner {
80
105
  * List all jobs for the user.
81
106
  */
82
107
  async listJobs(): Promise<
83
- Array<{ id: string; name: string; description: string; skillCount: number }>
108
+ Array<{ id: string; name: string; description: string; skillCount: number; agentCount: number }>
84
109
  > {
85
110
  try {
86
111
  const data = await callMcpHandler<unknown[]>("job.list");
@@ -93,6 +118,7 @@ export class JobRunner {
93
118
  name: row.name,
94
119
  description: row.description,
95
120
  skillCount: row.skill_count,
121
+ agentCount: row.agent_count,
96
122
  }));
97
123
  } catch (err) {
98
124
  log.debug(`Failed to list jobs: ${errorMessage(err)}`);
@@ -190,14 +216,25 @@ export class JobRunner {
190
216
  prompt += `\n`;
191
217
  }
192
218
 
193
- prompt += `## Instructions\n`;
219
+ prompt += `## Instructions\n\n`;
220
+ prompt += `### Step 1: Analyze and set up skills\n`;
194
221
  prompt += `For each capability the job needs:\n`;
195
222
  prompt += `1. Check if an existing skill covers it (use \`skill_search\` to check)\n`;
196
223
  prompt += `2. If no existing skill covers it, create a new one using \`skill_create\`\n`;
197
224
  prompt += `3. If an existing skill needs improvement, update it using \`skill_improve\`\n`;
198
225
  prompt += `4. Link all relevant skills to the job using \`skill_link_job\`\n\n`;
199
- prompt += `Be practical — only create skills that would genuinely help automate this job.\n`;
200
- prompt += `When done, summarize what skills were created, updated, or already existed.\n`;
226
+ prompt += `Be practical — only create skills that would genuinely help automate this job.\n\n`;
227
+
228
+ prompt += `### Step 2: Create the job's agent\n`;
229
+ prompt += `After setting up skills, create an agent for this job using \`job_assign_agent\`.\n`;
230
+ prompt += `The agent is the identity that will execute this job. Design it with:\n`;
231
+ prompt += `- **agent_name**: A concise, role-based name (e.g. "Daily Reporter", "Code Reviewer")\n`;
232
+ prompt += `- **agent_prompt**: A system prompt that defines the agent's behavior, expertise, and personality. `;
233
+ prompt += `Include the job's goals, preferred approach, and any domain knowledge the agent should have. `;
234
+ prompt += `Reference the linked skills as capabilities the agent can use.\n`;
235
+ prompt += `- **agent_bio**: A one-line description of what this agent does.\n\n`;
236
+
237
+ prompt += `When done, summarize what skills were created/updated and the agent that was assigned.\n`;
201
238
 
202
239
  return prompt;
203
240
  }
@@ -211,13 +248,41 @@ export class JobRunner {
211
248
  * chain them based on what it discovers at runtime.
212
249
  */
213
250
  buildJobPrompt(job: JobInfo, runId: string): string {
251
+ const primaryAgent = job.agents.length > 0
252
+ ? (job.agents.find((a) => a.role === "primary") || job.agents[0])
253
+ : null;
254
+ const parallelAgents = job.agents.filter(
255
+ (a) => a.role === "parallel" && a.agentId !== primaryAgent?.agentId,
256
+ );
214
257
  const effectiveDescription = job.jobPrompt || job.jobDescription;
215
258
  let prompt = `## Job: ${job.jobName}\n`;
216
259
  prompt += `*${effectiveDescription}*\n\n`;
217
260
  prompt += `**Run ID:** ${runId}\n\n`;
218
261
 
219
- prompt += `You are now acting as "${job.jobName}". `;
220
- prompt += `Your goal is to accomplish the objectives described above using the skills and tools available to you.\n\n`;
262
+ if (primaryAgent) {
263
+ prompt += `## Agent Identity\n`;
264
+ prompt += `You are **${primaryAgent.agentName}** (@${primaryAgent.agentUsername}).\n`;
265
+ if (primaryAgent.agentBio) {
266
+ prompt += `*${primaryAgent.agentBio}*\n`;
267
+ }
268
+ prompt += `\n### Agent Instructions\n`;
269
+ prompt += `${primaryAgent.agentPrompt}\n\n`;
270
+ } else {
271
+ prompt += `You are now acting as "${job.jobName}". `;
272
+ prompt += `Your goal is to accomplish the objectives described above using the skills and tools available to you.\n\n`;
273
+ }
274
+
275
+ if (parallelAgents.length > 0) {
276
+ prompt += `### Parallel Agents\n`;
277
+ prompt += `The following agents are running concurrently on this job. `;
278
+ prompt += `They handle their own tasks independently — coordinate to avoid duplicate work.\n\n`;
279
+ for (const pa of parallelAgents) {
280
+ prompt += `- **${pa.agentName}** (@${pa.agentUsername})`;
281
+ if (pa.agentBio) prompt += ` — ${pa.agentBio}`;
282
+ prompt += `\n`;
283
+ }
284
+ prompt += `\n`;
285
+ }
221
286
 
222
287
  prompt += `### Available Skills\n`;
223
288
  prompt += `These skills are your capabilities for this job. `;
@@ -17,7 +17,7 @@ export async function callMcpHandler<T = unknown>(
17
17
  ): Promise<T> {
18
18
  const config = getConfig();
19
19
  const token = overrideToken || getRawToken();
20
- const url = `${config.supabaseUrl}/functions/v1/mcp-handler`;
20
+ const url = `${config.supabaseUrl}/functions/v1/main-mcp-handler`;
21
21
 
22
22
  const response = await fetch(url, {
23
23
  method: "POST",
@@ -26,7 +26,7 @@ export function createJobTools(sessionId?: string, taskId?: string) {
26
26
  const jobs = await runner.listJobs();
27
27
  const available =
28
28
  jobs.length > 0
29
- ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills)`).join(", ")}`
29
+ ? `Available jobs: ${jobs.map((j) => `"${j.name}" (${j.skillCount} skills, ${j.agentCount} agents)`).join(", ")}`
30
30
  : "No jobs defined yet. Use skill_generate to create a job from a job description.";
31
31
  return textResponse(`Job "${args.job_name}" not found. ${available}`);
32
32
  }
@@ -72,13 +72,34 @@ export function createJobTools(sessionId?: string, taskId?: string) {
72
72
  }
73
73
  }
74
74
 
75
- const prompt = runner.buildJobPrompt(job, runId || "untracked");
75
+ const effectiveRunId = runId || "untracked";
76
+ const prompt = runner.buildJobPrompt(job, effectiveRunId);
77
+
78
+ // Build parallel agent prompts if any
79
+ const primaryAgentForFilter = job.agents.length > 0
80
+ ? (job.agents.find((p) => p.role === "primary") || job.agents[0])
81
+ : null;
82
+ const parallelAgents = job.agents.filter(
83
+ (a) => a.role === "parallel" && a.agentId !== primaryAgentForFilter?.agentId,
84
+ );
85
+
86
+ let fullResponse = prompt;
87
+ if (parallelAgents.length > 0) {
88
+ fullResponse += `\n\n---\n\n## Parallel Agent Dispatch\n`;
89
+ fullResponse += `The following parallel agents are assigned to this job. `;
90
+ fullResponse += `After you complete your primary tasks, review their responsibilities and execute any that haven't been covered:\n\n`;
91
+ for (const pa of parallelAgents) {
92
+ fullResponse += `### ${pa.agentName} (@${pa.agentUsername})\n`;
93
+ if (pa.agentBio) fullResponse += `*${pa.agentBio}*\n`;
94
+ fullResponse += `**Instructions:** ${pa.agentPrompt}\n\n`;
95
+ }
96
+ }
76
97
 
77
98
  log.info(
78
- `Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
99
+ `Job "${args.job_name}" started with ${job.skills.length} skills, ${job.agents.length} agents (run: ${effectiveRunId.slice(0, 8)})`
79
100
  );
80
101
 
81
- return textResponse(prompt);
102
+ return textResponse(fullResponse);
82
103
  })
83
104
  ),
84
105
  tool(
@@ -151,6 +172,89 @@ export function createJobTools(sessionId?: string, taskId?: string) {
151
172
  }
152
173
  )
153
174
  ),
175
+ tool(
176
+ "job_assign_agent",
177
+ "Create or update an agent for a job. The agent is a user with a specific prompt and identity that will execute this job.",
178
+ {
179
+ job_name: z.string().describe("Name of the job to assign the agent to"),
180
+ agent_name: z.string().describe("Display name for the agent (e.g. 'Daily Reporter')"),
181
+ agent_prompt: z.string().describe("The agent's system prompt that defines its behavior and personality"),
182
+ agent_bio: z.string().optional().describe("Short bio describing the agent's role"),
183
+ role: z.enum(["primary", "parallel"]).optional().describe("Agent role: 'primary' (default) or 'parallel' for concurrent execution"),
184
+ },
185
+ typedHandler(
186
+ async (args: {
187
+ job_name: string;
188
+ agent_name: string;
189
+ agent_prompt: string;
190
+ agent_bio?: string;
191
+ role?: string;
192
+ }) => {
193
+ try {
194
+ const result = await callMcpHandler<{
195
+ agent_id: string;
196
+ username: string;
197
+ job_id: string;
198
+ }>("job.save_agent", {
199
+ job_name: args.job_name,
200
+ agent_name: args.agent_name,
201
+ agent_prompt: args.agent_prompt,
202
+ agent_bio: args.agent_bio || null,
203
+ role: args.role || "primary",
204
+ });
205
+
206
+ if (!result) {
207
+ return textResponse(`Failed to create agent for job "${args.job_name}".`);
208
+ }
209
+
210
+ log.info(
211
+ `Agent "${result.username}" assigned to job "${args.job_name}" (${args.role || "primary"})`
212
+ );
213
+
214
+ let response = `## Agent Assigned to Job: ${args.job_name}\n\n`;
215
+ response += `- **Agent:** ${args.agent_name} (@${result.username})\n`;
216
+ response += `- **Role:** ${args.role || "primary"}\n`;
217
+ if (args.agent_bio) {
218
+ response += `- **Bio:** ${args.agent_bio}\n`;
219
+ }
220
+ response += `\nThe agent is now ready to execute this job.`;
221
+
222
+ return textResponse(response);
223
+ } catch (err) {
224
+ return textResponse(
225
+ `Failed to assign agent: ${err instanceof Error ? err.message : err}`
226
+ );
227
+ }
228
+ }
229
+ )
230
+ ),
231
+ tool(
232
+ "job_remove_agent",
233
+ "Remove (deactivate) an agent from a job.",
234
+ {
235
+ job_name: z.string().describe("Name of the job"),
236
+ agent_name: z.string().describe("Display name of the agent to remove"),
237
+ },
238
+ typedHandler(
239
+ async (args: { job_name: string; agent_name: string }) => {
240
+ try {
241
+ await callMcpHandler("job.remove_agent", {
242
+ job_name: args.job_name,
243
+ agent_name: args.agent_name,
244
+ });
245
+
246
+ log.info(`Agent "${args.agent_name}" removed from job "${args.job_name}"`);
247
+ return textResponse(
248
+ `Agent "${args.agent_name}" has been removed from job "${args.job_name}".`
249
+ );
250
+ } catch (err) {
251
+ return textResponse(
252
+ `Failed to remove agent: ${err instanceof Error ? err.message : err}`
253
+ );
254
+ }
255
+ }
256
+ )
257
+ ),
154
258
  tool(
155
259
  "job_status",
156
260
  "Check the status and run history of a job.",
@@ -171,7 +275,7 @@ export function createJobTools(sessionId?: string, taskId?: string) {
171
275
 
172
276
  let response = "## Your Jobs\n\n";
173
277
  for (const job of jobs) {
174
- response += `- **${job.name}** (${job.skillCount} skills): ${job.description.slice(0, 100)}\n`;
278
+ response += `- **${job.name}** (${job.skillCount} skills, ${job.agentCount} agents): ${job.description.slice(0, 100)}\n`;
175
279
  }
176
280
  response += "\nUse `job_run` to execute a job, or `job_schedule` to automate it.";
177
281
  return textResponse(response);
@@ -219,7 +219,24 @@ export class Orchestrator {
219
219
  log.debug(`Failed to link session to job run: ${linkErr}`);
220
220
  }
221
221
 
222
- const prompt = runner.buildJobPrompt(job, jobRun.id);
222
+ let prompt = runner.buildJobPrompt(job, jobRun.id);
223
+
224
+ // Append parallel agent instructions so the primary agent covers all responsibilities
225
+ const primaryAgent = job.agents.length > 0
226
+ ? (job.agents.find((a) => a.role === "primary") || job.agents[0])
227
+ : null;
228
+ const parallelAgents = job.agents.filter(
229
+ (a) => a.role === "parallel" && a.agentId !== primaryAgent?.agentId,
230
+ );
231
+ if (parallelAgents.length > 0) {
232
+ prompt += `\n\n---\n\n## Parallel Agent Responsibilities\n`;
233
+ prompt += `The following agents are also assigned. Execute their responsibilities as part of this run:\n\n`;
234
+ for (const pa of parallelAgents) {
235
+ prompt += `### ${pa.agentName}\n`;
236
+ if (pa.agentBio) prompt += `*${pa.agentBio}*\n`;
237
+ prompt += `**Instructions:** ${pa.agentPrompt}\n\n`;
238
+ }
239
+ }
223
240
 
224
241
  try {
225
242
  await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
@@ -49,6 +49,15 @@ export const SkillRowSchema = z.object({
49
49
 
50
50
  export type SkillRow = z.infer<typeof SkillRowSchema>;
51
51
 
52
+ export const JobAgentSchema = z.object({
53
+ agent_id: z.string(),
54
+ agent_username: z.string(),
55
+ agent_name: z.string().optional().default(""),
56
+ agent_bio: z.string().optional().nullable(),
57
+ agent_prompt: z.string(),
58
+ role: z.string().optional().default("primary"),
59
+ });
60
+
52
61
  export const JobRowSchema = z.object({
53
62
  job_id: z.string(),
54
63
  job_name: z.string(),
@@ -59,6 +68,7 @@ export const JobRowSchema = z.object({
59
68
  skill_description: z.string().optional().default(""),
60
69
  skill_emoji: z.string().optional().default(""),
61
70
  skill_content: z.string().optional().default(""),
71
+ agents: z.array(JobAgentSchema).optional().default([]),
62
72
  });
63
73
 
64
74
  export const JobListRowSchema = z.object({
@@ -67,6 +77,7 @@ export const JobListRowSchema = z.object({
67
77
  description: z.string().optional().default(""),
68
78
  prompt: z.string().optional().nullable(),
69
79
  skill_count: z.number().optional().default(0),
80
+ agent_count: z.number().optional().default(0),
70
81
  });
71
82
 
72
83
  export const JobRunRowSchema = z.object({
@@ -79,6 +79,8 @@ vi.mock("../../src/agent/memory/manager.js", () => ({
79
79
  MemoryManager: class MockMemoryManager {
80
80
  buildMemoryPrompt = vi.fn().mockResolvedValue("");
81
81
  remember = mockRemember;
82
+ setAgentContext = vi.fn();
83
+ clearAgentContext = vi.fn();
82
84
  },
83
85
  }));
84
86