@inneranimalmedia/agentsam-sdk 2.6.9 → 2.6.10

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 (176) hide show
  1. package/README.md +18 -0
  2. package/apps/cad-creator/shared/cad/package.json +1 -1
  3. package/apps/client-cms-editor/backend/package.json +3 -3
  4. package/apps/client-cms-editor/frontend/package.json +4 -4
  5. package/apps/client-cms-editor/package-lock.json +8 -8
  6. package/apps/client-cms-editor/package.json +1 -1
  7. package/apps/client-cms-editor/shared/cms/package.json +2 -2
  8. package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
  9. package/examples/hyperspace-demo/index.html +16 -0
  10. package/examples/hyperspace-demo/package.json +21 -0
  11. package/examples/hyperspace-demo/src/main.tsx +160 -0
  12. package/examples/hyperspace-demo/tsconfig.json +9 -0
  13. package/examples/hyperspace-demo/vite.config.ts +24 -0
  14. package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
  15. package/package.json +23 -7
  16. package/packages/agentsam-brand/package.json +1 -1
  17. package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
  18. package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
  19. package/packages/agentsam-contracts/package.json +1 -1
  20. package/packages/agentsam-contracts/src/errors.ts +19 -10
  21. package/packages/agentsam-errors/package.json +11 -3
  22. package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
  23. package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
  24. package/packages/agentsam-errors/src/envelope.js +63 -11
  25. package/packages/agentsam-errors/src/error.js +6 -0
  26. package/packages/agentsam-errors/src/fingerprint.js +9 -1
  27. package/packages/agentsam-errors/src/normalize.js +51 -6
  28. package/packages/agentsam-errors/src/recovery.js +9 -35
  29. package/packages/agentsam-errors/src/transport.js +6 -1
  30. package/packages/agentsam-errors/src/vocabulary.js +515 -126
  31. package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
  32. package/packages/agentsam-errors/types/index.d.ts +157 -0
  33. package/packages/agentsam-goap/package.json +2 -2
  34. package/packages/agentsam-hooks/README.md +206 -0
  35. package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
  36. package/packages/agentsam-hooks/bindings/go/command.go +55 -0
  37. package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
  38. package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
  39. package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
  40. package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
  41. package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
  42. package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
  43. package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
  44. package/packages/agentsam-hooks/package.json +69 -0
  45. package/packages/agentsam-hooks/schema/manifest.json +54 -0
  46. package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
  47. package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
  48. package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
  49. package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
  50. package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
  51. package/packages/agentsam-hooks/src/adapters/command.js +104 -0
  52. package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
  53. package/packages/agentsam-hooks/src/adapters/http.js +58 -0
  54. package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
  55. package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
  56. package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
  57. package/packages/agentsam-hooks/src/config.js +143 -0
  58. package/packages/agentsam-hooks/src/contracts.js +284 -0
  59. package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
  60. package/packages/agentsam-hooks/src/index.d.ts +193 -0
  61. package/packages/agentsam-hooks/src/index.js +14 -0
  62. package/packages/agentsam-hooks/src/policy.js +34 -0
  63. package/packages/agentsam-hooks/src/runtime.js +312 -0
  64. package/packages/agentsam-hooks/src/store.js +264 -0
  65. package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
  66. package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
  67. package/packages/agentsam-knowledge/package.json +1 -1
  68. package/packages/agentsam-nav/dist/Nav.d.ts +43 -0
  69. package/packages/agentsam-nav/dist/NavCollapsible.d.ts +10 -0
  70. package/packages/agentsam-nav/dist/NavContent.d.ts +2 -0
  71. package/packages/agentsam-nav/dist/NavFooter.d.ts +2 -0
  72. package/packages/agentsam-nav/dist/NavGroup.d.ts +3 -0
  73. package/packages/agentsam-nav/dist/NavHeader.d.ts +2 -0
  74. package/packages/agentsam-nav/dist/NavMenu.d.ts +22 -0
  75. package/packages/agentsam-nav/dist/NavProvider.d.ts +35 -0
  76. package/packages/agentsam-nav/dist/NavTrigger.d.ts +2 -0
  77. package/packages/agentsam-nav/dist/Surfaces.d.ts +41 -0
  78. package/packages/agentsam-nav/dist/browser.d.ts +11 -0
  79. package/packages/agentsam-nav/dist/browser.global.js +135 -0
  80. package/packages/agentsam-nav/dist/browser.js +7290 -0
  81. package/packages/agentsam-nav/dist/contracts.d.ts +76 -0
  82. package/packages/agentsam-nav/dist/index.d.ts +11 -0
  83. package/packages/agentsam-nav/dist/index.js +7291 -0
  84. package/packages/agentsam-nav/dist/legacy.css +83 -0
  85. package/packages/agentsam-nav/dist/shell.css +113 -0
  86. package/packages/agentsam-nav/dist/theme.css +113 -0
  87. package/packages/agentsam-nav/dist/utils.d.ts +1 -0
  88. package/packages/agentsam-nav/package.json +1 -1
  89. package/packages/agentsam-queue-control/package.json +1 -1
  90. package/packages/agentsam-repository/package.json +1 -1
  91. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +670 -0
  92. package/packages/agentsam-vault/package.json +1 -1
  93. package/packages/agentsam-workbench/package.json +3 -3
  94. package/packages/connectors/cloudflare/package.json +1 -1
  95. package/packages/heuristic-theme/package.json +1 -1
  96. package/packages/identity/package.json +2 -2
  97. package/packages/providers/completeful/package.json +1 -1
  98. package/packages/theme-church-site/package.json +2 -2
  99. package/packages/theme-companions-site/package.json +2 -2
  100. package/packages/theme-floors-site/package.json +2 -2
  101. package/packages/theme-fuelnfree-site/package.json +2 -2
  102. package/packages/theme-handyman-site/package.json +2 -2
  103. package/packages/theme-insurance-site/package.json +2 -2
  104. package/packages/theme-scenes/package.json +1 -1
  105. package/packages/theme-shinshu-site/package.json +2 -2
  106. package/packages/work-graph/package.json +1 -1
  107. package/protocol/apps/agentsam.app.v1.schema.json +1 -1
  108. package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
  109. package/protocol/assets/asset-record.v1.schema.json +1 -1
  110. package/protocol/brand/brandpack.v1.schema.json +1 -1
  111. package/protocol/capabilities/capability-manifest.schema.json +1 -1
  112. package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
  113. package/protocol/credentials/issue.v1.schema.json +1 -1
  114. package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
  115. package/protocol/database/connection.v1.schema.json +1 -1
  116. package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
  117. package/protocol/errors/README.md +36 -0
  118. package/protocol/errors/agentsam.error.v2.schema.json +821 -0
  119. package/protocol/errors/error-catalog.json +3506 -158
  120. package/protocol/errors/error-envelope.schema.json +195 -52
  121. package/protocol/errors/recovery.v1.schema.json +1 -1
  122. package/protocol/features/agentsam.feature.v1.schema.json +1 -1
  123. package/protocol/hooks/README.md +18 -0
  124. package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
  125. package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
  126. package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
  127. package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
  128. package/protocol/local-engine/benchmark.v1.schema.json +1 -1
  129. package/protocol/local-engine/engine.v1.schema.json +1 -1
  130. package/protocol/models/model-inventory-v2.schema.json +1 -1
  131. package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
  132. package/protocol/rpc/v1/errors.proto +1 -0
  133. package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
  134. package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
  135. package/protocol/sam/activity.v1.schema.json +1 -1
  136. package/protocol/sam/answer.v1.schema.json +1 -1
  137. package/protocol/sam/calibration.v1.schema.json +1 -1
  138. package/protocol/sam/decision-receipt.v1.schema.json +1 -1
  139. package/protocol/sam/evaluation.v1.schema.json +1 -1
  140. package/protocol/sam/operation.schema.json +1 -1
  141. package/protocol/sam/outcome.v1.schema.json +1 -1
  142. package/protocol/sam/question.v1.schema.json +1 -1
  143. package/protocol/sam/result.schema.json +1 -1
  144. package/protocol/sam/state.v1.schema.json +1 -1
  145. package/protocol/scoring/scorecard.v1.schema.json +1 -1
  146. package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
  147. package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
  148. package/protocol/theme-refinery/product-metadata.schema.json +1 -1
  149. package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
  150. package/protocol/ui/icon.v1.schema.json +1 -1
  151. package/python/README.md +20 -0
  152. package/python/agentsam_sdk/errors/__init__.py +4 -0
  153. package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
  154. package/python/agentsam_sdk/hooks/__init__.py +32 -0
  155. package/python/agentsam_sdk/hooks/adapters.py +132 -0
  156. package/python/agentsam_sdk/hooks/contracts.py +121 -0
  157. package/python/agentsam_sdk/hooks/runtime.py +253 -0
  158. package/python/tests/test_hooks.py +115 -0
  159. package/src/agent/responses-runner.js +145 -1
  160. package/src/cli/command-catalog.js +1 -0
  161. package/src/cli.js +9 -0
  162. package/src/commands/cheat-sheet.js +1 -0
  163. package/src/commands/hooks.js +260 -0
  164. package/src/commands/models.js +1 -1
  165. package/src/commands/shell.js +114 -9
  166. package/src/errors/diagnostic.js +9 -9
  167. package/src/index.js +32 -0
  168. package/src/knowledge/service/grpc-codec.js +2 -0
  169. package/src/lib/slash-commands.js +1 -0
  170. package/src/local/runtime-store.js +46 -0
  171. package/src/models/catalog.js +0 -49
  172. package/src/models/inventory-core.js +69 -0
  173. package/src/plugins/index.js +8 -1
  174. package/src/plugins/registry.js +56 -0
  175. package/src/rpc/generated/errors_pb.js +49 -1
  176. package/src/tools/runtime.js +13 -5
