opencode-swarm 5.0.10 → 5.1.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.
@@ -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[];
package/dist/index.js CHANGED
@@ -13696,7 +13696,8 @@ function resolveGuardrailsConfig(base, agentName) {
13696
13696
  return base;
13697
13697
  }
13698
13698
  const baseName = stripKnownSwarmPrefix(agentName);
13699
- const effectiveName = baseName === "unknown" ? ORCHESTRATOR_NAME : baseName;
13699
+ const builtInLookup = DEFAULT_AGENT_PROFILES[baseName];
13700
+ const effectiveName = builtInLookup ? baseName : ORCHESTRATOR_NAME;
13700
13701
  const builtIn = DEFAULT_AGENT_PROFILES[effectiveName];
13701
13702
  const userProfile = base.profiles?.[effectiveName] ?? base.profiles?.[baseName] ?? base.profiles?.[agentName];
13702
13703
  if (!builtIn && !userProfile) {
@@ -13833,6 +13834,30 @@ var DEFAULT_MODELS = {
13833
13834
  critic: "google/gemini-2.0-flash",
13834
13835
  default: "google/gemini-2.0-flash"
13835
13836
  };
13837
+ var DEFAULT_SCORING_CONFIG = {
13838
+ enabled: false,
13839
+ max_candidates: 100,
13840
+ weights: {
13841
+ phase: 1,
13842
+ current_task: 2,
13843
+ blocked_task: 1.5,
13844
+ recent_failure: 2.5,
13845
+ recent_success: 0.5,
13846
+ evidence_presence: 1,
13847
+ decision_recency: 1.5,
13848
+ dependency_proximity: 1
13849
+ },
13850
+ decision_decay: {
13851
+ mode: "exponential",
13852
+ half_life_hours: 24
13853
+ },
13854
+ token_ratios: {
13855
+ prose: 0.25,
13856
+ code: 0.4,
13857
+ markdown: 0.3,
13858
+ json: 0.35
13859
+ }
13860
+ };
13836
13861
  // src/config/plan-schema.ts
13837
13862
  var TaskStatusSchema = exports_external.enum([
13838
13863
  "pending",
@@ -16041,7 +16066,8 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
16041
16066
  warningIssued: false,
16042
16067
  warningReason: "",
16043
16068
  hardLimitHit: false,
16044
- lastSuccessTime: now
16069
+ lastSuccessTime: now,
16070
+ delegationActive: false
16045
16071
  };
16046
16072
  swarmState.agentSessions.set(sessionId, sessionState);
16047
16073
  }
@@ -16062,6 +16088,7 @@ function ensureAgentSession(sessionId, agentName) {
16062
16088
  session.warningReason = "";
16063
16089
  session.hardLimitHit = false;
16064
16090
  session.lastSuccessTime = now;
16091
+ session.delegationActive = false;
16065
16092
  }
16066
16093
  session.lastToolCallTime = now;
16067
16094
  return session;
@@ -16297,15 +16324,21 @@ function createContextBudgetHandler(config2) {
16297
16324
  function createDelegationTrackerHook(config2) {
16298
16325
  return async (input, _output) => {
16299
16326
  if (!input.agent || input.agent === "") {
16327
+ const session2 = swarmState.agentSessions.get(input.sessionID);
16328
+ if (session2) {
16329
+ session2.delegationActive = false;
16330
+ }
16300
16331
  return;
16301
16332
  }
16333
+ const agentName = input.agent;
16302
16334
  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) {
16335
+ swarmState.activeAgent.set(input.sessionID, agentName);
16336
+ const session = ensureAgentSession(input.sessionID, agentName);
16337
+ session.delegationActive = true;
16338
+ if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
16306
16339
  const entry = {
16307
16340
  from: previousAgent,
16308
- to: input.agent,
16341
+ to: agentName,
16309
16342
  timestamp: Date.now()
16310
16343
  };
16311
16344
  if (!swarmState.delegationChains.has(input.sessionID)) {
@@ -16536,7 +16569,69 @@ ${originalText}`;
16536
16569
  })
16537
16570
  };
16538
16571
  }
16572
+ // src/hooks/context-scoring.ts
16573
+ function calculateAgeFactor(ageHours, config2) {
16574
+ if (ageHours <= 0) {
16575
+ return 1;
16576
+ }
16577
+ if (config2.mode === "exponential") {
16578
+ return 2 ** (-ageHours / config2.half_life_hours);
16579
+ } else {
16580
+ const linearFactor = 1 - ageHours / (config2.half_life_hours * 2);
16581
+ return Math.max(0, linearFactor);
16582
+ }
16583
+ }
16584
+ function calculateBaseScore(candidate, weights, decayConfig) {
16585
+ const { kind, metadata } = candidate;
16586
+ const phase = kind === "phase" ? 1 : 0;
16587
+ const currentTask = metadata.isCurrentTask ? 1 : 0;
16588
+ const blockedTask = metadata.isBlockedTask ? 1 : 0;
16589
+ const recentFailure = metadata.hasFailure ? 1 : 0;
16590
+ const recentSuccess = metadata.hasSuccess ? 1 : 0;
16591
+ const evidencePresence = metadata.hasEvidence ? 1 : 0;
16592
+ let decisionRecency = 0;
16593
+ if (kind === "decision" && metadata.decisionAgeHours !== undefined) {
16594
+ decisionRecency = calculateAgeFactor(metadata.decisionAgeHours, decayConfig);
16595
+ }
16596
+ const dependencyProximity = 1 / (1 + (metadata.dependencyDepth ?? 0));
16597
+ 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;
16598
+ }
16599
+ function rankCandidates(candidates, config2) {
16600
+ if (!config2.enabled) {
16601
+ return candidates.map((c) => ({ ...c, score: 0 }));
16602
+ }
16603
+ if (candidates.length === 0) {
16604
+ return [];
16605
+ }
16606
+ const scored = candidates.map((candidate) => {
16607
+ const score = calculateBaseScore(candidate, config2.weights, config2.decision_decay);
16608
+ return { ...candidate, score };
16609
+ });
16610
+ scored.sort((a, b) => {
16611
+ if (b.score !== a.score) {
16612
+ return b.score - a.score;
16613
+ }
16614
+ if (b.priority !== a.priority) {
16615
+ return b.priority - a.priority;
16616
+ }
16617
+ return a.id.localeCompare(b.id);
16618
+ });
16619
+ return scored.slice(0, config2.max_candidates);
16620
+ }
16621
+
16539
16622
  // src/hooks/system-enhancer.ts
16623
+ function estimateContentType(text) {
16624
+ if (text.includes("```") || text.includes("function ") || text.includes("const ")) {
16625
+ return "code";
16626
+ }
16627
+ if (text.startsWith("{") || text.startsWith("[")) {
16628
+ return "json";
16629
+ }
16630
+ if (text.includes("#") || text.includes("*") || text.includes("- ")) {
16631
+ return "markdown";
16632
+ }
16633
+ return "prose";
16634
+ }
16540
16635
  function createSystemEnhancerHook(config2, directory) {
16541
16636
  const enabled = config2.hooks?.system_enhancer !== false;
16542
16637
  if (!enabled) {
@@ -16556,44 +16651,133 @@ function createSystemEnhancerHook(config2, directory) {
16556
16651
  const maxInjectionTokens = config2.context_budget?.max_injection_tokens ?? Number.POSITIVE_INFINITY;
16557
16652
  let injectedTokens = 0;
16558
16653
  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}`);
16654
+ const scoringEnabled = config2.context_budget?.scoring?.enabled === true;
16655
+ if (!scoringEnabled) {
16656
+ const plan2 = await loadPlan(directory);
16657
+ if (plan2 && plan2.migration_status !== "migration_failed") {
16658
+ const currentPhase2 = extractCurrentPhaseFromPlan(plan2);
16659
+ if (currentPhase2) {
16660
+ tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase2}`);
16661
+ }
16662
+ const currentTask2 = extractCurrentTaskFromPlan(plan2);
16663
+ if (currentTask2) {
16664
+ tryInject(`[SWARM CONTEXT] Current task: ${currentTask2}`);
16665
+ }
16666
+ } else {
16667
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
16668
+ if (planContent) {
16669
+ const currentPhase2 = extractCurrentPhase(planContent);
16670
+ if (currentPhase2) {
16671
+ tryInject(`[SWARM CONTEXT] Current phase: ${currentPhase2}`);
16672
+ }
16673
+ const currentTask2 = extractCurrentTask(planContent);
16674
+ if (currentTask2) {
16675
+ tryInject(`[SWARM CONTEXT] Current task: ${currentTask2}`);
16676
+ }
16677
+ }
16564
16678
  }
16565
- const currentTask = extractCurrentTaskFromPlan(plan);
16566
- if (currentTask) {
16567
- tryInject(`[SWARM CONTEXT] Current task: ${currentTask}`);
16679
+ if (contextContent) {
16680
+ const decisions = extractDecisions(contextContent, 200);
16681
+ if (decisions) {
16682
+ tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
16683
+ }
16684
+ if (config2.hooks?.agent_activity !== false && _input.sessionID) {
16685
+ const activeAgent = swarmState.activeAgent.get(_input.sessionID);
16686
+ if (activeAgent) {
16687
+ const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
16688
+ if (agentContext) {
16689
+ tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
16690
+ }
16691
+ }
16692
+ }
16568
16693
  }
16694
+ return;
16695
+ }
16696
+ const userScoringConfig = config2.context_budget?.scoring;
16697
+ const candidates = [];
16698
+ let idCounter = 0;
16699
+ const effectiveConfig = userScoringConfig?.weights ? {
16700
+ ...DEFAULT_SCORING_CONFIG,
16701
+ ...userScoringConfig,
16702
+ weights: userScoringConfig.weights
16703
+ } : DEFAULT_SCORING_CONFIG;
16704
+ const plan = await loadPlan(directory);
16705
+ let currentPhase = null;
16706
+ let currentTask = null;
16707
+ if (plan && plan.migration_status !== "migration_failed") {
16708
+ currentPhase = extractCurrentPhaseFromPlan(plan);
16709
+ currentTask = extractCurrentTaskFromPlan(plan);
16569
16710
  } else {
16570
16711
  const planContent = await readSwarmFileAsync(directory, "plan.md");
16571
16712
  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
- }
16713
+ currentPhase = extractCurrentPhase(planContent);
16714
+ currentTask = extractCurrentTask(planContent);
16580
16715
  }
16581
16716
  }
