opencode-swarm 5.0.10 → 5.1.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.
@@ -20,6 +20,8 @@ export declare const HooksConfigSchema: z.ZodObject<{
20
20
  agent_activity: z.ZodDefault<z.ZodBoolean>;
21
21
  delegation_tracker: z.ZodDefault<z.ZodBoolean>;
22
22
  agent_awareness_max_chars: z.ZodDefault<z.ZodNumber>;
23
+ delegation_gate: z.ZodDefault<z.ZodBoolean>;
24
+ delegation_max_chars: z.ZodDefault<z.ZodNumber>;
23
25
  }, z.core.$strip>;
24
26
  export type HooksConfig = z.infer<typeof HooksConfigSchema>;
25
27
  export declare const ScoringWeightsSchema: z.ZodObject<{
@@ -200,6 +202,8 @@ export declare const PluginConfigSchema: z.ZodObject<{
200
202
  agent_activity: z.ZodDefault<z.ZodBoolean>;
201
203
  delegation_tracker: z.ZodDefault<z.ZodBoolean>;
202
204
  agent_awareness_max_chars: z.ZodDefault<z.ZodNumber>;
205
+ delegation_gate: z.ZodDefault<z.ZodBoolean>;
206
+ delegation_max_chars: z.ZodDefault<z.ZodNumber>;
203
207
  }, z.core.$strip>>;
204
208
  context_budget: z.ZodOptional<z.ZodObject<{
205
209
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Context Scoring Utility
3
+ *
4
+ * Pure scoring/ranking helpers for context injection budget.
5
+ * Implements deterministic, reproducible candidate ranking based on configurable weights.
6
+ */
7
+ export type ContentType = 'prose' | 'code' | 'markdown' | 'json';
8
+ export type CandidateKind = 'phase' | 'task' | 'decision' | 'evidence' | 'agent_context';
9
+ export interface ContextCandidate {
10
+ id: string;
11
+ kind: CandidateKind;
12
+ text: string;
13
+ tokens: number;
14
+ priority: number;
15
+ metadata: {
16
+ contentType: ContentType;
17
+ dependencyDepth?: number;
18
+ decisionAgeHours?: number;
19
+ isCurrentTask?: boolean;
20
+ isBlockedTask?: boolean;
21
+ hasFailure?: boolean;
22
+ hasSuccess?: boolean;
23
+ hasEvidence?: boolean;
24
+ };
25
+ }
26
+ export interface RankedCandidate extends ContextCandidate {
27
+ score: number;
28
+ }
29
+ export interface ScoringWeights {
30
+ phase: number;
31
+ current_task: number;
32
+ blocked_task: number;
33
+ recent_failure: number;
34
+ recent_success: number;
35
+ evidence_presence: number;
36
+ decision_recency: number;
37
+ dependency_proximity: number;
38
+ }
39
+ export interface DecisionDecayConfig {
40
+ mode: 'linear' | 'exponential';
41
+ half_life_hours: number;
42
+ }
43
+ export interface TokenRatios {
44
+ prose: number;
45
+ code: number;
46
+ markdown: number;
47
+ json: number;
48
+ }
49
+ export interface ScoringConfig {
50
+ enabled: boolean;
51
+ max_candidates: number;
52
+ weights: ScoringWeights;
53
+ decision_decay: DecisionDecayConfig;
54
+ token_ratios: TokenRatios;
55
+ }
56
+ /**
57
+ * Rank context candidates by importance score.
58
+ *
59
+ * Scoring formula:
60
+ * - base_score = sum of (weight * feature_flag)
61
+ * - For items with dependency depth: adjusted_score = base_score / (1 + depth)
62
+ * - For decisions with age: age_factor = 2^(-age_hours / half_life_hours), score = decision_recency * age_factor
63
+ *
64
+ * Tie-breaker: score desc → priority desc → id asc (stable sort)
65
+ *
66
+ * @param candidates - Array of context candidates
67
+ * @param config - Scoring configuration
68
+ * @returns Ranked candidates (truncated to max_candidates, original order if disabled)
69
+ */
70
+ export declare function rankCandidates(candidates: ContextCandidate[], config: ScoringConfig): RankedCandidate[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Delegation Gate Hook
3
+ *
4
+ * Warns the architect when coder delegations are too large or batched.
5
+ * Uses experimental.chat.messages.transform to provide non-blocking guidance.
6
+ */
7
+ import type { PluginConfig } from '../config';
8
+ interface MessageInfo {
9
+ role: string;
10
+ agent?: string;
11
+ sessionID?: string;
12
+ }
13
+ interface MessagePart {
14
+ type: string;
15
+ text?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ interface MessageWithParts {
19
+ info: MessageInfo;
20
+ parts: MessagePart[];
21
+ }
22
+ /**
23
+ * Creates the experimental.chat.messages.transform hook for delegation gating.
24
+ * Inspects coder delegations and warns when tasks are oversized or batched.
25
+ */
26
+ export declare function createDelegationGateHook(config: PluginConfig): (input: Record<string, never>, output: {
27
+ messages?: MessageWithParts[];
28
+ }) => Promise<void>;
29
+ export {};
@@ -2,6 +2,7 @@ export { createAgentActivityHooks } from './agent-activity';
2
2
  export { createCompactionCustomizerHook } from './compaction-customizer';
3
3
  export { createContextBudgetHandler } from './context-budget';
4
4
  export { createDelegationTrackerHook } from './delegation-tracker';
5
+ export { createDelegationGateHook } from './delegation-gate';
5
6
  export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
6
7
  export { createGuardrailsHooks } from './guardrails';
7
8
  export { createPipelineTrackerHook } from './pipeline-tracker';
package/dist/index.js CHANGED
@@ -13578,7 +13578,9 @@ var HooksConfigSchema = exports_external.object({
13578
13578
  compaction: exports_external.boolean().default(true),
13579
13579
  agent_activity: exports_external.boolean().default(true),
13580
13580
  delegation_tracker: exports_external.boolean().default(false),
13581
- agent_awareness_max_chars: exports_external.number().min(50).max(2000).default(300)
13581
+ agent_awareness_max_chars: exports_external.number().min(50).max(2000).default(300),
13582
+ delegation_gate: exports_external.boolean().default(true),
13583
+ delegation_max_chars: exports_external.number().min(500).max(20000).default(4000)
13582
13584
  });
13583
13585
  var ScoringWeightsSchema = exports_external.object({
13584
13586
  phase: exports_external.number().min(0).max(5).default(1),
@@ -13696,7 +13698,8 @@ function resolveGuardrailsConfig(base, agentName) {
13696
13698
  return base;
13697
13699
  }
13698
13700
  const baseName = stripKnownSwarmPrefix(agentName);
13699
- const effectiveName = baseName === "unknown" ? ORCHESTRATOR_NAME : baseName;
13701
+ const builtInLookup = DEFAULT_AGENT_PROFILES[baseName];
13702
+ const effectiveName = builtInLookup ? baseName : ORCHESTRATOR_NAME;
13700
13703
  const builtIn = DEFAULT_AGENT_PROFILES[effectiveName];
13701
13704
  const userProfile = base.profiles?.[effectiveName] ?? base.profiles?.[baseName] ?? base.profiles?.[agentName];
13702
13705
  if (!builtIn && !userProfile) {
@@ -13833,6 +13836,30 @@ var DEFAULT_MODELS = {
13833
13836
  critic: "google/gemini-2.0-flash",
13834
13837
  default: "google/gemini-2.0-flash"
13835
13838
  };
13839
+ var DEFAULT_SCORING_CONFIG = {
13840
+ enabled: false,
13841
+ max_candidates: 100,
13842
+ weights: {
13843
+ phase: 1,
13844
+ current_task: 2,
13845
+ blocked_task: 1.5,
13846
+ recent_failure: 2.5,
13847
+ recent_success: 0.5,
13848
+ evidence_presence: 1,
13849
+ decision_recency: 1.5,
13850
+ dependency_proximity: 1
13851
+ },
13852
+ decision_decay: {
13853
+ mode: "exponential",
13854
+ half_life_hours: 24
13855
+ },
13856
+ token_ratios: {
13857
+ prose: 0.25,
13858
+ code: 0.4,
13859
+ markdown: 0.3,
13860
+ json: 0.35
13861
+ }
13862
+ };
13836
13863
  // src/config/plan-schema.ts
13837
13864
  var TaskStatusSchema = exports_external.enum([
13838
13865
  "pending",
@@ -16041,7 +16068,8 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
16041
16068
  warningIssued: false,
16042
16069
  warningReason: "",
16043
16070
  hardLimitHit: false,
16044
- lastSuccessTime: now
16071
+ lastSuccessTime: now,
16072
+ delegationActive: false
16045
16073
  };
16046
16074
  swarmState.agentSessions.set(sessionId, sessionState);
16047
16075
  }
@@ -16062,6 +16090,7 @@ function ensureAgentSession(sessionId, agentName) {
16062
16090
  session.warningReason = "";
16063
16091
  session.hardLimitHit = false;
16064
16092
  session.lastSuccessTime = now;
16093
+ session.delegationActive = false;
16065
16094
  }
16066
16095
  session.lastToolCallTime = now;
16067
16096
  return session;
@@ -16297,15 +16326,21 @@ function createContextBudgetHandler(config2) {
16297
16326
  function createDelegationTrackerHook(config2) {
16298
16327
  return async (input, _output) => {
16299
16328
  if (!input.agent || input.agent === "") {
16329
+ const session2 = swarmState.agentSessions.get(input.sessionID);
16330
+ if (session2) {
16331
+ session2.delegationActive = false;
16332
+ }
16300
16333
  return;
16301
16334
  }
16335
+ const agentName = input.agent;
16302
16336
  const previousAgent = swarmState.activeAgent.get(input.sessionID);
16303
- swarmState.activeAgent.set(input.sessionID, input.agent);
16304
- ensureAgentSession(input.sessionID, input.agent);
16305
- if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== input.agent) {
16337
+ swarmState.activeAgent.set(input.sessionID, agentName);
16338
+ const session = ensureAgentSession(input.sessionID, agentName);
16339
+ session.delegationActive = true;
16340
+ if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
16306
16341
  const entry = {
16307
16342
  from: previousAgent,
16308
- to: input.agent,
16343
+ to: agentName,
16309
16344
  timestamp: Date.now()
16310
16345
  };
16311
16346
  if (!swarmState.delegationChains.has(input.sessionID)) {
@@ -16317,6 +16352,70 @@ function createDelegationTrackerHook(config2) {
16317
16352
  }
16318
16353
  };
16319
16354
  }
16355
+ // src/hooks/delegation-gate.ts
16356
+ function createDelegationGateHook(config2) {
16357
+ const enabled = config2.hooks?.delegation_gate !== false;
16358
+ const delegationMaxChars = config2.hooks?.delegation_max_chars ?? 4000;
16359
+ if (!enabled) {
16360
+ return async (_input, _output) => {};
16361
+ }
16362
+ return async (_input, output) => {
16363
+ const messages = output?.messages;
16364
+ if (!messages || messages.length === 0)
16365
+ return;
16366
+ let lastUserMessageIndex = -1;
16367
+ for (let i = messages.length - 1;i >= 0; i--) {
16368
+ if (messages[i]?.info?.role === "user") {
16369
+ lastUserMessageIndex = i;
16370
+ break;
16371
+ }
16372
+ }
16373
+ if (lastUserMessageIndex === -1)
16374
+ return;
16375
+ const lastUserMessage = messages[lastUserMessageIndex];
16376
+ if (!lastUserMessage?.parts)
16377
+ return;
16378
+ const agent = lastUserMessage.info?.agent;
16379
+ const strippedAgent = agent ? stripKnownSwarmPrefix(agent) : undefined;
16380
+ if (strippedAgent && strippedAgent !== "architect")
16381
+ return;
16382
+ const textPartIndex = lastUserMessage.parts.findIndex((p) => p?.type === "text" && p.text !== undefined);
16383
+ if (textPartIndex === -1)
16384
+ return;
16385
+ const textPart = lastUserMessage.parts[textPartIndex];
16386
+ const text = textPart.text ?? "";
16387
+ const coderDelegationPattern = /(?:^|\n)\s*(?:\w+_)?coder\s*\n\s*TASK:/i;
16388
+ if (!coderDelegationPattern.test(text))
16389
+ return;
16390
+ const warnings = [];
16391
+ if (text.length > delegationMaxChars) {
16392
+ warnings.push(`Delegation exceeds recommended size (${text.length} chars, limit ${delegationMaxChars}). Consider splitting into smaller tasks.`);
16393
+ }
16394
+ const fileMatches = text.match(/^FILE:/gm);
16395
+ if (fileMatches && fileMatches.length > 1) {
16396
+ warnings.push(`Multiple FILE: directives detected (${fileMatches.length}). Each coder task should target ONE file.`);
16397
+ }
16398
+ const taskMatches = text.match(/^TASK:/gm);
16399
+ if (taskMatches && taskMatches.length > 1) {
16400
+ warnings.push(`Multiple TASK: sections detected (${taskMatches.length}). Send ONE task per coder call.`);
16401
+ }
16402
+ const batchingPattern = /\b(?:and also|then also|additionally|as well as|along with)\b/gi;
16403
+ const batchingMatches = text.match(batchingPattern);
16404
+ if (batchingMatches && batchingMatches.length > 0) {
16405
+ warnings.push("Batching language detected. Break compound objectives into separate coder calls.");
16406
+ }
16407
+ if (warnings.length === 0)
16408
+ return;
16409
+ const warningText = `[\u26A0\uFE0F DELEGATION GATE: Your coder delegation may be too complex. Issues:
16410
+ ${warnings.join(`
16411
+ `)}
16412
+ Split into smaller, atomic tasks for better results.]`;
16413
+ const originalText = textPart.text ?? "";
16414
+ textPart.text = `${warningText}
16415
+
16416
+ ${originalText}`;
16417
+ };
16418
+ }
16320
16419
  // src/hooks/guardrails.ts
16321
16420
  function createGuardrailsHooks(config2) {
16322
16421
  if (config2.enabled === false) {
@@ -16328,6 +16427,11 @@ function createGuardrailsHooks(config2) {
16328
16427
  }
16329
16428
  return {
16330
16429
  toolBefore: async (input, output) => {
16430
+ const rawActiveAgent = swarmState.activeAgent.get(input.sessionID);
16431
+ const strippedAgent = rawActiveAgent ? stripKnownSwarmPrefix(rawActiveAgent) : undefined;
16432
+ if (strippedAgent === ORCHESTRATOR_NAME) {
16433
+ return;
16434
+ }
16331
16435
  const agentName = swarmState.activeAgent.get(input.sessionID);
16332
16436
  const session = ensureAgentSession(input.sessionID, agentName);
16333
16437
  const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
@@ -16536,7 +16640,69 @@ ${originalText}`;
16536
16640
  })
16537
16641
  };
16538
16642
  }
16643
+ // src/hooks/context-scoring.ts
16644
+ function calculateAgeFactor(ageHours, config2) {
16645
+ if (ageHours <= 0) {
16646
+ return 1;
16647
+ }
16648
+ if (config2.mode === "exponential") {
16649
+ return 2 ** (-ageHours / config2.half_life_hours);
16650
+ } else {
16651
+ const linearFactor = 1 - ageHours / (config2.half_life_hours * 2);
16652
+ return Math.max(0, linearFactor);
16653
+ }
16654
+ }
16655
+ function calculateBaseScore(candidate, weights, decayConfig) {
16656
+ const { kind, metadata } = candidate;
16657
+ const phase = kind === "phase" ? 1 : 0;
16658
+ const currentTask = metadata.isCurrentTask ? 1 : 0;
16659
+ const blockedTask = metadata.isBlockedTask ? 1 : 0;
16660
+ const recentFailure = metadata.hasFailure ? 1 : 0;
16661
+ const recentSuccess = metadata.hasSuccess ? 1 : 0;
16662
+ const evidencePresence = metadata.hasEvidence ? 1 : 0;
16663
+ let decisionRecency = 0;
16664
+ if (kind === "decision" && metadata.decisionAgeHours !== undefined) {
16665
+ decisionRecency = calculateAgeFactor(metadata.decisionAgeHours, decayConfig);
16666
+ }
16667
+ const dependencyProximity = 1 / (1 + (metadata.dependencyDepth ?? 0));
16668
+ return weights.phase * phase + weights.current_task * currentTask + weights.blocked_task * blockedTask + weights.recent_failure * recentFailure + weights.recent_success * recentSuccess + weights.evidence_presence * evidencePresence + weights.decision_recency * decisionRecency + weights.dependency_proximity * dependencyProximity;
16669
+ }
16670
+ function rankCandidates(candidates, config2) {
16671
+ if (!config2.enabled) {
16672
+ return candidates.map((c) => ({ ...c, score: 0 }));
16673
+ }
16674
+ if (candidates.length === 0) {
16675
+ return [];
16676
+ }
16677
+ const scored = candidates.map((candidate) => {
16678
+ const score = calculateBaseScore(candidate, config2.weights, config2.decision_decay);
16679
+ return { ...candidate, score };
16680
+ });
16681
+ scored.sort((a, b) => {
16682
+ if (b.score !== a.score) {
16683
+ return b.score - a.score;
16684
+ }
16685
+ if (b.priority !== a.priority) {
16686
+ return b.priority - a.priority;
16687
+ }
16688
+ return a.id.localeCompare(b.id);
16689
+ });
16690
+ return scored.slice(0, config2.max_candidates);
16691
+ }
16692
+
16539
16693
  // src/hooks/system-enhancer.ts
16694
+ function estimateContentType(text) {
16695
+ if (text.includes("```") || text.includes("function ") || text.includes("const ")) {
16696
+ return "code";
16697
+ }
16698
+ if (text.startsWith("{") || text.startsWith("[")) {
16699
+ return "json";
16700
+ }
16701
+ if (text.includes("#") || text.includes("*") || text.includes("- ")) {
16702
+ return "markdown";
16703
+ }
16704
+ return "prose";
16705
+ }
16540
16706
  function createSystemEnhancerHook(config2, directory) {
16541
16707
  const enabled = config2.hooks?.system_enhancer !== false;
16542
16708
  if (!enabled) {
@@ -16556,44 +16722,133 @@ function createSystemEnhancerHook(config2, directory) {
16556
16722
  const maxInjectionTokens = config2.context_budget?.max_injection_tokens ?? Number.POSITIVE_INFINITY;
16557
16723
  let injectedTokens = 0;
16558
16724
  const contextContent = await readSwarmFileAsync(directory, "context.md");
16559
- const plan = await loadPlan(directory);
16560
- if (plan && plan.migration_status !== "migration_failed") {
16561
- const currentPhase = extractCurrentPhaseFromPlan(plan);
16562
- if (currentPhase) {
16563
- tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
16725
+ const scoringEnabled = config2.context_budget?.scoring?.enabled === true;
16726
+ if (!scoringEnabled) {
16727
+ const plan2 = await loadPlan(directory);
16728
+ if (plan2 && plan2.migration_status !== "migration_failed") {
16729
+ const currentPhase2 = extractCurrentPhaseFromPlan(plan2);
16730
+ if (currentPhase2) {
16731
+ tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase2}`);
16732
+ }
16733
+ const currentTask2 = extractCurrentTaskFromPlan(plan2);
16734
+ if (currentTask2) {
16735
+ tryInject(`[SWARM CONTEXT] Current task: ${currentTask2}`);
16736
+ }
16737
+ } else {
16738
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
16739
+ if (planContent) {
16740
+ const currentPhase2 = extractCurrentPhase(planContent);
16741
+ if (currentPhase2) {
16742
+ tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase2}`);
16743
+ }
16744
+ const currentTask2 = extractCurrentTask(planContent);
16745
+ if (currentTask2) {
16746
+ tryInject(`[SWARM CONTEXT] Current task: ${currentTask2}`);
16747
+ }
16748
+ }
16564
16749
  }
16565
- const currentTask = extractCurrentTaskFromPlan(plan);
16566
- if (currentTask) {
16567
- tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
16750
+ if (contextContent) {
16751
+ const decisions = extractDecisions(contextContent, 200);
16752
+ if (decisions) {
16753
+ tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
16754
+ }
16755
+ if (config2.hooks?.agent_activity !== false && _input.sessionID) {
16756
+ const activeAgent = swarmState.activeAgent.get(_input.sessionID);
16757
+ if (activeAgent) {
16758
+ const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
16759
+ if (agentContext) {
16760
+ tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
16761
+ }
16762
+ }
16763
+ }
16568
16764
  }
16765
+ return;
16766
+ }
16767
+ const userScoringConfig = config2.context_budget?.scoring;
16768
+ const candidates = [];
16769
+ let idCounter = 0;
16770
+ const effectiveConfig = userScoringConfig?.weights ? {
16771
+ ...DEFAULT_SCORING_CONFIG,
16772
+ ...userScoringConfig,
16773
+ weights: userScoringConfig.weights
16774
+ } : DEFAULT_SCORING_CONFIG;
16775
+ const plan = await loadPlan(directory);
16776
+ let currentPhase = null;
16777
+ let currentTask = null;
16778
+ if (plan && plan.migration_status !== "migration_failed") {
16779
+ currentPhase = extractCurrentPhaseFromPlan(plan);
16780
+ currentTask = extractCurrentTaskFromPlan(plan);
16569
16781
  } else {
16570
16782
  const planContent = await readSwarmFileAsync(directory, "plan.md");
16571
16783
  if (planContent) {
16572
- const currentPhase = extractCurrentPhase(planContent);
16573
- if (currentPhase) {
16574
- tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase}`);
16575
- }
16576
- const currentTask = extractCurrentTask(planContent);
16577
- if (currentTask) {
16578
- tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
16579
- }
16784
+ currentPhase = extractCurrentPhase(planContent);
16785
+ currentTask = extractCurrentTask(planContent);
16580
16786
  }
16581
16787
  }
16788
+ if (currentPhase) {
16789
+ const text = `[SWARM CONTEXT] Current phase: ${currentPhase}`;
16790
+ candidates.push({
16791
+ id: `candidate-${idCounter++}`,
16792
+ kind: "phase",
16793
+ text,
16794
+ tokens: estimateTokens(text),
16795
+ priority: 1,
16796
+ metadata: { contentType: estimateContentType(text) }
16797
+ });
16798
+ }
16799
+ if (currentTask) {
16800
+ const text = `[SWARM CONTEXT] Current task: ${currentTask}`;
16801
+ candidates.push({
16802
+ id: `candidate-${idCounter++}`,
16803
+ kind: "task",
16804
+ text,
16805
+ tokens: estimateTokens(text),
16806
+ priority: 2,
16807
+ metadata: {
16808
+ contentType: estimateContentType(text),
16809
+ isCurrentTask: true
16810
+ }
16811
+ });
16812
+ }
16582
16813
  if (contextContent) {
16583
16814
  const decisions = extractDecisions(contextContent, 200);
16584
16815
  if (decisions) {
16585
- tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
16816
+ const text = `[SWARM CONTEXT] Key decisions: ${decisions}`;
16817
+ candidates.push({
16818
+ id: `candidate-${idCounter++}`,
16819
+ kind: "decision",
16820
+ text,
16821
+ tokens: estimateTokens(text),
16822
+ priority: 3,
16823
+ metadata: { contentType: estimateContentType(text) }
16824
+ });
16586
16825
  }
16587
16826
  if (config2.hooks?.agent_activity !== false && _input.sessionID) {
16588
16827
  const activeAgent = swarmState.activeAgent.get(_input.sessionID);
16589
16828
  if (activeAgent) {
16590
16829
  const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
16591
16830
  if (agentContext) {
16592
- tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
16831
+ const text = `[SWARM AGENT CONTEXT] ${agentContext}`;
16832
+ candidates.push({
16833
+ id: `candidate-${idCounter++}`,
16834
+ kind: "agent_context",
16835
+ text,
16836
+ tokens: estimateTokens(text),
16837
+ priority: 4,
16838
+ metadata: { contentType: estimateContentType(text) }
16839
+ });
16593
16840
  }
16594
16841
  }
16595
16842
  }
16596
16843
  }
16844
+ const ranked = rankCandidates(candidates, effectiveConfig);
16845
+ for (const candidate of ranked) {
16846
+ if (injectedTokens + candidate.tokens > maxInjectionTokens) {
16847
+ continue;
16848
+ }
16849
+ output.system.push(candidate.text);
16850
+ injectedTokens += candidate.tokens;
16851
+ }
16597
16852
  } catch (error49) {
16598
16853
  warn("System enhancer failed:", error49);
16599
16854
  }
@@ -29338,6 +29593,7 @@ var OpenCodeSwarm = async (ctx) => {
29338
29593
  const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
29339
29594
  const activityHooks = createAgentActivityHooks(config3, ctx.directory);
29340
29595
  const delegationHandler = createDelegationTrackerHook(config3);
29596
+ const delegationGateHandler = createDelegationGateHook(config3);
29341
29597
  const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
29342
29598
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
29343
29599
  log("Plugin initialized", {
@@ -29385,7 +29641,8 @@ var OpenCodeSwarm = async (ctx) => {
29385
29641
  "experimental.chat.messages.transform": composeHandlers(...[
29386
29642
  pipelineHook["experimental.chat.messages.transform"],
29387
29643
  contextBudgetHandler,
29388
- guardrailsHooks.messagesTransform
29644
+ guardrailsHooks.messagesTransform,
29645
+ delegationGateHandler
29389
29646
  ].filter((fn) => Boolean(fn))),
29390
29647
  "experimental.chat.system.transform": systemEnhancerHook["experimental.chat.system.transform"],
29391
29648
  "experimental.session.compacting": compactionHook["experimental.session.compacting"],
@@ -29394,6 +29651,12 @@ var OpenCodeSwarm = async (ctx) => {
29394
29651
  if (!swarmState.activeAgent.has(input.sessionID)) {
29395
29652
  swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29396
29653
  }
29654
+ const session = swarmState.agentSessions.get(input.sessionID);
29655
+ const activeAgent = swarmState.activeAgent.get(input.sessionID);
29656
+ if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME && session.delegationActive === false) {
29657
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29658
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
29659
+ }
29397
29660
  await guardrailsHooks.toolBefore(input, output);
29398
29661
  await safeHook(activityHooks.toolBefore)(input, output);
29399
29662
  },
package/dist/state.d.ts CHANGED
@@ -61,6 +61,8 @@ export interface AgentSessionState {
61
61
  hardLimitHit: boolean;
62
62
  /** Timestamp of most recent SUCCESSFUL tool call (for idle timeout) */
63
63
  lastSuccessTime: number;
64
+ /** Whether active delegation is in progress for this session */
65
+ delegationActive: boolean;
64
66
  }
65
67
  /**
66
68
  * Singleton state object for sharing data across hooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "5.0.10",
3
+ "version": "5.1.1",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",