@jetrabbits/agentic 0.1.0 → 0.3.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/AGENTS.md +2 -11
- package/CHANGELOG.md +23 -0
- package/MEMORY.md +113 -0
- package/Makefile +55 -3
- package/README.md +23 -5
- package/agentic +1458 -154
- package/docs/agentic-lifecycle.md +3 -3
- package/docs/agentic-stabilization/README.md +33 -0
- package/docs/agentic-token-minimization/README.md +1 -1
- package/docs/agentic-usage.md +51 -10
- package/docs/opencode_setup.md +4 -2
- package/extensions/opencode/opencode.json +1 -1
- package/extensions/opencode/plugins/agent-model-mapper.ts +106 -0
- package/extensions/opencode/plugins/telegram-notification.ts +21 -11
- package/package.json +3 -1
- package/extensions/opencode/plugins/model-checker.json +0 -13
- package/extensions/opencode/plugins/model-checker.ts +0 -302
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
-
import { readFileSync } from "node:fs"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { tmpdir } from "node:os"
|
|
6
|
-
import { spawn } from "node:child_process"
|
|
7
|
-
|
|
8
|
-
interface ModelResult {
|
|
9
|
-
model: string
|
|
10
|
-
status: "passed" | "failed"
|
|
11
|
-
error?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ModelCheckerConfig {
|
|
15
|
-
models: string[]
|
|
16
|
-
timeoutMs: number
|
|
17
|
-
concurrency: number
|
|
18
|
-
prompt: string
|
|
19
|
-
probeDir: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type CommandResult = {
|
|
23
|
-
code: number | null
|
|
24
|
-
output: string
|
|
25
|
-
timedOut: boolean
|
|
26
|
-
durationMs: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type AgenticPluginConfig = {
|
|
30
|
-
modelChecker?: {
|
|
31
|
-
enabled?: boolean
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function readAgenticConfig(): AgenticPluginConfig {
|
|
36
|
-
const configHome = process.env.XDG_CONFIG_HOME || join(process.env.HOME || "", ".config")
|
|
37
|
-
const configPath = join(configHome, "agentic", "opencode-plugins.json")
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
return JSON.parse(readFileSync(configPath, "utf-8")) as AgenticPluginConfig
|
|
41
|
-
} catch {
|
|
42
|
-
return {}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function readModelsJson(projectDir: string): Promise<ModelCheckerConfig> {
|
|
47
|
-
const defaults: ModelCheckerConfig = {
|
|
48
|
-
models: [],
|
|
49
|
-
timeoutMs: 10000,
|
|
50
|
-
concurrency: 3,
|
|
51
|
-
prompt: "Reply with exactly: OK",
|
|
52
|
-
probeDir: tmpdir(),
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const filePath = join(projectDir, ".opencode", "plugins", "model-checker.json")
|
|
57
|
-
const content = await readFile(filePath, "utf-8")
|
|
58
|
-
const data = JSON.parse(content)
|
|
59
|
-
if (!Array.isArray(data.models)) return defaults
|
|
60
|
-
|
|
61
|
-
const models = [...new Set(data.models.map(String).filter(Boolean))]
|
|
62
|
-
const timeoutMs = Number.isFinite(data.timeoutMs)
|
|
63
|
-
? Math.min(120000, Math.max(1000, Number(data.timeoutMs)))
|
|
64
|
-
: defaults.timeoutMs
|
|
65
|
-
const concurrency = Number.isFinite(data.concurrency)
|
|
66
|
-
? Math.min(10, Math.max(1, Number(data.concurrency)))
|
|
67
|
-
: defaults.concurrency
|
|
68
|
-
const prompt = typeof data.prompt === "string" && data.prompt.trim() ? data.prompt.trim() : defaults.prompt
|
|
69
|
-
const probeDir = typeof data.probeDir === "string" && data.probeDir.trim() ? data.probeDir.trim() : defaults.probeDir
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
models,
|
|
73
|
-
timeoutMs,
|
|
74
|
-
concurrency,
|
|
75
|
-
prompt,
|
|
76
|
-
probeDir,
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
return defaults
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function runModelProbe(model: string, prompt: string, timeoutMs: number, probeDir: string): Promise<CommandResult> {
|
|
84
|
-
return await new Promise((resolve) => {
|
|
85
|
-
const startedAt = Date.now()
|
|
86
|
-
const child = spawn("opencode", ["run", prompt, "-m", model, "--dir", probeDir, "--log-level", "ERROR"], {
|
|
87
|
-
cwd: probeDir,
|
|
88
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
-
env: {
|
|
90
|
-
...process.env,
|
|
91
|
-
OPENCODE_MODEL_CHECKER_ACTIVE: "1",
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
let stdout = ""
|
|
96
|
-
let stderr = ""
|
|
97
|
-
let timedOut = false
|
|
98
|
-
|
|
99
|
-
const timer = setTimeout(() => {
|
|
100
|
-
timedOut = true
|
|
101
|
-
child.kill("SIGKILL")
|
|
102
|
-
}, timeoutMs)
|
|
103
|
-
|
|
104
|
-
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
105
|
-
stdout += chunk.toString()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
109
|
-
stderr += chunk.toString()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
child.on("close", (code) => {
|
|
113
|
-
clearTimeout(timer)
|
|
114
|
-
resolve({
|
|
115
|
-
code,
|
|
116
|
-
timedOut,
|
|
117
|
-
output: `${stdout}\n${stderr}`.toLowerCase(),
|
|
118
|
-
durationMs: Date.now() - startedAt,
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
child.on("error", (error) => {
|
|
123
|
-
clearTimeout(timer)
|
|
124
|
-
resolve({
|
|
125
|
-
code: 1,
|
|
126
|
-
timedOut,
|
|
127
|
-
output: String(error).toLowerCase(),
|
|
128
|
-
durationMs: Date.now() - startedAt,
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function runModelChecks(
|
|
135
|
-
models: string[],
|
|
136
|
-
prompt: string,
|
|
137
|
-
timeoutMs: number,
|
|
138
|
-
concurrency: number,
|
|
139
|
-
probeDir: string,
|
|
140
|
-
): Promise<CommandResult[]> {
|
|
141
|
-
const workers = Math.min(concurrency, models.length)
|
|
142
|
-
const results: CommandResult[] = new Array(models.length)
|
|
143
|
-
let cursor = 0
|
|
144
|
-
|
|
145
|
-
await Promise.all(
|
|
146
|
-
Array.from({ length: workers }, async () => {
|
|
147
|
-
while (true) {
|
|
148
|
-
const index = cursor++
|
|
149
|
-
if (index >= models.length) return
|
|
150
|
-
results[index] = await runModelProbe(models[index], prompt, timeoutMs, probeDir)
|
|
151
|
-
}
|
|
152
|
-
}),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
return results
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function classifyResult(model: string, probe: CommandResult): ModelResult {
|
|
159
|
-
if (probe.timedOut) return { model, status: "failed", error: "timeout" }
|
|
160
|
-
|
|
161
|
-
if (probe.output.includes("quota") || probe.output.includes("insufficient") || probe.output.includes("429")) {
|
|
162
|
-
return { model, status: "failed", error: "quota" }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (probe.code !== 0) {
|
|
166
|
-
if (/(\b4\d\d\b|\b5\d\d\b)/.test(probe.output)) {
|
|
167
|
-
return { model, status: "failed", error: "provider error" }
|
|
168
|
-
}
|
|
169
|
-
return { model, status: "failed", error: `exit code ${probe.code ?? "null"}` }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!probe.output.includes("ok")) {
|
|
173
|
-
return { model, status: "failed", error: "unexpected response" }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { model, status: "passed" }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function printLists(passed: ModelResult[], failed: ModelResult[]) {
|
|
180
|
-
console.log("\npassed_models:")
|
|
181
|
-
for (const model of passed) {
|
|
182
|
-
console.log(`- ${model.model}`)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.log("\nfailed_models:")
|
|
186
|
-
for (const model of failed) {
|
|
187
|
-
console.log(`- ${model.model}${model.error ? ` (${model.error})` : ""}`)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function formatTelegramMessage(passed: ModelResult[], failed: ModelResult[], selected: string): string {
|
|
192
|
-
const passedText = passed.length ? passed.map((x) => `• ${x.model}`).join("\n") : "(none)"
|
|
193
|
-
const failedText = failed.length
|
|
194
|
-
? failed.map((x) => `• ${x.model}${x.error ? ` (${x.error})` : ""}`).join("\n")
|
|
195
|
-
: "(none)"
|
|
196
|
-
|
|
197
|
-
return [
|
|
198
|
-
"<b>LLM availability check</b>",
|
|
199
|
-
"",
|
|
200
|
-
`<b>passed_models (${passed.length})</b>`,
|
|
201
|
-
passedText,
|
|
202
|
-
"",
|
|
203
|
-
`<b>failed_models (${failed.length})</b>`,
|
|
204
|
-
failedText,
|
|
205
|
-
"",
|
|
206
|
-
`<b>selected model:</b> ${selected}`,
|
|
207
|
-
].join("\n")
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async function sendTelegram(message: string) {
|
|
211
|
-
const token = process.env.OPENCODE_TELEGRAM_BOT_TOKEN
|
|
212
|
-
const chatID = process.env.OPENCODE_TELEGRAM_CHAT_ID
|
|
213
|
-
if (!token || !chatID) return
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
217
|
-
method: "POST",
|
|
218
|
-
headers: { "Content-Type": "application/json" },
|
|
219
|
-
body: JSON.stringify({
|
|
220
|
-
chat_id: chatID,
|
|
221
|
-
text: message,
|
|
222
|
-
parse_mode: "HTML",
|
|
223
|
-
}),
|
|
224
|
-
})
|
|
225
|
-
} catch {
|
|
226
|
-
// no-op
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function regenerateOpencodeJson(projectDir: string, selected: string, passed: string[]) {
|
|
231
|
-
const filePath = join(projectDir, ".opencode", "opencode.json")
|
|
232
|
-
try {
|
|
233
|
-
const content = await readFile(filePath, "utf-8")
|
|
234
|
-
const config = JSON.parse(content)
|
|
235
|
-
|
|
236
|
-
if (!config.agent || typeof config.agent !== "object") return
|
|
237
|
-
|
|
238
|
-
for (const [agentName, agentConfig] of Object.entries<any>(config.agent)) {
|
|
239
|
-
if (!agentConfig || typeof agentConfig !== "object") continue
|
|
240
|
-
if (agentConfig.mode !== "subagent") continue
|
|
241
|
-
|
|
242
|
-
config.agent[agentName].model = selected
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf-8")
|
|
246
|
-
} catch {
|
|
247
|
-
// no-op
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export const ModelCheckerPlugin: Plugin = async ({ directory }) => {
|
|
252
|
-
const config = readAgenticConfig()
|
|
253
|
-
if (!config.modelChecker?.enabled) {
|
|
254
|
-
return {}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (process.env.OPENCODE_MODEL_CHECKER_ACTIVE === "1") {
|
|
258
|
-
return {}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (process.argv[2] === "run") {
|
|
262
|
-
return {}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function runChecks() {
|
|
266
|
-
const config = await readModelsJson(directory)
|
|
267
|
-
const { models, prompt, timeoutMs, concurrency, probeDir } = config
|
|
268
|
-
if (!models.length) return
|
|
269
|
-
|
|
270
|
-
await mkdir(probeDir, { recursive: true })
|
|
271
|
-
|
|
272
|
-
console.log("\nStarting LLM model availability check...")
|
|
273
|
-
console.log(`- models: ${models.length}, concurrency: ${Math.min(concurrency, models.length)}, timeout: ${timeoutMs}ms`)
|
|
274
|
-
const results: ModelResult[] = []
|
|
275
|
-
const probes = await runModelChecks(models, prompt, timeoutMs, concurrency, probeDir)
|
|
276
|
-
for (const [index, model] of models.entries()) {
|
|
277
|
-
const probe = probes[index]
|
|
278
|
-
const result = classifyResult(model, probe)
|
|
279
|
-
results.push(result)
|
|
280
|
-
console.log(
|
|
281
|
-
`- checking ${model}... ${result.status === "passed" ? "OK" : `FAIL (${result.error})`} (${probe.durationMs}ms)`,
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const passedModels = results.filter((x) => x.status === "passed")
|
|
286
|
-
const failedModels = results.filter((x) => x.status === "failed")
|
|
287
|
-
const selected = passedModels[0]?.model ?? "none"
|
|
288
|
-
|
|
289
|
-
printLists(passedModels, failedModels)
|
|
290
|
-
console.log(`\nselected_model: ${selected}`)
|
|
291
|
-
|
|
292
|
-
if (selected !== "none") {
|
|
293
|
-
await regenerateOpencodeJson(directory, selected, passedModels.map((x) => x.model))
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
await sendTelegram(formatTelegramMessage(passedModels, failedModels, selected))
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
await runChecks()
|
|
300
|
-
|
|
301
|
-
return {}
|
|
302
|
-
}
|