16717
+ if (currentPhase) {
16718
+ const text = `[SWARM CONTEXT] Current phase: ${currentPhase}`;
16719
+ candidates.push({
16720
+ id: `candidate-${idCounter++}`,
16721
+ kind: "phase",
16722
+ text,
16723
+ tokens: estimateTokens(text),
16724
+ priority: 1,
16725
+ metadata: { contentType: estimateContentType(text) }
16726
+ });
16727
+ }
16728
+ if (currentTask) {
16729
+ const text = `[SWARM CONTEXT] Current task: ${currentTask}`;
16730
+ candidates.push({
16731
+ id: `candidate-${idCounter++}`,
16732
+ kind: "task",
16733
+ text,
16734
+ tokens: estimateTokens(text),
16735
+ priority: 2,
16736
+ metadata: {
16737
+ contentType: estimateContentType(text),
16738
+ isCurrentTask: true
16739
+ }
16740
+ });
16741
+ }
16582
16742
  if (contextContent) {
16583
16743
  const decisions = extractDecisions(contextContent, 200);
16584
16744
  if (decisions) {
16585
- tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
16745
+ const text = `[SWARM CONTEXT] Key decisions: ${decisions}`;
16746
+ candidates.push({
16747
+ id: `candidate-${idCounter++}`,
16748
+ kind: "decision",
16749
+ text,
16750
+ tokens: estimateTokens(text),
16751
+ priority: 3,
16752
+ metadata: { contentType: estimateContentType(text) }
16753
+ });
16586
16754
  }
16587
16755
  if (config2.hooks?.agent_activity !== false && _input.sessionID) {
16588
16756
  const activeAgent = swarmState.activeAgent.get(_input.sessionID);
16589
16757
  if (activeAgent) {
16590
16758
  const agentContext = extractAgentContext(contextContent, activeAgent, config2.hooks?.agent_awareness_max_chars ?? 300);
16591
16759
  if (agentContext) {
16592
- tryInject(`[SWARM AGENT CONTEXT] ${agentContext}`);
16760
+ const text = `[SWARM AGENT CONTEXT] ${agentContext}`;
16761
+ candidates.push({
16762
+ id: `candidate-${idCounter++}`,
16763
+ kind: "agent_context",
16764
+ text,
16765
+ tokens: estimateTokens(text),
16766
+ priority: 4,
16767
+ metadata: { contentType: estimateContentType(text) }
16768
+ });
16593
16769
  }
16594
16770
  }
16595
16771
  }
16596
16772
  }
16773
+ const ranked = rankCandidates(candidates, effectiveConfig);
16774
+ for (const candidate of ranked) {
16775
+ if (injectedTokens + candidate.tokens > maxInjectionTokens) {
16776
+ continue;
16777
+ }
16778
+ output.system.push(candidate.text);
16779
+ injectedTokens += candidate.tokens;
16780
+ }
16597
16781
  } catch (error49) {
16598
16782
  warn("System enhancer failed:", error49);
16599
16783
  }
@@ -29394,6 +29578,12 @@ var OpenCodeSwarm = async (ctx) => {
29394
29578
  if (!swarmState.activeAgent.has(input.sessionID)) {
29395
29579
  swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29396
29580
  }
29581
+ const session = swarmState.agentSessions.get(input.sessionID);
29582
+ const activeAgent = swarmState.activeAgent.get(input.sessionID);
29583
+ if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME && session.delegationActive === false) {
29584
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29585
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
29586
+ }
29397
29587
  await guardrailsHooks.toolBefore(input, output);
29398
29588
  await safeHook(activityHooks.toolBefore)(input, output);
29399
29589
  },
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.0",
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",