@sayknow-cli/coding-agent 0.5.21 → 0.5.22
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 +15 -0
- package/dist/types/cli/setup-cli.d.ts +15 -1
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/decisions/index.d.ts +18 -0
- package/dist/types/decisions/llm-backend.d.ts +51 -0
- package/dist/types/decisions/skill-routing.d.ts +8 -0
- package/dist/types/decisions/types.d.ts +91 -0
- package/dist/types/decisions/typesafe-backend.d.ts +15 -0
- package/dist/types/hooks/skill-state.d.ts +6 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/typesafe-key-prompt.d.ts +23 -0
- package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +0 -9
- package/dist/types/setup/decision-provider.d.ts +24 -0
- package/package.json +7 -7
- package/scripts/eval-skill-routing.ts +172 -0
- package/src/cli/setup-cli.ts +53 -1
- package/src/commands/setup.ts +5 -0
- package/src/config/settings-schema.ts +12 -0
- package/src/decisions/index.ts +84 -0
- package/src/decisions/llm-backend.ts +356 -0
- package/src/decisions/skill-routing.ts +83 -0
- package/src/decisions/types.ts +119 -0
- package/src/decisions/typesafe-backend.ts +168 -0
- package/src/hooks/skill-keywords.ts +56 -0
- package/src/hooks/skill-state.ts +18 -2
- package/src/modes/components/provider-onboarding-selector.ts +13 -1
- package/src/modes/components/typesafe-key-prompt.ts +108 -0
- package/src/modes/controllers/selector-controller.ts +44 -0
- package/src/sdk/bus/native-runtime-compatibility.ts +30 -3
- package/src/session/agent-session.ts +51 -1
- package/src/setup/decision-provider.ts +94 -0
|
@@ -202,6 +202,8 @@ import { onAppendOnlyModeChanged } from "../config/settings";
|
|
|
202
202
|
import type { SettingPath } from "../config/settings-schema";
|
|
203
203
|
import { getDefault } from "../config/settings-schema";
|
|
204
204
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
205
|
+
import { createDecisionService } from "../decisions";
|
|
206
|
+
import { createSemanticSkillRouter, type SkillRouter } from "../decisions/skill-routing";
|
|
205
207
|
import { loadCapability } from "../discovery";
|
|
206
208
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
207
209
|
import { MAX_EDIT_FILE_BYTES } from "../edit/read-file";
|
|
@@ -260,7 +262,11 @@ import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slas
|
|
|
260
262
|
import { GoalRuntime } from "../goals/runtime";
|
|
261
263
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
262
264
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
263
|
-
import {
|
|
265
|
+
import {
|
|
266
|
+
buildSkillStopOutput,
|
|
267
|
+
detectPrimarySkillKeyword,
|
|
268
|
+
ensureWorkflowSkillActivationState,
|
|
269
|
+
} from "../hooks/skill-state";
|
|
264
270
|
import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
265
271
|
import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
|
|
266
272
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
@@ -1842,6 +1848,9 @@ export class AgentSession {
|
|
|
1842
1848
|
// Model registry for API key resolution
|
|
1843
1849
|
#modelRegistry: ModelRegistry;
|
|
1844
1850
|
|
|
1851
|
+
/** Built on first use; the decision service resolves model and credential lazily. */
|
|
1852
|
+
#semanticSkillRouter?: SkillRouter;
|
|
1853
|
+
|
|
1845
1854
|
// Tool registry and prompt builder for extensions
|
|
1846
1855
|
#toolRegistry: Map<string, AgentTool>;
|
|
1847
1856
|
#workflowGateToolSession: ToolSession | undefined;
|
|
@@ -7548,6 +7557,41 @@ export class AgentSession {
|
|
|
7548
7557
|
* @throws Error if streaming and no streamingBehavior specified
|
|
7549
7558
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
7550
7559
|
*/
|
|
7560
|
+
/**
|
|
7561
|
+
* Second-stage workflow routing for prompts the keyword table cannot see.
|
|
7562
|
+
*
|
|
7563
|
+
* Runs in this process, not the hook process: the hook only receives paths and
|
|
7564
|
+
* config, so it has no model registry and no credentials to call anything with.
|
|
7565
|
+
*
|
|
7566
|
+
* Deliberately best-effort — a disabled setting, a missing credential, a timeout or
|
|
7567
|
+
* a nonsense answer all resolve to "no activation", which is precisely the
|
|
7568
|
+
* behaviour before this stage existed.
|
|
7569
|
+
*/
|
|
7570
|
+
async #routeWorkflowSemantically(text: string): Promise<void> {
|
|
7571
|
+
if (!this.settings.get("decisions.enabled")) return;
|
|
7572
|
+
if (detectPrimarySkillKeyword(text)) return; // deterministic stage already decided
|
|
7573
|
+
try {
|
|
7574
|
+
this.#semanticSkillRouter ??= createSemanticSkillRouter(
|
|
7575
|
+
createDecisionService({
|
|
7576
|
+
registry: this.#modelRegistry,
|
|
7577
|
+
settings: this.settings,
|
|
7578
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
7579
|
+
enabled: true,
|
|
7580
|
+
}),
|
|
7581
|
+
);
|
|
7582
|
+
const skill = await this.#semanticSkillRouter(text);
|
|
7583
|
+
if (!skill) return;
|
|
7584
|
+
await ensureWorkflowSkillActivationState({
|
|
7585
|
+
cwd: this.sessionManager.getCwd(),
|
|
7586
|
+
skill,
|
|
7587
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
7588
|
+
});
|
|
7589
|
+
this.#attachAskTool();
|
|
7590
|
+
} catch (error) {
|
|
7591
|
+
logger.debug("agent-session: semantic workflow routing failed", { error: String(error) });
|
|
7592
|
+
}
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7551
7595
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
7552
7596
|
this.#assertRecoveryHydrationPromoted();
|
|
7553
7597
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
@@ -7606,6 +7650,12 @@ export class AgentSession {
|
|
|
7606
7650
|
const deepInterviewUserIntentEpoch =
|
|
7607
7651
|
claimsGenuineUserIntent && !this.isStreaming ? this.#claimDeepInterviewUserIntent() : undefined;
|
|
7608
7652
|
|
|
7653
|
+
// The keyword table in `hooks/skill-keywords.ts` is thirteen literal strings, so a
|
|
7654
|
+
// Korean phrasing of "plan this before you touch code" activates nothing. Ask a
|
|
7655
|
+
// cheap model only when the deterministic stage found nothing, and only for real
|
|
7656
|
+
// user turns. Any failure leaves routing to the system prompt, exactly as before.
|
|
7657
|
+
if (claimsGenuineUserIntent && !this.isStreaming) await this.#routeWorkflowSemantically(expandedText);
|
|
7658
|
+
|
|
7609
7659
|
// If streaming, queue via steer() or followUp() based on option
|
|
7610
7660
|
if (this.isStreaming) {
|
|
7611
7661
|
if (!options?.streamingBehavior) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store a TypeSafe key so the typed-decision service can use the hosted model.
|
|
3
|
+
*
|
|
4
|
+
* TypeSafe is **not** a chat provider: no streaming, no messages, no text output. So it
|
|
5
|
+
* must not be written into `models.yml` the way `addApiCompatibleProvider` writes
|
|
6
|
+
* OpenAI-compatible endpoints — that would invent a chat model that cannot answer a
|
|
7
|
+
* chat request and would show up in the model picker as something users could select.
|
|
8
|
+
*
|
|
9
|
+
* Only the credential is stored. The decision backend reads it through the same
|
|
10
|
+
* `getApiKeyForProvider` path every other provider uses, so adding a key turns the
|
|
11
|
+
* backend on and removing it turns the backend off, with nothing else to configure.
|
|
12
|
+
*/
|
|
13
|
+
import { AuthStorage } from "@sayknow-cli/ai";
|
|
14
|
+
import { getAgentDbPath } from "@sayknow-cli/utils";
|
|
15
|
+
import { TYPESAFE_PROVIDER } from "../decisions";
|
|
16
|
+
|
|
17
|
+
const VERIFY_ENDPOINT = "https://api.typesafe.ai/v1/models";
|
|
18
|
+
const VERIFY_TIMEOUT_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
export interface TypeSafeKeySetupResult {
|
|
21
|
+
provider: string;
|
|
22
|
+
verified: boolean;
|
|
23
|
+
/** Present when verification ran and failed; the key is not stored in that case. */
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verify a key against the live API before storing it.
|
|
29
|
+
*
|
|
30
|
+
* Storing an unverified key is worse than refusing it: the decision service fails open,
|
|
31
|
+
* so a bad key produces no error anywhere — it silently falls back to the user's own
|
|
32
|
+
* model forever, and the user believes TypeSafe is active.
|
|
33
|
+
*/
|
|
34
|
+
export async function verifyTypeSafeKey(apiKey: string, fetchImpl: typeof fetch = fetch): Promise<string | null> {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetchImpl(VERIFY_ENDPOINT, {
|
|
39
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (response.ok) return null;
|
|
43
|
+
if (response.status === 401 || response.status === 403) return "key rejected by TypeSafe (401/403)";
|
|
44
|
+
return `TypeSafe returned ${response.status}`;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return `could not reach TypeSafe: ${error instanceof Error ? error.message : String(error)}`;
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SetTypeSafeKeyOptions {
|
|
53
|
+
apiKey: string;
|
|
54
|
+
/** Skip the live check. Only for offline setup; the key may be wrong. */
|
|
55
|
+
skipVerify?: boolean;
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
57
|
+
dbPath?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function setTypeSafeKey(options: SetTypeSafeKeyOptions): Promise<TypeSafeKeySetupResult> {
|
|
61
|
+
const apiKey = options.apiKey.trim();
|
|
62
|
+
if (!apiKey) throw new Error("TypeSafe API key must not be empty");
|
|
63
|
+
|
|
64
|
+
let verified = false;
|
|
65
|
+
if (!options.skipVerify) {
|
|
66
|
+
const failure = await verifyTypeSafeKey(apiKey, options.fetchImpl ?? fetch);
|
|
67
|
+
if (failure) return { provider: TYPESAFE_PROVIDER, verified: false, error: failure };
|
|
68
|
+
verified = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const authStorage = await AuthStorage.create(options.dbPath ?? getAgentDbPath());
|
|
72
|
+
try {
|
|
73
|
+
await authStorage.set(TYPESAFE_PROVIDER, { type: "api_key", key: apiKey });
|
|
74
|
+
} finally {
|
|
75
|
+
authStorage.close();
|
|
76
|
+
}
|
|
77
|
+
return { provider: TYPESAFE_PROVIDER, verified };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function removeTypeSafeKey(dbPath?: string): Promise<void> {
|
|
81
|
+
const authStorage = await AuthStorage.create(dbPath ?? getAgentDbPath());
|
|
82
|
+
try {
|
|
83
|
+
await authStorage.remove(TYPESAFE_PROVIDER);
|
|
84
|
+
} finally {
|
|
85
|
+
authStorage.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatTypeSafeKeyResult(result: TypeSafeKeySetupResult): string {
|
|
90
|
+
if (result.error) return `✗ TypeSafe key not stored: ${result.error}`;
|
|
91
|
+
return result.verified
|
|
92
|
+
? "✓ TypeSafe key verified and stored. Typed decisions will use the hosted model."
|
|
93
|
+
: "✓ TypeSafe key stored (unverified). Typed decisions will try the hosted model.";
|
|
94
|
+
}
|