@@ -7,6 +7,11 @@ import { searchToolCards, hydrateToolSchemas } from '../tools/index.js';
7
7
  import { createAgentEvent } from '../telemetry/index.js';
8
8
  import { diagnosticFromError } from '../errors/index.js';
9
9
  import { runAgentSamModelTurn } from './model-turn.js';
10
+ import {
11
+ createHookedCapabilityAdapter,
12
+ createHookedProviderAdapter,
13
+ ensureHookRuntime,
14
+ } from '../../packages/agentsam-hooks/src/index.js';
10
15
 
11
16
  const RUNTIME_OWNED_KEYS = new Set(['account_id', 'user_id', 'tenant_id', 'workspace_id', 'connection_id', 'runtime_lease_id', 'execution_id']);
12
17
 
@@ -229,7 +234,7 @@ function toolOutputInput(outputs) {
229
234
  }));
230
235
  }
231
236
 
232
- export async function runAgentSamTurn(options = {}) {
237
+ async function runAgentSamTurnCore(options = {}) {
233
238
  const provider = options.provider;
234
239
  if (!provider?.create || !provider?.continueWithToolOutputs) throw new TypeError('Responses provider adapter is required');
235
240
  if (!options.capabilityAdapter) throw new TypeError('capabilityAdapter is required');
@@ -615,5 +620,144 @@ export async function runAgentSamTurn(options = {}) {
615
620
  });
616
621
  }
617
622
 
