@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/types/cli/setup-cli.d.ts +15 -1
  3. package/dist/types/commands/setup.d.ts +6 -0
  4. package/dist/types/config/settings-schema.d.ts +9 -0
  5. package/dist/types/decisions/index.d.ts +18 -0
  6. package/dist/types/decisions/llm-backend.d.ts +51 -0
  7. package/dist/types/decisions/skill-routing.d.ts +8 -0
  8. package/dist/types/decisions/types.d.ts +91 -0
  9. package/dist/types/decisions/typesafe-backend.d.ts +15 -0
  10. package/dist/types/hooks/skill-state.d.ts +6 -0
  11. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  12. package/dist/types/modes/components/typesafe-key-prompt.d.ts +23 -0
  13. package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +3 -1
  14. package/dist/types/session/agent-session.d.ts +0 -9
  15. package/dist/types/setup/decision-provider.d.ts +24 -0
  16. package/package.json +7 -7
  17. package/scripts/eval-skill-routing.ts +172 -0
  18. package/src/cli/setup-cli.ts +53 -1
  19. package/src/commands/setup.ts +5 -0
  20. package/src/config/settings-schema.ts +12 -0
  21. package/src/decisions/index.ts +84 -0
  22. package/src/decisions/llm-backend.ts +356 -0
  23. package/src/decisions/skill-routing.ts +83 -0
  24. package/src/decisions/types.ts +119 -0
  25. package/src/decisions/typesafe-backend.ts +168 -0
  26. package/src/hooks/skill-keywords.ts +56 -0
  27. package/src/hooks/skill-state.ts +18 -2
  28. package/src/modes/components/provider-onboarding-selector.ts +13 -1
  29. package/src/modes/components/typesafe-key-prompt.ts +108 -0
  30. package/src/modes/controllers/selector-controller.ts +44 -0
  31. package/src/sdk/bus/native-runtime-compatibility.ts +30 -3
  32. package/src/session/agent-session.ts +51 -1
  33. 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 { buildSkillStopOutput, ensureWorkflowSkillActivationState } from "../hooks/skill-state";
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
+ }