opencode-swarm 4.4.0 → 5.0.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/README.md +155 -28
- package/dist/agents/index.d.ts +6 -0
- package/dist/commands/agents.d.ts +2 -1
- package/dist/commands/archive.d.ts +5 -0
- package/dist/commands/diagnose.d.ts +5 -0
- package/dist/commands/evidence.d.ts +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/reset.d.ts +6 -0
- package/dist/config/evidence-schema.d.ts +353 -0
- package/dist/config/index.d.ts +7 -3
- package/dist/config/plan-schema.d.ts +124 -0
- package/dist/config/schema.d.ts +63 -0
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/manager.d.ts +38 -0
- package/dist/guardrails.js +178 -0
- package/dist/hooks/extractors.d.ts +13 -0
- package/dist/hooks/guardrails.d.ts +52 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/index.js +1385 -105
- package/dist/plan/index.d.ts +1 -0
- package/dist/plan/manager.d.ts +33 -0
- package/dist/state.d.ts +44 -0
- package/dist/state.js +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const TaskStatusSchema: z.ZodEnum<{
|
|
3
|
+
pending: "pending";
|
|
4
|
+
in_progress: "in_progress";
|
|
5
|
+
completed: "completed";
|
|
6
|
+
blocked: "blocked";
|
|
7
|
+
}>;
|
|
8
|
+
export type TaskStatus = z.infer<typeof TaskStatusSchema>;
|
|
9
|
+
export declare const TaskSizeSchema: z.ZodEnum<{
|
|
10
|
+
small: "small";
|
|
11
|
+
medium: "medium";
|
|
12
|
+
large: "large";
|
|
13
|
+
}>;
|
|
14
|
+
export type TaskSize = z.infer<typeof TaskSizeSchema>;
|
|
15
|
+
export declare const PhaseStatusSchema: z.ZodEnum<{
|
|
16
|
+
pending: "pending";
|
|
17
|
+
in_progress: "in_progress";
|
|
18
|
+
blocked: "blocked";
|
|
19
|
+
complete: "complete";
|
|
20
|
+
}>;
|
|
21
|
+
export type PhaseStatus = z.infer<typeof PhaseStatusSchema>;
|
|
22
|
+
export declare const MigrationStatusSchema: z.ZodEnum<{
|
|
23
|
+
native: "native";
|
|
24
|
+
migrated: "migrated";
|
|
25
|
+
migration_failed: "migration_failed";
|
|
26
|
+
}>;
|
|
27
|
+
export type MigrationStatus = z.infer<typeof MigrationStatusSchema>;
|
|
28
|
+
export declare const TaskSchema: z.ZodObject<{
|
|
29
|
+
id: z.ZodString;
|
|
30
|
+
phase: z.ZodNumber;
|
|
31
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
32
|
+
pending: "pending";
|
|
33
|
+
in_progress: "in_progress";
|
|
34
|
+
completed: "completed";
|
|
35
|
+
blocked: "blocked";
|
|
36
|
+
}>>;
|
|
37
|
+
size: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
small: "small";
|
|
39
|
+
medium: "medium";
|
|
40
|
+
large: "large";
|
|
41
|
+
}>>;
|
|
42
|
+
description: z.ZodString;
|
|
43
|
+
depends: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
44
|
+
acceptance: z.ZodOptional<z.ZodString>;
|
|
45
|
+
files_touched: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
46
|
+
evidence_path: z.ZodOptional<z.ZodString>;
|
|
47
|
+
blocked_reason: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type Task = z.infer<typeof TaskSchema>;
|
|
50
|
+
export declare const PhaseSchema: z.ZodObject<{
|
|
51
|
+
id: z.ZodNumber;
|
|
52
|
+
name: z.ZodString;
|
|
53
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
54
|
+
pending: "pending";
|
|
55
|
+
in_progress: "in_progress";
|
|
56
|
+
blocked: "blocked";
|
|
57
|
+
complete: "complete";
|
|
58
|
+
}>>;
|
|
59
|
+
tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
phase: z.ZodNumber;
|
|
62
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
63
|
+
pending: "pending";
|
|
64
|
+
in_progress: "in_progress";
|
|
65
|
+
completed: "completed";
|
|
66
|
+
blocked: "blocked";
|
|
67
|
+
}>>;
|
|
68
|
+
size: z.ZodDefault<z.ZodEnum<{
|
|
69
|
+
small: "small";
|
|
70
|
+
medium: "medium";
|
|
71
|
+
large: "large";
|
|
72
|
+
}>>;
|
|
73
|
+
description: z.ZodString;
|
|
74
|
+
depends: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
75
|
+
acceptance: z.ZodOptional<z.ZodString>;
|
|
76
|
+
files_touched: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
77
|
+
evidence_path: z.ZodOptional<z.ZodString>;
|
|
78
|
+
blocked_reason: z.ZodOptional<z.ZodString>;
|
|
79
|
+
}, z.core.$strip>>>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
export type Phase = z.infer<typeof PhaseSchema>;
|
|
82
|
+
export declare const PlanSchema: z.ZodObject<{
|
|
83
|
+
schema_version: z.ZodLiteral<"1.0.0">;
|
|
84
|
+
title: z.ZodString;
|
|
85
|
+
swarm: z.ZodString;
|
|
86
|
+
current_phase: z.ZodNumber;
|
|
87
|
+
phases: z.ZodArray<z.ZodObject<{
|
|
88
|
+
id: z.ZodNumber;
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
91
|
+
pending: "pending";
|
|
92
|
+
in_progress: "in_progress";
|
|
93
|
+
blocked: "blocked";
|
|
94
|
+
complete: "complete";
|
|
95
|
+
}>>;
|
|
96
|
+
tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
phase: z.ZodNumber;
|
|
99
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
100
|
+
pending: "pending";
|
|
101
|
+
in_progress: "in_progress";
|
|
102
|
+
completed: "completed";
|
|
103
|
+
blocked: "blocked";
|
|
104
|
+
}>>;
|
|
105
|
+
size: z.ZodDefault<z.ZodEnum<{
|
|
106
|
+
small: "small";
|
|
107
|
+
medium: "medium";
|
|
108
|
+
large: "large";
|
|
109
|
+
}>>;
|
|
110
|
+
description: z.ZodString;
|
|
111
|
+
depends: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
112
|
+
acceptance: z.ZodOptional<z.ZodString>;
|
|
113
|
+
files_touched: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
114
|
+
evidence_path: z.ZodOptional<z.ZodString>;
|
|
115
|
+
blocked_reason: z.ZodOptional<z.ZodString>;
|
|
116
|
+
}, z.core.$strip>>>;
|
|
117
|
+
}, z.core.$strip>>;
|
|
118
|
+
migration_status: z.ZodOptional<z.ZodEnum<{
|
|
119
|
+
native: "native";
|
|
120
|
+
migrated: "migrated";
|
|
121
|
+
migration_failed: "migration_failed";
|
|
122
|
+
}>>;
|
|
123
|
+
}, z.core.$strip>;
|
|
124
|
+
export type Plan = z.infer<typeof PlanSchema>;
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -27,8 +27,49 @@ export declare const ContextBudgetConfigSchema: z.ZodObject<{
|
|
|
27
27
|
warn_threshold: z.ZodDefault<z.ZodNumber>;
|
|
28
28
|
critical_threshold: z.ZodDefault<z.ZodNumber>;
|
|
29
29
|
model_limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
30
|
+
max_injection_tokens: z.ZodDefault<z.ZodNumber>;
|
|
30
31
|
}, z.core.$strip>;
|
|
31
32
|
export type ContextBudgetConfig = z.infer<typeof ContextBudgetConfigSchema>;
|
|
33
|
+
export declare const EvidenceConfigSchema: z.ZodObject<{
|
|
34
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
35
|
+
max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
max_bundles: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
auto_archive: z.ZodDefault<z.ZodBoolean>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type EvidenceConfig = z.infer<typeof EvidenceConfigSchema>;
|
|
40
|
+
export declare const GuardrailsProfileSchema: z.ZodObject<{
|
|
41
|
+
max_tool_calls: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
max_duration_minutes: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
max_repetitions: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
max_consecutive_errors: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
warning_threshold: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type GuardrailsProfile = z.infer<typeof GuardrailsProfileSchema>;
|
|
48
|
+
export declare const GuardrailsConfigSchema: z.ZodObject<{
|
|
49
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
50
|
+
max_tool_calls: z.ZodDefault<z.ZodNumber>;
|
|
51
|
+
max_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
52
|
+
max_repetitions: z.ZodDefault<z.ZodNumber>;
|
|
53
|
+
max_consecutive_errors: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
warning_threshold: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
profiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
56
|
+
max_tool_calls: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
max_duration_minutes: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
max_repetitions: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
max_consecutive_errors: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
warning_threshold: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
}, z.core.$strip>>>;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
export type GuardrailsConfig = z.infer<typeof GuardrailsConfigSchema>;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve guardrails configuration for a specific agent.
|
|
66
|
+
* Merges the base config with any per-agent profile overrides.
|
|
67
|
+
*
|
|
68
|
+
* @param base - The base guardrails configuration
|
|
69
|
+
* @param agentName - Optional agent name to look up profile overrides
|
|
70
|
+
* @returns The effective guardrails configuration for the agent
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveGuardrailsConfig(base: GuardrailsConfig, agentName?: string): GuardrailsConfig;
|
|
32
73
|
export declare const PluginConfigSchema: z.ZodObject<{
|
|
33
74
|
agents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
34
75
|
model: z.ZodOptional<z.ZodString>;
|
|
@@ -58,6 +99,28 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
58
99
|
warn_threshold: z.ZodDefault<z.ZodNumber>;
|
|
59
100
|
critical_threshold: z.ZodDefault<z.ZodNumber>;
|
|
60
101
|
model_limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
102
|
+
max_injection_tokens: z.ZodDefault<z.ZodNumber>;
|
|
103
|
+
}, z.core.$strip>>;
|
|
104
|
+
guardrails: z.ZodOptional<z.ZodObject<{
|
|
105
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
106
|
+
max_tool_calls: z.ZodDefault<z.ZodNumber>;
|
|
107
|
+
max_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
108
|
+
max_repetitions: z.ZodDefault<z.ZodNumber>;
|
|
109
|
+
max_consecutive_errors: z.ZodDefault<z.ZodNumber>;
|
|
110
|
+
warning_threshold: z.ZodDefault<z.ZodNumber>;
|
|
111
|
+
profiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
112
|
+
max_tool_calls: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
max_duration_minutes: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
max_repetitions: z.ZodOptional<z.ZodNumber>;
|
|
115
|
+
max_consecutive_errors: z.ZodOptional<z.ZodNumber>;
|
|
116
|
+
warning_threshold: z.ZodOptional<z.ZodNumber>;
|
|
117
|
+
}, z.core.$strip>>>;
|
|
118
|
+
}, z.core.$strip>>;
|
|
119
|
+
evidence: z.ZodOptional<z.ZodObject<{
|
|
120
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
121
|
+
max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
122
|
+
max_bundles: z.ZodDefault<z.ZodNumber>;
|
|
123
|
+
auto_archive: z.ZodDefault<z.ZodBoolean>;
|
|
61
124
|
}, z.core.$strip>>;
|
|
62
125
|
}, z.core.$strip>;
|
|
63
126
|
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { archiveEvidence, deleteEvidence, listEvidenceTaskIds, loadEvidence, sanitizeTaskId, saveEvidence, } from './manager';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Evidence, type EvidenceBundle } from '../config/evidence-schema';
|
|
2
|
+
/**
|
|
3
|
+
* Validate and sanitize task ID.
|
|
4
|
+
* Must match regex ^[\w-]+(\.[\w-]+)*$
|
|
5
|
+
* Rejects: .., ../, null bytes, control characters, empty string
|
|
6
|
+
* @throws Error with descriptive message on failure
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeTaskId(taskId: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Save evidence to a task's evidence bundle.
|
|
11
|
+
* Creates new bundle if doesn't exist, appends to existing.
|
|
12
|
+
* Performs atomic write via temp file + rename.
|
|
13
|
+
* @throws Error if task ID is invalid or size limit would be exceeded
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveEvidence(directory: string, taskId: string, evidence: Evidence): Promise<EvidenceBundle>;
|
|
16
|
+
/**
|
|
17
|
+
* Load evidence bundle for a task.
|
|
18
|
+
* Returns null if file doesn't exist or validation fails.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadEvidence(directory: string, taskId: string): Promise<EvidenceBundle | null>;
|
|
21
|
+
/**
|
|
22
|
+
* List all task IDs that have evidence bundles.
|
|
23
|
+
* Returns sorted array of valid task IDs.
|
|
24
|
+
* Returns empty array if evidence directory doesn't exist.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listEvidenceTaskIds(directory: string): Promise<string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Delete evidence bundle for a task.
|
|
29
|
+
* Returns true if deleted, false if didn't exist or deletion failed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deleteEvidence(directory: string, taskId: string): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Archive old evidence bundles based on retention policy.
|
|
34
|
+
* Removes evidence older than maxAgeDays.
|
|
35
|
+
* If maxBundles is provided, enforces a maximum bundle count by deleting oldest first.
|
|
36
|
+
* Returns array of archived (deleted) task IDs.
|
|
37
|
+
*/
|
|
38
|
+
export declare function archiveEvidence(directory: string, maxAgeDays: number, maxBundles?: number): Promise<string[]>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/state.ts
|
|
2
|
+
var swarmState = {
|
|
3
|
+
activeToolCalls: new Map,
|
|
4
|
+
toolAggregates: new Map,
|
|
5
|
+
activeAgent: new Map,
|
|
6
|
+
delegationChains: new Map,
|
|
7
|
+
pendingEvents: 0,
|
|
8
|
+
agentSessions: new Map
|
|
9
|
+
};
|
|
10
|
+
function startAgentSession(sessionId, agentName, staleDurationMs = 3600000) {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
for (const [id, session] of swarmState.agentSessions) {
|
|
13
|
+
if (now - session.startTime > staleDurationMs) {
|
|
14
|
+
swarmState.agentSessions.delete(id);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const sessionState = {
|
|
18
|
+
agentName,
|
|
19
|
+
startTime: now,
|
|
20
|
+
toolCallCount: 0,
|
|
21
|
+
consecutiveErrors: 0,
|
|
22
|
+
recentToolCalls: [],
|
|
23
|
+
warningIssued: false,
|
|
24
|
+
hardLimitHit: false
|
|
25
|
+
};
|
|
26
|
+
swarmState.agentSessions.set(sessionId, sessionState);
|
|
27
|
+
}
|
|
28
|
+
function getAgentSession(sessionId) {
|
|
29
|
+
return swarmState.agentSessions.get(sessionId);
|
|
30
|
+
}
|
|
31
|
+
// src/utils/logger.ts
|
|
32
|
+
var DEBUG = process.env.OPENCODE_SWARM_DEBUG === "1";
|
|
33
|
+
function warn(message, data) {
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
if (data !== undefined) {
|
|
36
|
+
console.warn(`[opencode-swarm ${timestamp}] WARN: ${message}`, data);
|
|
37
|
+
} else {
|
|
38
|
+
console.warn(`[opencode-swarm ${timestamp}] WARN: ${message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// src/hooks/guardrails.ts
|
|
42
|
+
function createGuardrailsHooks(config) {
|
|
43
|
+
if (config.enabled === false) {
|
|
44
|
+
return {
|
|
45
|
+
toolBefore: async () => {},
|
|
46
|
+
toolAfter: async () => {},
|
|
47
|
+
messagesTransform: async () => {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
toolBefore: async (input, output) => {
|
|
52
|
+
let session = getAgentSession(input.sessionID);
|
|
53
|
+
if (!session) {
|
|
54
|
+
startAgentSession(input.sessionID, "unknown");
|
|
55
|
+
session = getAgentSession(input.sessionID);
|
|
56
|
+
if (!session) {
|
|
57
|
+
warn(`Failed to create session for ${input.sessionID}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (session.hardLimitHit) {
|
|
62
|
+
throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
|
|
63
|
+
}
|
|
64
|
+
session.toolCallCount++;
|
|
65
|
+
const hash = hashArgs(output.args);
|
|
66
|
+
session.recentToolCalls.push({
|
|
67
|
+
tool: input.tool,
|
|
68
|
+
argsHash: hash,
|
|
69
|
+
timestamp: Date.now()
|
|
70
|
+
});
|
|
71
|
+
if (session.recentToolCalls.length > 20) {
|
|
72
|
+
session.recentToolCalls.shift();
|
|
73
|
+
}
|
|
74
|
+
let repetitionCount = 0;
|
|
75
|
+
if (session.recentToolCalls.length > 0) {
|
|
76
|
+
const lastEntry = session.recentToolCalls[session.recentToolCalls.length - 1];
|
|
77
|
+
for (let i = session.recentToolCalls.length - 1;i >= 0; i--) {
|
|
78
|
+
const entry = session.recentToolCalls[i];
|
|
79
|
+
if (entry.tool === lastEntry.tool && entry.argsHash === lastEntry.argsHash) {
|
|
80
|
+
repetitionCount++;
|
|
81
|
+
} else {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const elapsedMinutes = (Date.now() - session.startTime) / 60000;
|
|
87
|
+
if (session.toolCallCount >= config.max_tool_calls) {
|
|
88
|
+
session.hardLimitHit = true;
|
|
89
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Tool call limit reached (${session.toolCallCount}/${config.max_tool_calls}). Stop making tool calls and return your progress summary.`);
|
|
90
|
+
}
|
|
91
|
+
if (elapsedMinutes >= config.max_duration_minutes) {
|
|
92
|
+
session.hardLimitHit = true;
|
|
93
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Duration limit reached (${Math.floor(elapsedMinutes)} min). Stop making tool calls and return your progress summary.`);
|
|
94
|
+
}
|
|
95
|
+
if (repetitionCount >= config.max_repetitions) {
|
|
96
|
+
session.hardLimitHit = true;
|
|
97
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Repetition detected (same call ${repetitionCount} times). Stop making tool calls and return your progress summary.`);
|
|
98
|
+
}
|
|
99
|
+
if (session.consecutiveErrors >= config.max_consecutive_errors) {
|
|
100
|
+
session.hardLimitHit = true;
|
|
101
|
+
throw new Error(`\uD83D\uDED1 CIRCUIT BREAKER: Too many consecutive errors (${session.consecutiveErrors}). Stop making tool calls and return your progress summary.`);
|
|
102
|
+
}
|
|
103
|
+
if (!session.warningIssued) {
|
|
104
|
+
const toolWarning = session.toolCallCount >= config.max_tool_calls * config.warning_threshold;
|
|
105
|
+
const durationWarning = elapsedMinutes >= config.max_duration_minutes * config.warning_threshold;
|
|
106
|
+
const repetitionWarning = repetitionCount >= config.max_repetitions * config.warning_threshold;
|
|
107
|
+
const errorWarning = session.consecutiveErrors >= config.max_consecutive_errors * config.warning_threshold;
|
|
108
|
+
if (toolWarning || durationWarning || repetitionWarning || errorWarning) {
|
|
109
|
+
session.warningIssued = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
toolAfter: async (input, output) => {
|
|
114
|
+
const session = getAgentSession(input.sessionID);
|
|
115
|
+
if (!session) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const outputStr = String(output.output ?? "");
|
|
119
|
+
const hasError = output.output === null || output.output === undefined || outputStr === "" || outputStr.toLowerCase().includes("error");
|
|
120
|
+
if (hasError) {
|
|
121
|
+
session.consecutiveErrors++;
|
|
122
|
+
} else {
|
|
123
|
+
session.consecutiveErrors = 0;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
messagesTransform: async (input, output) => {
|
|
127
|
+
const messages = output.messages;
|
|
128
|
+
if (!messages || messages.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const lastMessage = messages[messages.length - 1];
|
|
132
|
+
let sessionId = lastMessage.info?.sessionID;
|
|
133
|
+
if (!sessionId) {
|
|
134
|
+
for (const [id, session2] of swarmState.agentSessions) {
|
|
135
|
+
if (session2.warningIssued || session2.hardLimitHit) {
|
|
136
|
+
sessionId = id;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!sessionId) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const session = getAgentSession(sessionId);
|
|
145
|
+
if (!session || !session.warningIssued && !session.hardLimitHit) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const textPart = lastMessage.parts.find((part) => part.type === "text" && typeof part.text === "string");
|
|
149
|
+
if (!textPart) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (session.hardLimitHit) {
|
|
153
|
+
textPart.text = `[\uD83D\uDED1 CIRCUIT BREAKER ACTIVE: You have exceeded your resource limits. Do NOT make any more tool calls. Immediately return a summary of your progress so far. Any further tool calls will be blocked.]
|
|
154
|
+
|
|
155
|
+
` + textPart.text;
|
|
156
|
+
} else if (session.warningIssued) {
|
|
157
|
+
textPart.text = `[⚠️ GUARDRAIL WARNING: You are approaching resource limits. Please wrap up your current task efficiently. Avoid unnecessary tool calls and prepare to return your results soon.]
|
|
158
|
+
|
|
159
|
+
` + textPart.text;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function hashArgs(args) {
|
|
165
|
+
try {
|
|
166
|
+
if (typeof args !== "object" || args === null) {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
const sortedKeys = Object.keys(args).sort();
|
|
170
|
+
return Number(Bun.hash(JSON.stringify(args, sortedKeys)));
|
|
171
|
+
} catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
hashArgs,
|
|
177
|
+
createGuardrailsHooks
|
|
178
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Plan } from '../config/plan-schema';
|
|
1
2
|
/**
|
|
2
3
|
* Swarm File Extractors
|
|
3
4
|
*
|
|
@@ -24,3 +25,15 @@ export declare function extractIncompleteTasks(planContent: string, maxChars?: n
|
|
|
24
25
|
* Extracts patterns section from context content.
|
|
25
26
|
*/
|
|
26
27
|
export declare function extractPatterns(contextContent: string, maxChars?: number): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Extracts current phase info from a Plan object.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractCurrentPhaseFromPlan(plan: Plan): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Extracts the first incomplete task from the current phase of a Plan object.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractCurrentTaskFromPlan(plan: Plan): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Extracts incomplete tasks from the current phase of a Plan object.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractIncompleteTasksFromPlan(plan: Plan, maxChars?: number): string | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardrails Hook Module
|
|
3
|
+
*
|
|
4
|
+
* Circuit breaker for runaway LLM agents. Monitors tool usage via OpenCode Plugin API hooks
|
|
5
|
+
* and implements two-layer protection:
|
|
6
|
+
* - Layer 1 (Soft Warning @ warning_threshold): Sets warning flag for messagesTransform to inject warning
|
|
7
|
+
* - Layer 2 (Hard Block @ 100%): Throws error in toolBefore to block further calls, injects STOP message
|
|
8
|
+
*/
|
|
9
|
+
import { type GuardrailsConfig } from '../config/schema';
|
|
10
|
+
/**
|
|
11
|
+
* Creates guardrails hooks for circuit breaker protection
|
|
12
|
+
* @param config Guardrails configuration
|
|
13
|
+
* @returns Tool before/after hooks and messages transform hook
|
|
14
|
+
*/
|
|
15
|
+
export declare function createGuardrailsHooks(config: GuardrailsConfig): {
|
|
16
|
+
toolBefore: (input: {
|
|
17
|
+
tool: string;
|
|
18
|
+
sessionID: string;
|
|
19
|
+
callID: string;
|
|
20
|
+
}, output: {
|
|
21
|
+
args: unknown;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
toolAfter: (input: {
|
|
24
|
+
tool: string;
|
|
25
|
+
sessionID: string;
|
|
26
|
+
callID: string;
|
|
27
|
+
}, output: {
|
|
28
|
+
title: string;
|
|
29
|
+
output: string;
|
|
30
|
+
metadata: unknown;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
messagesTransform: (input: Record<string, never>, output: {
|
|
33
|
+
messages?: Array<{
|
|
34
|
+
info: {
|
|
35
|
+
role: string;
|
|
36
|
+
agent?: string;
|
|
37
|
+
sessionID?: string;
|
|
38
|
+
};
|
|
39
|
+
parts: Array<{
|
|
40
|
+
type: string;
|
|
41
|
+
text?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
}>;
|
|
45
|
+
}) => Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Hashes tool arguments for repetition detection
|
|
49
|
+
* @param args Tool arguments to hash
|
|
50
|
+
* @returns Numeric hash (0 if hashing fails)
|
|
51
|
+
*/
|
|
52
|
+
export declare function hashArgs(args: unknown): number;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ 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 { extractCurrentPhase, extractCurrentTask, extractDecisions, extractIncompleteTasks, extractPatterns, } from './extractors';
|
|
5
|
+
export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
|
|
6
|
+
export { createGuardrailsHooks } from './guardrails';
|
|
6
7
|
export { createPipelineTrackerHook } from './pipeline-tracker';
|
|
7
8
|
export { createSystemEnhancerHook } from './system-enhancer';
|
|
8
9
|
export { composeHandlers, estimateTokens, readSwarmFileAsync, safeHook, validateSwarmPath, } from './utils';
|