623
+ function hookInvocation(options = {}) {
624
+ return {
625
+ session_id: options.sessionId,
626
+ run_id: options.runId,
627
+ turn_id: options.turnId,
628
+ message_id: options.messageId,
629
+ agent_id: options.agentId,
630
+ source: options.source || 'agentsam-sdk',
631
+ metadata: options.hookMetadata,
632
+ };
633
+ }
634
+
635
+ /**
636
+ * Provider-neutral AgentSam turn with optional lifecycle instrumentation.
637
+ *
638
+ * Passing `hookRuntime` (or a plain `hooks` registry) instruments the model
639
+ * and capability adapters without teaching the orchestration loop any
640
+ * provider, MCP, LSP, or sub-agent wire format. An agent_stop hook may request
641
+ * another bounded turn; continuation reuses provider state and cumulative
642
+ * usage instead of constructing a hidden second session.
643
+ */
644
+ export async function runAgentSamTurn(options = {}) {
645
+ const hookSource = options.hookRuntime || options.hooks;
646
+ if (!hookSource) return runAgentSamTurnCore(options);
647
+
648
+ const hooks = ensureHookRuntime(hookSource);
649
+ const cwd = path.resolve(options.cwd || process.cwd());
650
+ const invocation = hookInvocation(options);
651
+ const submitted = await hooks.dispatch('user_prompt_submitted', {
652
+ prompt: clean(options.prompt),
653
+ cwd,
654
+ }, invocation, { cwd });
655
+ const submittedPrompt = clean(submitted.input.prompt);
656
+ if (!submittedPrompt) throw new TypeError('prompt is required');
657
+ const transformed = await hooks.dispatch('user_prompt_transformed', {
658
+ original_prompt: clean(options.prompt),
659
+ transformed_prompt: submittedPrompt,
660
+ cwd,
661
+ }, invocation, { cwd });
662
+ const prompt = clean(transformed.input.transformed_prompt);
663
+ if (!prompt) throw new TypeError('transformed prompt is required');
664
+ const additionalContext = [submitted.output.additional_context, transformed.output.additional_context]
665
+ .filter(Boolean).join('\n\n');
666
+ const instrumented = {
667
+ ...options,
668
+ prompt,
669
+ provider: createHookedProviderAdapter(options.provider, {
670
+ hookRuntime: hooks,
671
+ invocation,
672
+ cwd,
673
+ requestPermission: options.requestPermission,
674
+ maxRetries: options.maxHookRetries,
675
+ }),
676
+ capabilityAdapter: createHookedCapabilityAdapter(options.capabilityAdapter, {
677
+ hookRuntime: hooks,
678
+ invocation,
679
+ cwd,
680
+ requestPermission: options.requestPermission,
681
+ maxRetries: options.maxHookRetries,
682
+ }),
683
+ ...(additionalContext && options.instructions != null
684
+ ? { instructions: [String(options.instructions), additionalContext].filter(Boolean).join('\n\n') }
685
+ : {}),
686
+ ...(additionalContext && options.instructions == null
687
+ ? {
688
+ contextItems: [
689
+ ...(options.contextItems || []),
690
+ { kind: 'hook', ref: 'hook://user-prompt/context', priority: 95, content: additionalContext },
691
+ ],
692
+ }
693
+ : {}),
694
+ };
695
+
696
+ const maxContinuations = Number.isInteger(options.maxHookContinuations)
697
+ ? Math.max(0, Math.min(options.maxHookContinuations, 8))
698
+ : 4;
699
+ const continuationReceipts = [];
700
+ let current = instrumented;
701
+ let result;
702
+ let totalCost = 0;
703
+ let totalRounds = 0;
704
+ let totalCalls = 0;
705
+ let totalElapsedMs = 0;
706
+ let hookContinuations = 0;
707
+ const costBreakdown = { input: 0, cached_input: 0, cache_write: 0, output: 0 };
708
+ const allToolReceipts = [];
709
+ const allContinuationCompactions = [];
710
+
711
+ for (let continuationIndex = 0; ; continuationIndex += 1) {
712
+ result = await runAgentSamTurnCore(current);
713
+ totalCost += Number(result.total_cost_usd || 0);
714
+ totalRounds += Number(result.run_budget?.tool_rounds || 0);
715
+ totalCalls += Number(result.run_budget?.tool_calls || 0);
716
+ totalElapsedMs += Number(result.run_budget?.elapsed_ms || 0);
717
+ for (const key of Object.keys(costBreakdown)) costBreakdown[key] += Number(result.cost_breakdown_usd?.[key] || 0);
718
+ allToolReceipts.push(...(result.tool_receipts || []));
719
+ allContinuationCompactions.push(...(result.continuation_compactions || []));
720
+ const stopped = await hooks.dispatch('agent_stop', {
721
+ stop_reason: result.output_text ? 'completed' : 'no_output',
722
+ stop_hook_active: continuationIndex > 0,
723
+ output_text: result.output_text,
724
+ continuation_index: continuationIndex,
725
+ run_budget: result.run_budget,
726
+ }, invocation, { cwd });
727
+ continuationReceipts.push(...stopped.receipts);
728
+ if (stopped.output.decision !== 'block') break;
729
+ if (continuationIndex >= maxContinuations) throw new Error(`hook_continuation_limit_exceeded:${maxContinuations}`);
730
+ const reason = clean(stopped.output.reason);
731
+ if (!reason) throw new Error('hook_continuation_reason_required');
732
+ hookContinuations += 1;
733
+ current = {
734
+ ...instrumented,
735
+ prompt: reason,
736
+ previousProviderState: result.provider_state,
737
+ previousUsageSnapshot: result.usage_snapshot,
738
+ cumulativeUsage: result.cumulative_usage,
739
+ };
740
+ }
741
+
742
+ return Object.freeze({
743
+ ...result,
744
+ ...(submitted.output.suppress_output === true || transformed.output.suppress_output === true
745
+ ? { output_text: '', suppressed_by_hook: true } : {}),
746
+ total_cost_usd: totalCost,
747
+ cost_breakdown_usd: Object.freeze(costBreakdown),
748
+ tool_receipts: Object.freeze(allToolReceipts),
749
+ continuation_compactions: Object.freeze(allContinuationCompactions),
750
+ run_budget: Object.freeze({
751
+ ...result.run_budget,
752
+ tool_rounds: totalRounds,
753
+ tool_calls: totalCalls,
754
+ elapsed_ms: totalElapsedMs,
755
+ hook_continuations: hookContinuations,
756
+ max_hook_continuations: maxContinuations,
757
+ }),
758
+ hook_receipts: Object.freeze([...submitted.receipts, ...transformed.receipts, ...continuationReceipts]),
759
+ });
760
+ }
761
+
618
762
  /** @deprecated Use runAgentSamTurn(). */
