@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.
- package/README.md +18 -0
- package/apps/cad-creator/shared/cad/package.json +1 -1
- package/apps/client-cms-editor/backend/package.json +3 -3
- package/apps/client-cms-editor/frontend/package.json +4 -4
- package/apps/client-cms-editor/package-lock.json +8 -8
- package/apps/client-cms-editor/package.json +1 -1
- package/apps/client-cms-editor/shared/cms/package.json +2 -2
- package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
- package/examples/hyperspace-demo/index.html +16 -0
- package/examples/hyperspace-demo/package.json +21 -0
- package/examples/hyperspace-demo/src/main.tsx +160 -0
- package/examples/hyperspace-demo/tsconfig.json +9 -0
- package/examples/hyperspace-demo/vite.config.ts +24 -0
- package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
- package/package.json +23 -7
- package/packages/agentsam-brand/package.json +1 -1
- package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
- package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
- package/packages/agentsam-contracts/package.json +1 -1
- package/packages/agentsam-contracts/src/errors.ts +19 -10
- package/packages/agentsam-errors/package.json +11 -3
- package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
- package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
- package/packages/agentsam-errors/src/envelope.js +63 -11
- package/packages/agentsam-errors/src/error.js +6 -0
- package/packages/agentsam-errors/src/fingerprint.js +9 -1
- package/packages/agentsam-errors/src/normalize.js +51 -6
- package/packages/agentsam-errors/src/recovery.js +9 -35
- package/packages/agentsam-errors/src/transport.js +6 -1
- package/packages/agentsam-errors/src/vocabulary.js +515 -126
- package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
- package/packages/agentsam-errors/types/index.d.ts +157 -0
- package/packages/agentsam-goap/package.json +2 -2
- package/packages/agentsam-hooks/README.md +206 -0
- package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
- package/packages/agentsam-hooks/bindings/go/command.go +55 -0
- package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
- package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
- package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
- package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
- package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
- package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
- package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
- package/packages/agentsam-hooks/package.json +69 -0
- package/packages/agentsam-hooks/schema/manifest.json +54 -0
- package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
- package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
- package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
- package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
- package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
- package/packages/agentsam-hooks/src/adapters/command.js +104 -0
- package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
- package/packages/agentsam-hooks/src/adapters/http.js +58 -0
- package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
- package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
- package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
- package/packages/agentsam-hooks/src/config.js +143 -0
- package/packages/agentsam-hooks/src/contracts.js +284 -0
- package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
- package/packages/agentsam-hooks/src/index.d.ts +193 -0
- package/packages/agentsam-hooks/src/index.js +14 -0
- package/packages/agentsam-hooks/src/policy.js +34 -0
- package/packages/agentsam-hooks/src/runtime.js +312 -0
- package/packages/agentsam-hooks/src/store.js +264 -0
- package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
- package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
- package/packages/agentsam-knowledge/package.json +1 -1
- package/packages/agentsam-nav/dist/Nav.d.ts +43 -0
- package/packages/agentsam-nav/dist/NavCollapsible.d.ts +10 -0
- package/packages/agentsam-nav/dist/NavContent.d.ts +2 -0
- package/packages/agentsam-nav/dist/NavFooter.d.ts +2 -0
- package/packages/agentsam-nav/dist/NavGroup.d.ts +3 -0
- package/packages/agentsam-nav/dist/NavHeader.d.ts +2 -0
- package/packages/agentsam-nav/dist/NavMenu.d.ts +22 -0
- package/packages/agentsam-nav/dist/NavProvider.d.ts +35 -0
- package/packages/agentsam-nav/dist/NavTrigger.d.ts +2 -0
- package/packages/agentsam-nav/dist/Surfaces.d.ts +41 -0
- package/packages/agentsam-nav/dist/browser.d.ts +11 -0
- package/packages/agentsam-nav/dist/browser.global.js +135 -0
- package/packages/agentsam-nav/dist/browser.js +7290 -0
- package/packages/agentsam-nav/dist/contracts.d.ts +76 -0
- package/packages/agentsam-nav/dist/index.d.ts +11 -0
- package/packages/agentsam-nav/dist/index.js +7291 -0
- package/packages/agentsam-nav/dist/legacy.css +83 -0
- package/packages/agentsam-nav/dist/shell.css +113 -0
- package/packages/agentsam-nav/dist/theme.css +113 -0
- package/packages/agentsam-nav/dist/utils.d.ts +1 -0
- package/packages/agentsam-nav/package.json +1 -1
- package/packages/agentsam-queue-control/package.json +1 -1
- package/packages/agentsam-repository/package.json +1 -1
- package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +670 -0
- package/packages/agentsam-vault/package.json +1 -1
- package/packages/agentsam-workbench/package.json +3 -3
- package/packages/connectors/cloudflare/package.json +1 -1
- package/packages/heuristic-theme/package.json +1 -1
- package/packages/identity/package.json +2 -2
- package/packages/providers/completeful/package.json +1 -1
- package/packages/theme-church-site/package.json +2 -2
- package/packages/theme-companions-site/package.json +2 -2
- package/packages/theme-floors-site/package.json +2 -2
- package/packages/theme-fuelnfree-site/package.json +2 -2
- package/packages/theme-handyman-site/package.json +2 -2
- package/packages/theme-insurance-site/package.json +2 -2
- package/packages/theme-scenes/package.json +1 -1
- package/packages/theme-shinshu-site/package.json +2 -2
- package/packages/work-graph/package.json +1 -1
- package/protocol/apps/agentsam.app.v1.schema.json +1 -1
- package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
- package/protocol/assets/asset-record.v1.schema.json +1 -1
- package/protocol/brand/brandpack.v1.schema.json +1 -1
- package/protocol/capabilities/capability-manifest.schema.json +1 -1
- package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
- package/protocol/credentials/issue.v1.schema.json +1 -1
- package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
- package/protocol/database/connection.v1.schema.json +1 -1
- package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
- package/protocol/errors/README.md +36 -0
- package/protocol/errors/agentsam.error.v2.schema.json +821 -0
- package/protocol/errors/error-catalog.json +3506 -158
- package/protocol/errors/error-envelope.schema.json +195 -52
- package/protocol/errors/recovery.v1.schema.json +1 -1
- package/protocol/features/agentsam.feature.v1.schema.json +1 -1
- package/protocol/hooks/README.md +18 -0
- package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
- package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
- package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
- package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
- package/protocol/local-engine/benchmark.v1.schema.json +1 -1
- package/protocol/local-engine/engine.v1.schema.json +1 -1
- package/protocol/models/model-inventory-v2.schema.json +1 -1
- package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
- package/protocol/rpc/v1/errors.proto +1 -0
- package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
- package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
- package/protocol/sam/activity.v1.schema.json +1 -1
- package/protocol/sam/answer.v1.schema.json +1 -1
- package/protocol/sam/calibration.v1.schema.json +1 -1
- package/protocol/sam/decision-receipt.v1.schema.json +1 -1
- package/protocol/sam/evaluation.v1.schema.json +1 -1
- package/protocol/sam/operation.schema.json +1 -1
- package/protocol/sam/outcome.v1.schema.json +1 -1
- package/protocol/sam/question.v1.schema.json +1 -1
- package/protocol/sam/result.schema.json +1 -1
- package/protocol/sam/state.v1.schema.json +1 -1
- package/protocol/scoring/scorecard.v1.schema.json +1 -1
- package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
- package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
- package/protocol/theme-refinery/product-metadata.schema.json +1 -1
- package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
- package/protocol/ui/icon.v1.schema.json +1 -1
- package/python/README.md +20 -0
- package/python/agentsam_sdk/errors/__init__.py +4 -0
- package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
- package/python/agentsam_sdk/hooks/__init__.py +32 -0
- package/python/agentsam_sdk/hooks/adapters.py +132 -0
- package/python/agentsam_sdk/hooks/contracts.py +121 -0
- package/python/agentsam_sdk/hooks/runtime.py +253 -0
- package/python/tests/test_hooks.py +115 -0
- package/src/agent/responses-runner.js +145 -1
- package/src/cli/command-catalog.js +1 -0
- package/src/cli.js +9 -0
- package/src/commands/cheat-sheet.js +1 -0
- package/src/commands/hooks.js +260 -0
- package/src/commands/models.js +1 -1
- package/src/commands/shell.js +114 -9
- package/src/errors/diagnostic.js +9 -9
- package/src/index.js +32 -0
- package/src/knowledge/service/grpc-codec.js +2 -0
- package/src/lib/slash-commands.js +1 -0
- package/src/local/runtime-store.js +46 -0
- package/src/models/catalog.js +0 -49
- package/src/models/inventory-core.js +69 -0
- package/src/plugins/index.js +8 -1
- package/src/plugins/registry.js +56 -0
- package/src/rpc/generated/errors_pb.js +49 -1
- 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
|
-
|
|
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
|
+
}
|
package/src/commands/models.js
CHANGED
|
@@ -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: '
|
|
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
|
]);
|
package/src/commands/shell.js
CHANGED
|
@@ -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
|
|
267
|
-
const
|
|
268
|
-
|
|
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(
|
|
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, {
|