@praneeth_54/agentdoctor 2.0.1 → 3.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/CHANGELOG.md +123 -1
- package/README.md +510 -260
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +246 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +28 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/loop.d.ts +53 -0
- package/dist/agent/loop.js +253 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +83 -0
- package/dist/agent/runtime.js +293 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +20 -0
- package/dist/agent/tools/execute.js +365 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +69 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +9 -0
- package/dist/ai/redact.js +61 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +29 -0
- package/dist/cli/commands/agent.js +164 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +153 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +233 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +333 -43
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/intelligence/graph/build.js +36 -8
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +91 -26
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +320 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +24 -0
- package/dist/product/decisions/ledger.js +110 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +42 -0
- package/dist/product/discovery/roots.js +215 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +193 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +69 -0
- package/dist/product/index.js +35 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +100 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/security/paths.js +43 -15
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { analyzeChange, verifyChange } from "../assurance/change.js";
|
|
2
|
+
import { buildProofFromEvidence } from "../assurance/proof.js";
|
|
3
|
+
import { checkArchitectureAtRoot } from "../architecture/contract.js";
|
|
4
|
+
import { buildRepositoryGraph } from "../platform/graph/build.js";
|
|
5
|
+
import { analyzeChanges } from "../core/changes/analyze.js";
|
|
6
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
7
|
+
import { executeAgentTool, newToolCall } from "./tools/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Post-change verification for the coding agent (M5).
|
|
10
|
+
* Reuses analyzeChange / verifyChange / proof / architecture — does not invent results.
|
|
11
|
+
*/
|
|
12
|
+
export async function verifyAgentWork(options) {
|
|
13
|
+
const root = resolveRepoRoot(options.root);
|
|
14
|
+
const checks = [];
|
|
15
|
+
const verified = [];
|
|
16
|
+
const notVerified = [
|
|
17
|
+
"production database",
|
|
18
|
+
"external deployment",
|
|
19
|
+
"external API behavior",
|
|
20
|
+
"full product correctness",
|
|
21
|
+
];
|
|
22
|
+
const git = await analyzeChanges({ root, impact: true });
|
|
23
|
+
checks.push({
|
|
24
|
+
id: "git-status",
|
|
25
|
+
label: "Git status / change set",
|
|
26
|
+
status: git.gitAvailable ? "passed" : "skipped",
|
|
27
|
+
detail: git.gitAvailable
|
|
28
|
+
? `${git.files.length} changed path(s); impact=${git.impact?.status ?? "n/a"}`
|
|
29
|
+
: "Git not available — change set UNKNOWN",
|
|
30
|
+
});
|
|
31
|
+
if (git.gitAvailable)
|
|
32
|
+
verified.push("Git change set inspected");
|
|
33
|
+
const filesChanged = options.filesChanged?.length
|
|
34
|
+
? options.filesChanged
|
|
35
|
+
: git.files.map((f) => f.path).slice(0, 50);
|
|
36
|
+
let changeId = null;
|
|
37
|
+
let proofId = null;
|
|
38
|
+
try {
|
|
39
|
+
const assessment = await analyzeChange({ root });
|
|
40
|
+
changeId = assessment.changeId;
|
|
41
|
+
checks.push({
|
|
42
|
+
id: "change-analyze",
|
|
43
|
+
label: "Change impact analyzed",
|
|
44
|
+
status: "passed",
|
|
45
|
+
detail: `changeId=${assessment.changeId}; symbols=${assessment.changedSymbols.length}`,
|
|
46
|
+
});
|
|
47
|
+
verified.push("Change impact analyzed");
|
|
48
|
+
const { assessment: verifiedAssessment, manifest } = await verifyChange({
|
|
49
|
+
root,
|
|
50
|
+
changeId: assessment.changeId,
|
|
51
|
+
});
|
|
52
|
+
checks.push({
|
|
53
|
+
id: "change-verify",
|
|
54
|
+
label: "Evidence bundle produced",
|
|
55
|
+
status: verifiedAssessment.verificationStatus === "evidence-produced" ? "passed" : "failed",
|
|
56
|
+
detail: `status=${verifiedAssessment.verificationStatus}; evidence=${manifest.changeId}`,
|
|
57
|
+
});
|
|
58
|
+
if (verifiedAssessment.verificationStatus === "evidence-produced") {
|
|
59
|
+
verified.push("Evidence bundle produced");
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const proof = await buildProofFromEvidence(root, assessment.changeId);
|
|
63
|
+
proofId = proof.proofId;
|
|
64
|
+
checks.push({
|
|
65
|
+
id: "proof",
|
|
66
|
+
label: "Change proof",
|
|
67
|
+
status: "passed",
|
|
68
|
+
detail: `proofId=${proof.proofId}; integrity=${proof.integrityStatus}; correctness=${proof.correctnessStatus}`,
|
|
69
|
+
});
|
|
70
|
+
verified.push(`Proof built (${proof.integrityStatus})`);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
checks.push({
|
|
74
|
+
id: "proof",
|
|
75
|
+
label: "Change proof",
|
|
76
|
+
status: "failed",
|
|
77
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
checks.push({
|
|
83
|
+
id: "change-analyze",
|
|
84
|
+
label: "Change impact analyzed",
|
|
85
|
+
status: "failed",
|
|
86
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const graph = await buildRepositoryGraph(root);
|
|
91
|
+
const arch = await checkArchitectureAtRoot(root, graph);
|
|
92
|
+
const hasContract = Boolean(arch.contractPath && arch.contract);
|
|
93
|
+
const ok = arch.violations.length === 0;
|
|
94
|
+
checks.push({
|
|
95
|
+
id: "architecture",
|
|
96
|
+
label: "Architecture check",
|
|
97
|
+
status: !hasContract ? "skipped" : ok ? "passed" : "failed",
|
|
98
|
+
detail: !hasContract
|
|
99
|
+
? "No architecture contract present"
|
|
100
|
+
: `${arch.violations.length} violation(s); edges checked=${arch.importEdgesChecked}`,
|
|
101
|
+
});
|
|
102
|
+
if (hasContract) {
|
|
103
|
+
if (ok)
|
|
104
|
+
verified.push("Architecture check passed");
|
|
105
|
+
else
|
|
106
|
+
verified.push("Architecture check ran (violations found)");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
notVerified.push("architecture contract (none present)");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
checks.push({
|
|
114
|
+
id: "architecture",
|
|
115
|
+
label: "Architecture check",
|
|
116
|
+
status: "failed",
|
|
117
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (options.runTests && options.approvedByHuman) {
|
|
121
|
+
const testResult = await executeAgentTool(root, newToolCall("verify", "run_tests", {}), {
|
|
122
|
+
allowExecute: true,
|
|
123
|
+
approvedByHuman: true,
|
|
124
|
+
});
|
|
125
|
+
checks.push({
|
|
126
|
+
id: "tests",
|
|
127
|
+
label: "Controlled test run",
|
|
128
|
+
status: testResult.ok ? "passed" : "failed",
|
|
129
|
+
detail: testResult.ok
|
|
130
|
+
? `exit=${testResult.data.exitCode ?? 0}`
|
|
131
|
+
: `${testResult.error?.code}: ${testResult.error?.message}`,
|
|
132
|
+
});
|
|
133
|
+
if (testResult.ok)
|
|
134
|
+
verified.push("Relevant tests executed (controlled runner)");
|
|
135
|
+
else
|
|
136
|
+
notVerified.push("test suite (execution failed or policy blocked)");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
checks.push({
|
|
140
|
+
id: "tests",
|
|
141
|
+
label: "Controlled test run",
|
|
142
|
+
status: "not-run",
|
|
143
|
+
detail: options.runTests
|
|
144
|
+
? "Tests requested but human approval missing"
|
|
145
|
+
: "Tests not requested for this verification pass",
|
|
146
|
+
});
|
|
147
|
+
notVerified.push("test execution (not run in this pass)");
|
|
148
|
+
}
|
|
149
|
+
checks.push({
|
|
150
|
+
id: "workspace",
|
|
151
|
+
label: "Workspace boundary",
|
|
152
|
+
status: "passed",
|
|
153
|
+
detail: "All prior agent writes used path-safe resolveSafeRepoPath",
|
|
154
|
+
});
|
|
155
|
+
verified.push("Workspace boundary respected (write path)");
|
|
156
|
+
const summaryText = formatVerificationReport({
|
|
157
|
+
root,
|
|
158
|
+
changeId,
|
|
159
|
+
proofId,
|
|
160
|
+
filesChanged,
|
|
161
|
+
checks,
|
|
162
|
+
verified,
|
|
163
|
+
notVerified,
|
|
164
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
165
|
+
summaryText: "",
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
root,
|
|
169
|
+
changeId,
|
|
170
|
+
proofId,
|
|
171
|
+
filesChanged,
|
|
172
|
+
checks,
|
|
173
|
+
verified,
|
|
174
|
+
notVerified,
|
|
175
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
176
|
+
summaryText,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function formatVerificationReport(report) {
|
|
180
|
+
const lines = [
|
|
181
|
+
"",
|
|
182
|
+
"IMPLEMENTATION COMPLETE",
|
|
183
|
+
"",
|
|
184
|
+
report.filesChanged.length
|
|
185
|
+
? `Changed:\n${report.filesChanged.map((f) => ` - ${f}`).join("\n")}`
|
|
186
|
+
: "Changed: (none recorded)",
|
|
187
|
+
"",
|
|
188
|
+
"Verification:",
|
|
189
|
+
...report.checks.map((c) => {
|
|
190
|
+
const mark = c.status === "passed"
|
|
191
|
+
? "✓"
|
|
192
|
+
: c.status === "failed"
|
|
193
|
+
? "✗"
|
|
194
|
+
: c.status === "skipped"
|
|
195
|
+
? "○"
|
|
196
|
+
: "·";
|
|
197
|
+
return ` ${mark} ${c.label} — ${c.detail}`;
|
|
198
|
+
}),
|
|
199
|
+
"",
|
|
200
|
+
"Verified:",
|
|
201
|
+
...report.verified.map((v) => ` - ${v}`),
|
|
202
|
+
"",
|
|
203
|
+
"Not verified:",
|
|
204
|
+
...report.notVerified.map((v) => ` - ${v}`),
|
|
205
|
+
"",
|
|
206
|
+
report.correctnessStatus,
|
|
207
|
+
"",
|
|
208
|
+
];
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AiProviderId } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* AI provider configuration.
|
|
4
|
+
* Secrets come only from environment variables — never from Brain or committed files.
|
|
5
|
+
*/
|
|
6
|
+
export interface AiConfig {
|
|
7
|
+
provider: AiProviderId;
|
|
8
|
+
model: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Present only in memory from env — never serialized to Brain/evidence */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
configured: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load AI config from environment.
|
|
16
|
+
* Does not read API keys from disk. Does not write secrets anywhere.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadAiConfig(env?: NodeJS.ProcessEnv): AiConfig;
|
|
19
|
+
/** Safe view for logs / CLI — never includes apiKey. */
|
|
20
|
+
export declare function publicAiConfig(config: AiConfig): Omit<AiConfig, "apiKey"> & {
|
|
21
|
+
apiKeySet: boolean;
|
|
22
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const DEFAULT_MODELS = {
|
|
2
|
+
none: "none",
|
|
3
|
+
deterministic: "local-analyzers",
|
|
4
|
+
mock: "mock",
|
|
5
|
+
"openai-compatible": "gpt-4o-mini",
|
|
6
|
+
ollama: "llama3.2",
|
|
7
|
+
anthropic: "claude-sonnet-4-5",
|
|
8
|
+
gemini: "gemini-2.0-flash",
|
|
9
|
+
};
|
|
10
|
+
function normalizeProvider(raw) {
|
|
11
|
+
const v = (raw ?? "none").trim().toLowerCase();
|
|
12
|
+
switch (v) {
|
|
13
|
+
case "none":
|
|
14
|
+
case "":
|
|
15
|
+
return "none";
|
|
16
|
+
case "mock":
|
|
17
|
+
return "mock";
|
|
18
|
+
case "openai":
|
|
19
|
+
case "openai-compatible":
|
|
20
|
+
case "openrouter":
|
|
21
|
+
return "openai-compatible";
|
|
22
|
+
case "ollama":
|
|
23
|
+
return "ollama";
|
|
24
|
+
case "anthropic":
|
|
25
|
+
return "anthropic";
|
|
26
|
+
case "gemini":
|
|
27
|
+
case "google":
|
|
28
|
+
return "gemini";
|
|
29
|
+
default:
|
|
30
|
+
return "none";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load AI config from environment.
|
|
35
|
+
* Does not read API keys from disk. Does not write secrets anywhere.
|
|
36
|
+
*/
|
|
37
|
+
export function loadAiConfig(env = process.env) {
|
|
38
|
+
const provider = normalizeProvider(env.AGENTDOCTOR_AI_PROVIDER);
|
|
39
|
+
const model = (env.AGENTDOCTOR_AI_MODEL ?? DEFAULT_MODELS[provider]).trim();
|
|
40
|
+
const baseUrl = env.AGENTDOCTOR_AI_BASE_URL?.trim() || undefined;
|
|
41
|
+
const apiKey = env.AGENTDOCTOR_AI_API_KEY?.trim() || undefined;
|
|
42
|
+
const configured = provider !== "none" &&
|
|
43
|
+
(provider === "mock" ||
|
|
44
|
+
provider === "ollama" ||
|
|
45
|
+
Boolean(apiKey) ||
|
|
46
|
+
(provider === "openai-compatible" && Boolean(baseUrl)));
|
|
47
|
+
const result = {
|
|
48
|
+
provider,
|
|
49
|
+
model,
|
|
50
|
+
configured: provider === "mock" ? true : configured,
|
|
51
|
+
};
|
|
52
|
+
if (baseUrl)
|
|
53
|
+
result.baseUrl = baseUrl;
|
|
54
|
+
if (apiKey)
|
|
55
|
+
result.apiKey = apiKey;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** Safe view for logs / CLI — never includes apiKey. */
|
|
59
|
+
export function publicAiConfig(config) {
|
|
60
|
+
const pub = {
|
|
61
|
+
provider: config.provider,
|
|
62
|
+
model: config.model,
|
|
63
|
+
configured: config.configured,
|
|
64
|
+
apiKeySet: Boolean(config.apiKey),
|
|
65
|
+
};
|
|
66
|
+
if (config.baseUrl)
|
|
67
|
+
pub.baseUrl = config.baseUrl;
|
|
68
|
+
return pub;
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AiConfig } from "./config.js";
|
|
2
|
+
import type { ModelProvider } from "./types.js";
|
|
3
|
+
export type { AiConfig } from "./config.js";
|
|
4
|
+
export { loadAiConfig, publicAiConfig } from "./config.js";
|
|
5
|
+
export { redactForModel, redactMessages } from "./redact.js";
|
|
6
|
+
export type { AiProviderId, ChatMessage, ChatRequest, ChatResponse, ModelMetadata, ModelProvider, ToolCallRequest, ToolSpec, TokenUsage, } from "./types.js";
|
|
7
|
+
export { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
|
|
8
|
+
export { NoneModelProvider } from "./providers/none.js";
|
|
9
|
+
export { MockModelProvider } from "./providers/mock.js";
|
|
10
|
+
export { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
|
|
11
|
+
/**
|
|
12
|
+
* Create a ModelProvider from config / env.
|
|
13
|
+
* Unsupported providers (anthropic/gemini native) return none with a clear error path via none,
|
|
14
|
+
* unless mapped later — for M1 only none/mock/openai-compatible/ollama-via-compatible are live.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createModelProvider(config?: AiConfig): ModelProvider;
|
|
17
|
+
export declare function requireAiProviderMessage(provider: ModelProvider): string | null;
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { loadAiConfig } from "./config.js";
|
|
2
|
+
import { NoneModelProvider } from "./providers/none.js";
|
|
3
|
+
import { MockModelProvider } from "./providers/mock.js";
|
|
4
|
+
import { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
|
|
5
|
+
import { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
|
|
6
|
+
export { loadAiConfig, publicAiConfig } from "./config.js";
|
|
7
|
+
export { redactForModel, redactMessages } from "./redact.js";
|
|
8
|
+
export { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
|
|
9
|
+
export { NoneModelProvider } from "./providers/none.js";
|
|
10
|
+
export { MockModelProvider } from "./providers/mock.js";
|
|
11
|
+
export { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
|
|
12
|
+
/**
|
|
13
|
+
* Create a ModelProvider from config / env.
|
|
14
|
+
* Unsupported providers (anthropic/gemini native) return none with a clear error path via none,
|
|
15
|
+
* unless mapped later — for M1 only none/mock/openai-compatible/ollama-via-compatible are live.
|
|
16
|
+
*/
|
|
17
|
+
export function createModelProvider(config = loadAiConfig()) {
|
|
18
|
+
switch (config.provider) {
|
|
19
|
+
case "mock":
|
|
20
|
+
return new MockModelProvider();
|
|
21
|
+
case "openai-compatible": {
|
|
22
|
+
const opts = {
|
|
23
|
+
model: config.model,
|
|
24
|
+
};
|
|
25
|
+
if (config.apiKey)
|
|
26
|
+
opts.apiKey = config.apiKey;
|
|
27
|
+
if (config.baseUrl)
|
|
28
|
+
opts.baseUrl = config.baseUrl;
|
|
29
|
+
return new OpenAiCompatibleProvider(opts);
|
|
30
|
+
}
|
|
31
|
+
case "ollama":
|
|
32
|
+
// Ollama OpenAI-compatible endpoint when available; otherwise fail closed via HTTP errors.
|
|
33
|
+
return new OpenAiCompatibleProvider({
|
|
34
|
+
apiKey: config.apiKey ?? "ollama",
|
|
35
|
+
baseUrl: config.baseUrl ?? "http://127.0.0.1:11434/v1",
|
|
36
|
+
model: config.model,
|
|
37
|
+
});
|
|
38
|
+
case "anthropic":
|
|
39
|
+
case "gemini":
|
|
40
|
+
// Native SDKs are NOT IMPLEMENTED in M1 — fail closed via none message.
|
|
41
|
+
return new NoneModelProvider();
|
|
42
|
+
case "none":
|
|
43
|
+
default:
|
|
44
|
+
return new NoneModelProvider();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function requireAiProviderMessage(provider) {
|
|
48
|
+
if (provider.id === "none") {
|
|
49
|
+
return AI_PROVIDER_REQUIRED_MESSAGE;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResponse, ModelMetadata, ModelProvider, ToolCallRequest } from "../types.js";
|
|
2
|
+
export interface MockScriptStep {
|
|
3
|
+
content?: string;
|
|
4
|
+
toolCalls?: ToolCallRequest[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic mock provider for tests and offline demos.
|
|
8
|
+
* Never calls the network.
|
|
9
|
+
* Optional `script` advances one step per chat() for coding-loop tests.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MockModelProvider implements ModelProvider {
|
|
12
|
+
readonly id: "mock";
|
|
13
|
+
private turn;
|
|
14
|
+
private readonly script;
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
script?: MockScriptStep[];
|
|
17
|
+
});
|
|
18
|
+
metadata(): ModelMetadata;
|
|
19
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { redactForModel, redactMessages } from "../redact.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic mock provider for tests and offline demos.
|
|
4
|
+
* Never calls the network.
|
|
5
|
+
* Optional `script` advances one step per chat() for coding-loop tests.
|
|
6
|
+
*/
|
|
7
|
+
export class MockModelProvider {
|
|
8
|
+
id = "mock";
|
|
9
|
+
turn = 0;
|
|
10
|
+
script;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.script = options?.script;
|
|
13
|
+
}
|
|
14
|
+
metadata() {
|
|
15
|
+
return {
|
|
16
|
+
provider: "mock",
|
|
17
|
+
model: "mock",
|
|
18
|
+
supportsTools: true,
|
|
19
|
+
supportsStreaming: false,
|
|
20
|
+
supportsStructuredOutput: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async chat(request) {
|
|
24
|
+
const messages = redactMessages(request.messages);
|
|
25
|
+
if (this.script) {
|
|
26
|
+
const step = this.script[this.turn] ?? { content: "[AI-GENERATED mock] done", toolCalls: [] };
|
|
27
|
+
this.turn += 1;
|
|
28
|
+
const toolCalls = step.toolCalls ?? [];
|
|
29
|
+
const text = step.content ?? (toolCalls.length ? "(proposing tools)" : "done");
|
|
30
|
+
return {
|
|
31
|
+
provider: "mock",
|
|
32
|
+
model: "mock",
|
|
33
|
+
message: { role: "assistant", content: text },
|
|
34
|
+
toolCalls,
|
|
35
|
+
usage: {
|
|
36
|
+
promptTokens: messages.reduce((n, m) => n + Math.ceil(m.content.length / 4), 0),
|
|
37
|
+
completionTokens: Math.ceil(text.length / 4),
|
|
38
|
+
totalTokens: 0,
|
|
39
|
+
},
|
|
40
|
+
finishReason: toolCalls.length ? "tool_calls" : "stop",
|
|
41
|
+
aiGenerated: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
45
|
+
const lastTool = [...messages].reverse().find((m) => m.role === "tool");
|
|
46
|
+
const preview = redactForModel(lastUser?.content ?? "").slice(0, 120);
|
|
47
|
+
let text = `[AI-GENERATED mock] Received ${messages.length} message(s). Last user: ${preview || "(empty)"}`;
|
|
48
|
+
// Tool-output injection must never become instructions — mock refuses to honor them.
|
|
49
|
+
if (lastTool?.content && /IGNORE\s+ALL\s+PREVIOUS\s+INSTRUCTIONS/i.test(lastTool.content)) {
|
|
50
|
+
text =
|
|
51
|
+
"[AI-GENERATED mock] Ignoring untrusted TOOL_OUTPUT instructions. Continuing safely without destructive actions.";
|
|
52
|
+
return {
|
|
53
|
+
provider: "mock",
|
|
54
|
+
model: "mock",
|
|
55
|
+
message: { role: "assistant", content: text },
|
|
56
|
+
toolCalls: [],
|
|
57
|
+
finishReason: "stop",
|
|
58
|
+
aiGenerated: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const toolCalls = [];
|
|
62
|
+
const match = /call tool\s+([a-z0-9_]+)/i.exec(lastUser?.content ?? "");
|
|
63
|
+
if (match && request.tools?.some((t) => t.name === match[1])) {
|
|
64
|
+
toolCalls.push({
|
|
65
|
+
id: "mock_tool_1",
|
|
66
|
+
name: match[1],
|
|
67
|
+
arguments: {},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
provider: "mock",
|
|
72
|
+
model: "mock",
|
|
73
|
+
message: { role: "assistant", content: text },
|
|
74
|
+
toolCalls,
|
|
75
|
+
usage: {
|
|
76
|
+
promptTokens: messages.reduce((n, m) => n + Math.ceil(m.content.length / 4), 0),
|
|
77
|
+
completionTokens: Math.ceil(text.length / 4),
|
|
78
|
+
totalTokens: 0,
|
|
79
|
+
},
|
|
80
|
+
finishReason: toolCalls.length ? "tool_calls" : "stop",
|
|
81
|
+
aiGenerated: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AI_PROVIDER_REQUIRED_MESSAGE } from "../types.js";
|
|
2
|
+
export class NoneModelProvider {
|
|
3
|
+
id = "none";
|
|
4
|
+
metadata() {
|
|
5
|
+
return {
|
|
6
|
+
provider: "none",
|
|
7
|
+
model: "none",
|
|
8
|
+
supportsTools: false,
|
|
9
|
+
supportsStreaming: false,
|
|
10
|
+
supportsStructuredOutput: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async chat(_request) {
|
|
14
|
+
return {
|
|
15
|
+
provider: "none",
|
|
16
|
+
model: "none",
|
|
17
|
+
message: { role: "assistant", content: "" },
|
|
18
|
+
toolCalls: [],
|
|
19
|
+
aiGenerated: true,
|
|
20
|
+
error: AI_PROVIDER_REQUIRED_MESSAGE,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResponse, ModelMetadata, ModelProvider } from "../types.js";
|
|
2
|
+
export interface OpenAiCompatibleOptions {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
model: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Minimal OpenAI-compatible Chat Completions client (custom base URL supported).
|
|
10
|
+
* Uses Node fetch. Does not silently fall back to another provider.
|
|
11
|
+
*/
|
|
12
|
+
export declare class OpenAiCompatibleProvider implements ModelProvider {
|
|
13
|
+
readonly id: "openai-compatible";
|
|
14
|
+
private readonly apiKey;
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
private readonly model;
|
|
17
|
+
private readonly timeoutMs;
|
|
18
|
+
constructor(options: OpenAiCompatibleOptions);
|
|
19
|
+
metadata(): ModelMetadata;
|
|
20
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { redactMessages } from "../redact.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal OpenAI-compatible Chat Completions client (custom base URL supported).
|
|
4
|
+
* Uses Node fetch. Does not silently fall back to another provider.
|
|
5
|
+
*/
|
|
6
|
+
export class OpenAiCompatibleProvider {
|
|
7
|
+
id = "openai-compatible";
|
|
8
|
+
apiKey;
|
|
9
|
+
baseUrl;
|
|
10
|
+
model;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
15
|
+
this.model = options.model;
|
|
16
|
+
this.timeoutMs = options.timeoutMs ?? 60_000;
|
|
17
|
+
}
|
|
18
|
+
metadata() {
|
|
19
|
+
return {
|
|
20
|
+
provider: "openai-compatible",
|
|
21
|
+
model: this.model,
|
|
22
|
+
supportsTools: true,
|
|
23
|
+
supportsStreaming: false,
|
|
24
|
+
supportsStructuredOutput: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async chat(request) {
|
|
28
|
+
if (!this.apiKey &&
|
|
29
|
+
!this.baseUrl.includes("localhost") &&
|
|
30
|
+
!this.baseUrl.includes("127.0.0.1")) {
|
|
31
|
+
return {
|
|
32
|
+
provider: "openai-compatible",
|
|
33
|
+
model: this.model,
|
|
34
|
+
message: { role: "assistant", content: "" },
|
|
35
|
+
toolCalls: [],
|
|
36
|
+
aiGenerated: true,
|
|
37
|
+
error: "AI provider unavailable: AGENTDOCTOR_AI_API_KEY is not set.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const messages = redactMessages(request.messages).map((m) => ({
|
|
41
|
+
role: m.role === "tool" ? "tool" : m.role,
|
|
42
|
+
content: m.content,
|
|
43
|
+
...(m.toolCallId ? { tool_call_id: m.toolCallId } : {}),
|
|
44
|
+
...(m.name ? { name: m.name } : {}),
|
|
45
|
+
}));
|
|
46
|
+
const body = {
|
|
47
|
+
model: request.model ?? this.model,
|
|
48
|
+
messages,
|
|
49
|
+
temperature: request.temperature ?? 0.2,
|
|
50
|
+
max_tokens: request.maxTokens ?? 2048,
|
|
51
|
+
};
|
|
52
|
+
if (request.tools && request.tools.length > 0) {
|
|
53
|
+
body.tools = request.tools.map((t) => ({
|
|
54
|
+
type: "function",
|
|
55
|
+
function: {
|
|
56
|
+
name: t.name,
|
|
57
|
+
description: t.description,
|
|
58
|
+
parameters: t.parameters,
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
64
|
+
const onAbort = () => controller.abort();
|
|
65
|
+
request.signal?.addEventListener("abort", onAbort);
|
|
66
|
+
try {
|
|
67
|
+
const headers = {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
};
|
|
70
|
+
if (this.apiKey) {
|
|
71
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
72
|
+
}
|
|
73
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers,
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const errText = await res.text().catch(() => "");
|
|
81
|
+
return {
|
|
82
|
+
provider: "openai-compatible",
|
|
83
|
+
model: this.model,
|
|
84
|
+
message: { role: "assistant", content: "" },
|
|
85
|
+
toolCalls: [],
|
|
86
|
+
aiGenerated: true,
|
|
87
|
+
error: `AI provider unavailable: HTTP ${res.status}${errText ? ` (${errText.slice(0, 200)})` : ""}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const data = (await res.json());
|
|
91
|
+
const choice = data.choices?.[0];
|
|
92
|
+
const rawTools = choice?.message?.tool_calls ?? [];
|
|
93
|
+
const toolCalls = rawTools.map((tc) => {
|
|
94
|
+
let args = {};
|
|
95
|
+
try {
|
|
96
|
+
args = JSON.parse(tc.function?.arguments ?? "{}");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
args = {};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: tc.id,
|
|
103
|
+
name: tc.function?.name ?? "unknown",
|
|
104
|
+
arguments: args,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
const message = {
|
|
108
|
+
role: "assistant",
|
|
109
|
+
content: String(choice?.message?.content ?? ""),
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
provider: "openai-compatible",
|
|
113
|
+
model: data.model ?? this.model,
|
|
114
|
+
message,
|
|
115
|
+
toolCalls,
|
|
116
|
+
...(data.usage
|
|
117
|
+
? {
|
|
118
|
+
usage: {
|
|
119
|
+
...(data.usage.prompt_tokens !== undefined
|
|
120
|
+
? { promptTokens: data.usage.prompt_tokens }
|
|
121
|
+
: {}),
|
|
122
|
+
...(data.usage.completion_tokens !== undefined
|
|
123
|
+
? { completionTokens: data.usage.completion_tokens }
|
|
124
|
+
: {}),
|
|
125
|
+
...(data.usage.total_tokens !== undefined
|
|
126
|
+
? { totalTokens: data.usage.total_tokens }
|
|
127
|
+
: {}),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
...(choice?.finish_reason ? { finishReason: choice.finish_reason } : {}),
|
|
132
|
+
aiGenerated: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
137
|
+
return {
|
|
138
|
+
provider: "openai-compatible",
|
|
139
|
+
model: this.model,
|
|
140
|
+
message: { role: "assistant", content: "" },
|
|
141
|
+
toolCalls: [],
|
|
142
|
+
aiGenerated: true,
|
|
143
|
+
error: `AI provider unavailable: ${msg}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|