619
763
  export const runResponsesAgent = runAgentSamTurn;
@@ -47,6 +47,7 @@ export const CLI_COMMAND_CATALOG = Object.freeze([
47
47
  },
48
48
  { id: 'db', summary: 'Project-local SQLite status/init', topic: 'runtime', skill: 'agentsam-app-fundamentals' },
49
49
  { id: 'models', summary: 'Probe hosted/local model availability', topic: 'start', skill: 'agentsam-app-fundamentals', common: true },
50
+ { id: 'hooks', aliases: ['hook'], summary: 'Inspect lifecycle hooks, sources, and execution receipts', topic: 'runtime', skill: 'agentsam-app-fundamentals', common: true, usage: ['agentsam hooks status', 'agentsam hooks list', 'agentsam hooks executions [--limit N]', 'agentsam hooks init'] },
50
51
  { id: 'providers', summary: 'Configure machine provider credentials', topic: 'start', skill: 'agentsam-app-fundamentals' },
51
52
  { id: 'credentials', aliases: ['credential'], summary: 'Audit credentials, drift, and failure remediation (never prints secrets)', topic: 'start', skill: 'agentsam-app-fundamentals', common: true },
52
53
  { id: 'cheat-sheet', aliases: ['cheatsheet'], summary: 'Organized command groups (gcloud-like)', topic: 'start', skill: 'agentsam-app-fundamentals', common: true },
package/src/cli.js CHANGED
@@ -52,6 +52,7 @@ import { runLogin, runLogout } from './commands/account-auth.js';
52
52
  import { runPlugins } from './commands/plugins.js';
53
53
  import { runCredentials } from './commands/credentials.js';
54
54
  import { runCheatSheet } from './commands/cheat-sheet.js';
55
+ import { runHooks } from './commands/hooks.js';
55
56
  import { runCompute } from './commands/compute.js';
56
57
  import { runGoogleCloud } from './commands/google-cloud.js';
57
58
  import { runBilling } from './commands/billing.js';
@@ -503,6 +504,14 @@ if (command === '--version' || command === '-v') {
503
504
  reportCliError(e);
504
505
  process.exit(1);
505
506
  }
507
+ } else if (command === 'hooks' || command === 'hook') {
508
+ try {
509
+ const code = await runHooks(rest);
510
+ if (typeof code === 'number' && code !== 0) process.exitCode = code;
511
+ } catch (e) {
512
+ reportCliError(e);
513
+ process.exitCode = 1;
514
+ }
506
515
  } else if (command === 'app' || command === 'apps') {
507
516
  try {
508
517
  await runApp(rest);
@@ -75,6 +75,7 @@ const GROUPS = Object.freeze([
75
75
  label: 'AI',
76
76
  commands: [
77
77
  ['models', 'Hosted + local model probes'],
78
+ ['hooks', 'Lifecycle sources, dispatch order, and receipts'],
78
79
  ['skills', 'Portable skill vocabulary'],
79
80
  ['context', 'Context economics + git bridge'],
80
81
  ['usage / session', 'Spend and resume receipts'],
@@ -0,0 +1,260 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ HOOK_CONFIG_SCHEMA,
5
+ createHookRuntime,
6
+ createHookRuntimeFromConfig,
7
+ findHookConfig,
8
+ loadHookConfig,
9
+ registerStoredHooks,
10
+ } from '../../packages/agentsam-hooks/src/index.js';
11
+ import { findCliProjectRoot } from '../lib/cli-preferences.js';
12
+ import { readAccountSession } from '../lib/account-session.js';
13
+ import {
14
+ listRuntimeHooks,
15
+ listRuntimeHookExecutions,
16
+ recordRuntimeHookReceipt,
17
+ setRuntimeHookActive,
18
+ } from '../local/runtime-store.js';
19
+
20
+ function clean(value) { return value == null ? '' : String(value).trim(); }
21
+ function writeLine(write, value = '') { write(`${value}\n`); }
22
+ function countConfigured(config) {
23
+ return Object.values(config?.hooks || {}).reduce((sum, rows) => sum + rows.length, 0);
24
+ }
25
+
26
+ function registerCodeHooks(runtime, hooks = {}) {
27
+ for (const [event, definitions] of Object.entries(hooks || {})) {
28
+ for (const definition of Array.isArray(definitions) ? definitions : [definitions]) {
29
+ if (typeof definition === 'function') {
30
+ runtime.register(event, { handler: definition, metadata: { hook_source: 'code' } });
31
+ } else {
32
+ runtime.register(event, {
33
+ ...definition,
34
+ metadata: { ...(definition?.metadata || {}), hook_source: 'code' },
35
+ });
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Resolve code, project config, and stored definitions into one ordered runtime.
43
+ * The source changes authoring/ownership only; dispatch semantics stay identical.
44
+ */
45
+ export async function createProjectHookRuntime(options = {}) {
46
+ const cwd = path.resolve(options.cwd || process.cwd());
47
+ const projectRoot = path.resolve(options.projectRoot || findCliProjectRoot(cwd));
48
+ const accountId = clean(options.ownerId || readAccountSession({ home: options.home })?.account_id) || 'local';
49
+ const sourceByHook = new Map();
50
+ const persistReceipts = options.persistReceipts !== false;
51
+ const runtime = createHookRuntime({
52
+ clock: options.clock,
53
+ async onReceipt(receipt) {
54
+ const definition = sourceByHook.get(`${receipt.hook}:${receipt.hook_id}`) || { source: 'code', stored_hook_id: null };
55
+ if (persistReceipts) {
56
+ await recordRuntimeHookReceipt({
57
+ projectRoot,
58
+ owner_id: accountId,
59
+ receipt,
60
+ source_kind: definition.source,
61
+ hook_id: definition.stored_hook_id,
62
+ session_id: options.sessionId,
63
+ conversation_id: options.conversationId,
64
+ });
65
+ }
66
+ if (typeof options.onReceipt === 'function') await options.onReceipt(receipt);
67
+ },
68
+ });
69
+
70
+ registerCodeHooks(runtime, options.hooks);
71
+ const configFile = options.configFile || findHookConfig(cwd, { stopAt: projectRoot });
72
+ let config = null;
73
+ if (configFile) {
74
+ ({ config } = createHookRuntimeFromConfig(configFile, {
75
+ runtime,
76
+ cwd,
77
+ env: options.env,
78
+ fetchImpl: options.fetchImpl,
79
+ restrictCommandCwd: options.restrictCommandCwd,
80
+ }));
81
+ }
82
+
83
+ const stored = await registerStoredHooks(runtime, {
84
+ listHooks: (query) => listRuntimeHooks({
85
+ projectRoot,
86
+ owner_id: accountId,
87
+ ...query,
88
+ }),
89
+ }, {
90
+ cwd: projectRoot,
91
+ env: options.env,
92
+ fetchImpl: options.fetchImpl,
93
+ restrictCommandCwd: options.restrictCommandCwd,
94
+ resolveHandler: options.resolveHandler,
95
+ context: {
96
+ owner_id: accountId,
97
+ account_id: accountId,
98
+ project_id: options.projectId,
99
+ project_root: projectRoot,
100
+ repository_id: options.repositoryId,
101
+ session_id: options.sessionId,
102
+ },
103
+ });
104
+
105
+ for (const definition of runtime.list()) {
106
+ sourceByHook.set(`${definition.hook}:${definition.id}`, {
107
+ source: clean(definition.metadata?.hook_source) || (definition.metadata?.stored_hook_id ? 'stored' : definition.metadata?.config_source ? 'config' : 'code'),
108
+ stored_hook_id: clean(definition.metadata?.stored_hook_id) || null,
109
+ });
110
+ }
111
+ return Object.freeze({
112
+ runtime,
113
+ owner_id: accountId,
114
+ project_root: projectRoot,
115
+ config,
116
+ config_file: configFile || null,
117
+ stored,
118
+ definitions: runtime.list(),
119
+ });
120
+ }
121
+
122
+ function hookTemplate() {
123
+ return {
124
+ schema: HOOK_CONFIG_SCHEMA,
125
+ adapters: {},
126
+ hooks: {},
127
+ };
128
+ }
129
+
130
+ function parseLimit(argv, fallback = 30) {
131
+ const index = argv.indexOf('--limit');
132
+ if (index === -1) return fallback;
133
+ const value = Number(argv[index + 1]);
134
+ if (!Number.isInteger(value) || value < 1 || value > 500) throw new Error('hooks_limit_must_be_between_1_and_500');
135
+ return value;
136
+ }
137
+
138
+ function renderHookRows(rows, write) {
139
+ if (!rows.length) {
140
+ writeLine(write, ' No hooks configured. Run `agentsam hooks init` or add a stored hook.');
141
+ return;
142
+ }
143
+ for (const row of rows) {
144
+ const source = clean(row.metadata?.hook_source || row.source_kind) || 'code';
145
+ const state = row.enabled === false || row.is_active === false ? 'disabled' : 'active';
146
+ const handler = clean(row.handler_type || row.metadata?.adapter) || 'callback';
147
+ writeLine(write, ` ${String(row.hook || row.event_type).padEnd(25)} ${String(row.id || row.hook_key).padEnd(28)} ${source.padEnd(7)} ${handler.padEnd(10)} ${state} · p${row.priority ?? 100}`);
148
+ }
149
+ }
150
+
151
+ export async function runHooks(argv = [], options = {}) {
152
+ const write = options.write || ((text) => process.stdout.write(text));
153
+ const cwd = path.resolve(options.cwd || process.cwd());
154
+ const projectRoot = findCliProjectRoot(cwd);
155
+ const jsonOutput = argv.includes('--json');
156
+ const command = argv.find((arg) => !arg.startsWith('-')) || 'status';
157
+ if (argv.includes('--help') || argv.includes('-h') || command === 'help') {
158
+ write([
159
+ 'agentsam hooks [status|list|executions|validate|init|enable|disable] [--json]',
160
+ '',
161
+ 'One hook concept, three authoring sources: code, project config, and stored rows.',
162
+ ' status Show active sources and execution evidence',
163
+ ' list Show the merged dispatch order with source badges',
164
+ ' executions [--limit N] Show recent value-free receipts',
165
+ ' validate [config] Validate a portable hook config',
166
+ ' init Create .agentsam/hooks.json (idempotent)',
167
+ ' enable|disable <key> Toggle a stored hook without editing code/config',
168
+ '',
169
+ ].join('\n'));
170
+ return 0;
171
+ }
172
+
173
+ if (command === 'init') {
174
+ const filename = path.join(projectRoot, '.agentsam', 'hooks.json');
175
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
176
+ const created = !fs.existsSync(filename);
177
+ if (created) fs.writeFileSync(filename, `${JSON.stringify(hookTemplate(), null, 2)}\n`, { mode: 0o600 });
178
+ const loaded = loadHookConfig(filename);
179
+ const result = { ok: true, created, source: filename, schema: loaded.schema };
180
+ if (jsonOutput) writeLine(write, JSON.stringify(result, null, 2));
181
+ else writeLine(write, ` Hooks config ready · ${filename}`);
182
+ return result;
183
+ }
184
+
185
+ if (command === 'validate') {
186
+ const commandIndex = argv.indexOf(command);
187
+ const candidate = argv.slice(commandIndex + 1).find((arg) => !arg.startsWith('-'));
188
+ const filename = candidate || findHookConfig(cwd, { stopAt: projectRoot });
189
+ if (!filename) throw new Error('agentsam_hook_config_not_found');
190
+ const config = loadHookConfig(filename);
191
+ const result = { ok: true, schema: config.schema, source: config.source, adapters: Object.keys(config.adapters).length, hooks: countConfigured(config) };
192
+ if (jsonOutput) writeLine(write, JSON.stringify(result, null, 2));
193
+ else writeLine(write, ` Valid · ${result.hooks} hooks · ${result.adapters} adapters · ${result.source}`);
194
+ return result;
195
+ }
196
+
197
+ const ownerId = clean(options.ownerId || readAccountSession({ home: options.home })?.account_id) || 'local';
198
+ if (command === 'enable' || command === 'disable') {
199
+ const key = argv[argv.indexOf(command) + 1];
200
+ if (!key || key.startsWith('-')) throw new Error(`hooks_${command}_requires_key`);
201
+ const changed = await setRuntimeHookActive({ projectRoot, owner_id: ownerId, hook_key: key, active: command === 'enable' });
202
+ if (!changed) throw new Error(`stored_hook_not_found:${key}`);
203
+ const result = { ok: true, hook_key: key, active: command === 'enable' };
204
+ if (jsonOutput) writeLine(write, JSON.stringify(result, null, 2));
205
+ else writeLine(write, ` ${key} → ${result.active ? 'enabled' : 'disabled'}`);
206
+ return result;
207
+ }
208
+
209
+ if (command === 'executions') {
210
+ const executions = await listRuntimeHookExecutions({ projectRoot, owner_id: ownerId, limit: parseLimit(argv) });
211
+ if (jsonOutput) writeLine(write, JSON.stringify({ schema: 'agentsam.hook.executions.v1', executions }, null, 2));
212
+ else {
213
+ writeLine(write, '\n Recent hook executions');
214
+ if (!executions.length) writeLine(write, ' No hook receipts recorded yet.');
215
+ for (const row of executions) {
216
+ const at = new Date(Number(row.ran_at_unix) * 1000).toISOString();
217
+ writeLine(write, ` ${at} ${String(row.status).padEnd(9)} ${String(row.event_type).padEnd(24)} ${row.hook_key} · ${row.duration_ms}ms`);
218
+ }
219
+ writeLine(write);
220
+ }
221
+ return executions;
222
+ }
223
+
224
+ if (!['status', 'list'].includes(command)) throw new Error(`unknown_hooks_command:${command}`);
225
+ const resolved = await createProjectHookRuntime({
226
+ cwd,
227
+ projectRoot,
228
+ ownerId,
229
+ home: options.home,
230
+ hooks: options.hooks,
231
+ persistReceipts: false,
232
+ });
233
+ const storedAll = await listRuntimeHooks({ projectRoot, owner_id: ownerId, activeOnly: false });
234
+ const result = {
235
+ schema: 'agentsam.hooks.status.v1',
236
+ project_root: projectRoot,
237
+ owner_id: ownerId,
238
+ config: resolved.config ? { source: resolved.config_file, hooks: countConfigured(resolved.config), adapters: Object.keys(resolved.config.adapters).length } : null,
239
+ sources: {
240
+ code: resolved.definitions.filter((row) => row.metadata?.hook_source === 'code').length,
241
+ config: resolved.definitions.filter((row) => row.metadata?.hook_source === 'config').length,
242
+ stored_active: resolved.definitions.filter((row) => row.metadata?.hook_source === 'stored').length,
243
+ stored_total: storedAll.length,
244
+ },
245
+ hooks: resolved.definitions,
246
+ };
247
+ if (jsonOutput) writeLine(write, JSON.stringify(result, null, 2));
248
+ else {
249
+ writeLine(write, '\n Agent Sam · hooks');
250
+ writeLine(write, ' One ordered lifecycle contract · code is read-only · config/stored hooks are portable.');
251
+ writeLine(write, ` project ${projectRoot}`);
252
+ writeLine(write, ` config ${result.config ? `${result.config.hooks} hooks · ${result.config.source}` : 'not configured'}`);
253
+ writeLine(write, ` stored ${result.sources.stored_active} active · ${result.sources.stored_total} total`);
254
+ writeLine(write, '');
255
+ if (command === 'list') renderHookRows([...resolved.definitions, ...storedAll.filter((row) => !row.is_active)], write);
256
+ else writeLine(write, ' Use `agentsam hooks list` for dispatch order and `agentsam hooks executions` for receipts.');
257
+ writeLine(write);
258
+ }
259
+ return result;
260
+ }
@@ -17,7 +17,7 @@ export const API_PROVIDERS = Object.freeze([
17
17
  { id: 'openai', label: 'OpenAI', credential: 'OPENAI_API_KEY' },
18
18
  { id: 'anthropic', label: 'Anthropic', credential: 'ANTHROPIC_API_KEY' },
19
19
  { id: 'gemini', label: 'Gemini', credential: 'GEMINI_API_KEY' },
20
- { id: 'grok', label: 'Grok / xAI', credential: 'XAI_API_KEY' },
20
+ { id: 'xai', label: 'Grok / xAI', credential: 'XAI_API_KEY' },
21
21
  { id: 'cursor', label: 'Cursor', credential: 'CURSOR_API_KEY' },
22
22
  { id: 'cloudflare', label: 'Cloudflare', credential: 'CLOUDFLARE_API_TOKEN' },
23
23
  ]);
@@ -33,6 +33,7 @@ import { grantExecutionApproval, isExecutionApproved, toolApprovalKey } from '..
33
33
  import { runWhoami } from './whoami.js';
34
34
  import { runCredentials } from './credentials.js';
35
35
  import { runCheatSheet } from './cheat-sheet.js';
36
+ import { createProjectHookRuntime, runHooks } from './hooks.js';
36
37
  import pkg from '../../package.json' with { type: 'json' };
37
38
  import { runLogin, runLogout } from './account-auth.js';
38
39
  import { runHelp } from '../ui/cli/help.js';
@@ -262,21 +263,48 @@ function spawnGit(cwd, args) {
262
263
  if (result.status !== 0) throw new Error(`git exited ${result.status}`);
263
264
  }
264
265
 
265
- function selectedModel(cwd) {
266
- const preferences = readCliPreferences(cwd) || {};
267
- const snapshot = preferences.modelSnapshot?.model_key === preferences.modelPreference
268
- ? mergeModelReference(preferences.modelSnapshot)
266
+ function selectedModel(cwd, state = null) {
267
+ const saved = readCliPreferences(cwd) || {};
268
+ const sessionConfig = state?.hookEffectiveConfig || {};
269
+ const preferences = {
270
+ ...saved,
271
+ ...(sessionConfig.model_key ? { modelPreference: sessionConfig.model_key } : {}),
272
+ ...(sessionConfig.reasoning_effort ? { reasoningEffort: sessionConfig.reasoning_effort } : {}),
273
+ ...(sessionConfig.requested_service_tier ? { serviceTier: sessionConfig.requested_service_tier } : {}),
274
+ };
275
+ const snapshot = saved.modelSnapshot?.model_key === preferences.modelPreference
276
+ ? mergeModelReference(saved.modelSnapshot)
269
277
  : null;
270
278
  let model = snapshot || getModelRecord(preferences.modelPreference);
279
+ if (!model && sessionConfig.model_key && sessionConfig.model_key.includes(':')) {
280
+ const [provider, ...modelParts] = sessionConfig.model_key.split(':');
281
+ const providerModelId = modelParts.join(':');
282
+ if (provider && providerModelId) {
283
+ model = {
284
+ model_key: sessionConfig.model_key,
285
+ provider,
286
+ provider_model_id: providerModelId,
287
+ label: providerModelId,
288
+ context_window: null,
289
+ context_window_source: 'unknown',
290
+ max_output_tokens: null,
291
+ max_output_tokens_source: 'unknown',
292
+ reasoning_efforts: ['auto'],
293
+ default_reasoning_effort: null,
294
+ service_tiers: ['default'],
295
+ capabilities: {},
296
+ };
297
+ }
298
+ }
271
299
  if (!model && (preferences.modelPreference === 'auto' || !preferences.modelPreference)) {
272
- model = mergeModelReference(preferences.modelSnapshot) || listModelCatalog()[0];
300
+ model = mergeModelReference(saved.modelSnapshot) || listModelCatalog()[0];
273
301
  }
274
302
  if (!model) throw new Error('Select an exact provider-verified model with /model first so Agent Sam can verify supported runtime controls.');
275
303
  return { preferences, model };
276
304
  }
277
305
 
278
306
  async function resolveModelForTurn(cwd, state) {
279
- const selected = selectedModel(cwd);
307
+ const selected = selectedModel(cwd, state);
280
308
  const { preferences } = selected;
281
309
  let model = selected.model;
282
310
 
@@ -291,7 +319,7 @@ async function resolveModelForTurn(cwd, state) {
291
319
  capabilities: { ...(model.capabilities || {}), local_runtime: true, ...(Object.fromEntries((probe.capabilities || []).map((name) => [name, true]))) },
292
320
  };
293
321
  updateCliPreferences(cwd, { modelPreference: model.model_key, modelSnapshot: model });
294
- return { preferences: readCliPreferences(cwd) || preferences, model, credential: null, verification: 'local_runtime' };
322
+ return { preferences: { ...(readCliPreferences(cwd) || preferences), reasoningEffort: preferences.reasoningEffort, serviceTier: preferences.serviceTier }, model, credential: null, verification: 'local_runtime' };
295
323
  }
296
324
 
297
325
  const credential = resolveProviderCredential(model.provider, { home: state.home });
@@ -305,7 +333,7 @@ async function resolveModelForTurn(cwd, state) {
305
333
  if (!live) throw new Error(`selected_model_not_available_for_credential:${model.provider}:${model.provider_model_id}`);
306
334
  model = live;
307
335
  updateCliPreferences(cwd, { modelPreference: live.model_key, modelSnapshot: live });
308
- return { preferences: readCliPreferences(cwd) || preferences, model, credential, verification: 'provider_api' };
336
+ return { preferences: { ...(readCliPreferences(cwd) || preferences), reasoningEffort: preferences.reasoningEffort, serviceTier: preferences.serviceTier }, model, credential, verification: 'provider_api' };
309
337
  }
310
338
 
311
339
  if (preferences.modelSnapshot?.availability === 'available') {
@@ -595,7 +623,47 @@ function checkpointCompaction(state, model, result) {
595
623
  return receipt;
596
624
  }
597
625
 
626
+ async function ensureShellHookExperience(state) {
627
+ if (state.hookExperience) return state.hookExperience;
628
+ const experience = await createProjectHookRuntime({
629
+ cwd: state.cwd,
630
+ projectRoot: state.projectRoot,
631
+ home: state.home,
632
+ sessionId: state.session?.id,
633
+ conversationId: state.session?.conversation_id,
634
+ hooks: state.codeHooks,
635
+ });
636
+ state.hookExperience = experience;
637
+ if (!state.hookSessionStarted) {
638
+ const config = {
639
+ model_key: state.session?.model_key || null,
640
+ reasoning_effort: state.session?.reasoning_effort || null,
641
+ requested_service_tier: state.session?.requested_service_tier || 'default',
642
+ auto_compact: state.session?.auto_compact !== false,
643
+ };
644
+ const started = await experience.runtime.dispatch('session_start', {
645
+ config,
646
+ project_root: state.projectRoot,
647
+ cwd: state.cwd,
648
+ }, {
649
+ session_id: state.session?.id,
650
+ source: 'agentsam-cli',
651
+ }, { cwd: state.cwd });
652
+ const modified = started.input.config || config;
653
+ const allowed = {};
654
+ for (const key of ['model_key', 'reasoning_effort', 'requested_service_tier', 'auto_compact']) {
655
+ if (modified[key] !== undefined) allowed[key] = modified[key];
656
+ }
657
+ state.hookEffectiveConfig = { ...config, ...allowed };
658
+ if (state.session && Object.keys(allowed).length) persistSession(state, allowed);
659
+ state.hookSessionContext = started.output.additional_context || null;
660
+ state.hookSessionStarted = true;
661
+ }
662
+ return experience;
663
+ }
664
+
598
665
  async function runInteractiveModelTurn(prompt, state) {
666
+ const hookExperience = await ensureShellHookExperience(state);
599
667
  const resolved = await resolveModelForTurn(state.cwd, state);
600
668
  const { preferences, model, credential } = resolved;
601
669
  const provider = createProviderAdapter({
@@ -660,7 +728,13 @@ async function runInteractiveModelTurn(prompt, state) {
660
728
  autoCompact: state.session?.auto_compact !== false,
661
729
  onCompaction: result => { if (state.session) checkpointCompaction(state, model, result); },
662
730
  promptCacheKey: state.session?.id || undefined,
731
+ hookRuntime: hookExperience.runtime,
732
+ sessionId: state.session?.id || undefined,
663
733
  runId: runtimeRunId || state.session?.id || undefined,
734
+ source: 'agentsam-cli',
735
+ ...(state.hookSessionContext ? {
736
+ contextItems: [{ kind: 'hook', ref: 'hook://session-start/context', priority: 96, content: state.hookSessionContext }],
737
+ } : {}),
664
738
  beforeRequest: (preflight) => approveModelRequest(preflight, state),
665
739
  beforeTool: (request) => approveToolExecution(request, state),
666
740
  emit(event) {
@@ -793,7 +867,7 @@ export async function dispatchShellLine(line, state = {}) {
793
867
  'help', 'exit', 'quit', 'clear', 'status', 'models', 'model',
794
868
  'whoami', 'cf', 'cloudflare', 'db', 'tunnel', 'connections',
795
869
  'connect', 'usage', 'session', 'providers', 'credentials', 'cheatsheet',
796
- 'settings', 'logs',
870
+ 'settings', 'logs', 'hooks', 'hook',
797
871
  'git', 'diff', 'pwd', 'cd', 'fast', 'flex', 'standard', 'reasoning',
798
872
  ];
799
873
  if (commonVerbs.includes(bare)) {
@@ -862,6 +936,22 @@ export async function dispatchShellLine(line, state = {}) {
862
936
  state.rl?.resume?.();
863
937
  }
864
938
  break;
939
+ case '/hooks':
940
+ case '/hook': {
941
+ const hookCommand = args[0] || 'status';
942
+ await runHooks(args.length ? args : ['status'], { cwd: state.cwd, write, home: state.home, hooks: state.codeHooks });
943
+ if (['init', 'enable', 'disable'].includes(hookCommand)) {
944
+ if (state.hookExperience && state.hookSessionStarted) {
945
+ await state.hookExperience.runtime.dispatch('session_end', {
946
+ status: 'reloading_hooks',
947
+ elapsed_ms: state.session ? localSessionElapsedMs(state.session) : 0,
948
+ }, { session_id: state.session?.id, source: 'agentsam-cli' }, { cwd: state.cwd });
949
+ }
950
+ state.hookExperience = null;
951
+ state.hookSessionStarted = false;
952
+ }
953
+ break;
954
+ }
865
955
  case '/providers':
866
956
  state.rl?.pause?.();
867
957
  try {
@@ -1113,6 +1203,7 @@ export async function runShell(argv = [], options = {}) {
1113
1203
  session: options.session || null,
1114
1204
  home: options.home,
1115
1205
  providerFetchImpl: options.providerFetchImpl,
1206
+ codeHooks: options.hooks || null,
1116
1207
  persistFooter: options.persistFooter === true,
1117
1208
  };
1118
1209
  const sub = argv[0] || '';
@@ -1201,6 +1292,20 @@ export async function runShell(argv = [], options = {}) {
1201
1292
  restorePaste();
1202
1293
  state.rl = null;
1203
1294
  }
1295
+ if (state.hookExperience && state.hookSessionStarted) {
1296
+ try {
1297
+ await state.hookExperience.runtime.dispatch('session_end', {
1298
+ status: interrupted ? 'interrupted' : 'paused',
1299
+ elapsed_ms: state.session ? localSessionElapsedMs(state.session) : 0,
1300
+ cumulative_usage: state.session?.cumulative_usage || {},
1301
+ }, {
1302
+ session_id: state.session?.id,
1303
+ source: 'agentsam-cli',
1304
+ }, { cwd: state.cwd });
1305
+ } catch (error) {
1306
+ writeLine(write, ` Hook cleanup warning: ${error?.message || error}`);
1307
+ }
1308
+ }
1204
1309
  if (state.session) {
1205
1310
  const activeElapsedMs = localSessionElapsedMs(state.session);
1206
1311
  persistSession(state, {