@keemakr/agent-sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,11 +19,11 @@ Peer dependencies (match your eve agent): `eve@0.13.0`, `jose@^6.2.3`.
19
19
 
20
20
  Set these in your deployed agent's environment:
21
21
 
22
- | Variable | Purpose |
23
- |---|---|
24
- | `KEE_CORE_JWKS_URL` | keemakr-core's JWKS endpoint, e.g. `https://app.keemakr.com/.well-known/jwks.json`. Enables grant verification. |
22
+ | Variable | Purpose |
23
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `KEE_CORE_JWKS_URL` | keemakr-core's JWKS endpoint, e.g. `https://app.keemakr.com/.well-known/jwks.json`. Enables grant verification. |
25
25
  | `KEE_AGENT_AUDIENCE` | This deployment's audience — your runtime URL's origin, e.g. `https://my-agent.example.com`. Must match the audience the operator mints. |
26
- | `KEE_CORE_URL` | keemakr-core's base URL for capability calls, e.g. `https://app.keemakr.com`. (Derived from `KEE_CORE_JWKS_URL` if unset.) |
26
+ | `KEE_CORE_URL` | keemakr-core's base URL for capability calls, e.g. `https://app.keemakr.com`. (Derived from `KEE_CORE_JWKS_URL` if unset.) |
27
27
 
28
28
  If `KEE_CORE_JWKS_URL` is unset, `grantAuth()` skips entirely — useful during local development.
29
29
 
@@ -32,9 +32,9 @@ If `KEE_CORE_JWKS_URL` is unset, `grantAuth()` skips entirely — useful during
32
32
  `grantAuth()` returns an eve `AuthFn`. Put it ahead of any fallback:
33
33
 
34
34
  ```ts title="agent/channels/eve.ts"
35
- import { localDev, vercelOidc } from "eve/channels/auth";
36
- import { eveChannel } from "eve/channels/eve";
37
- import { grantAuth } from "@keemakr/agent-sdk";
35
+ import { localDev, vercelOidc } from 'eve/channels/auth';
36
+ import { eveChannel } from 'eve/channels/eve';
37
+ import { grantAuth } from '@keemakr/agent-sdk';
38
38
 
39
39
  export default eveChannel({
40
40
  auth: [localDev(), vercelOidc(), grantAuth()],
@@ -46,9 +46,9 @@ On success the verified tenant id and scopes are attached to the session auth co
46
46
  ## 2. Reach tenant data from a tool
47
47
 
48
48
  ```ts title="agent/tools/find_email.ts"
49
- import { defineTool } from "eve/tools";
50
- import { z } from "zod";
51
- import { useKee } from "@keemakr/agent-sdk";
49
+ import { defineTool } from 'eve/tools';
50
+ import { z } from 'zod';
51
+ import { useKee } from '@keemakr/agent-sdk';
52
52
 
53
53
  export default defineTool({
54
54
  description: "Find a lead's work email.",
@@ -60,7 +60,7 @@ export default defineTool({
60
60
  async execute(args, ctx) {
61
61
  const kee = useKee(ctx);
62
62
  // Proxy path: the credential stays in keemakr-core; you get the result.
63
- const result = await kee.connections.hunter.call("email-finder", args);
63
+ const result = await kee.connections.hunter.call('email-finder', args);
64
64
  return result; // { email, score, status }
65
65
  },
66
66
  });
@@ -87,12 +87,12 @@ const { access_token } = await kee.connections.hunter.token();
87
87
  `@keemakr/agent-sdk/connectors` ships a generated, typed manifest of every connector keemakr-core exposes — provider slugs, `maturity`, and each operation's name + JSON-Schema arg contract. Use it to discover what's callable (and get autocomplete on provider + op names) **without** scanning a core checkout or hitting a running instance. It's **metadata only** — no credentials.
88
88
 
89
89
  ```ts
90
- import { connectors, opNames, isReady } from "@keemakr/agent-sdk/connectors";
90
+ import { connectors, opNames, isReady } from '@keemakr/agent-sdk/connectors';
91
91
 
92
- opNames("hunter"); // → ["email-finder"]
93
- connectors.hunter.ops["email-finder"].inputSchema; // JSON Schema for the args
94
- isReady("meta"); // false while a connector is coming_soon
95
- connectors.meta.maturity; // "coming_soon" | "ready"
92
+ opNames('hunter'); // → ["email-finder"]
93
+ connectors.hunter.ops['email-finder'].inputSchema; // JSON Schema for the args
94
+ isReady('meta'); // false while a connector is coming_soon
95
+ connectors.meta.maturity; // "coming_soon" | "ready"
96
96
  ```
97
97
 
98
98
  A `coming_soon` connector is declarable in your `entry.json` `dependencies` today; its operations start callable (and `isReady` flips to `true`) once core ships them — **no change to your agent**.
@@ -107,20 +107,54 @@ npm run gen:connectors # or: npm run build (runs gen first)
107
107
  ### Memory (cross-session, tenant-shared)
108
108
 
109
109
  ```ts
110
- await kee.memory.set("prefs", "tone", { tone: "formal" });
111
- await kee.memory.get("prefs", "tone"); // → { tone: "formal" }
112
- await kee.memory.list("prefs"); // → entries in the namespace
113
- await kee.memory.delete("prefs", "tone");
110
+ await kee.memory.set('prefs', 'tone', { tone: 'formal' });
111
+ await kee.memory.get('prefs', 'tone'); // → { tone: "formal" }
112
+ await kee.memory.list('prefs'); // → entries in the namespace
113
+ await kee.memory.delete('prefs', 'tone');
114
114
  // Semantic search by meaning (embeddings):
115
- const hits = await kee.memory.search("how should I speak to the user?", { limit: 5 });
115
+ const hits = await kee.memory.search('how should I speak to the user?', { limit: 5 });
116
116
  // → [{ namespace, key, value, score, … }] (score 0–1, nearest first)
117
117
  ```
118
118
 
119
+ Memory is tenant-shared: any of the tenant's installed agents can read/write any
120
+ namespace. Concurrent writers should take turns — every entry carries a
121
+ monotonic `version`, and conditional writes lose gracefully instead of
122
+ clobbering:
123
+
124
+ ```ts
125
+ const entry = await kee.memory.getEntry('crm', 'lead:acme'); // { value, version, … }
126
+ try {
127
+ await kee.memory.set('crm', 'lead:acme', next, { ifVersion: entry!.version });
128
+ } catch (e) {
129
+ if (e instanceof MemoryConflictError) {
130
+ // someone wrote first — e.current is the winning entry; re-read, re-derive, retry
131
+ }
132
+ }
133
+ // Or merge one field with no read at all (object values only):
134
+ await kee.memory.patch('crm', 'lead:acme', { status: 'contacted' });
135
+ ```
136
+
137
+ Use memory for your agent's own continuity (preferences, cursors, entity
138
+ state) — durable documents belong in the tenant knowledge base, which you read
139
+ via `kee.kb`.
140
+
141
+ ### Knowledge base (read-only retrieval)
142
+
143
+ ```ts
144
+ const hits = await kee.kb.search('what is our refund policy?', { k: 5 });
145
+ // → [{ text, score, provenance: { title, source_uri, … } }]
146
+ ```
147
+
148
+ Scoped server-side to the collections bound to your agent + the tenant's
149
+ default corpus + the shared platform KB (`kb:retrieve` scope, granted to every
150
+ install). Hybrid retrieval, reranked in core; `text` may be a wider parent
151
+ context for clause-level documents.
152
+
119
153
  ### Platform tools
120
154
 
121
155
  ```ts
122
- await kee.tools.list(); // tools this grant is entitled to
123
- await kee.tools.run("current-time"); // run one in keemakr-core
156
+ await kee.tools.list(); // tools this grant is entitled to
157
+ await kee.tools.run('current-time'); // run one in keemakr-core
124
158
  ```
125
159
 
126
160
  A call whose grant lacks the required scope returns a `KeeError` with `status: 403`; an expired/invalid grant returns `status: 401`.
@@ -130,10 +164,10 @@ A call whose grant lacks the required scope returns a `KeeError` with `status: 4
130
164
  A cron/scheduled turn has no operator session, so it gets no session grant. keemakr-core can mint a **machine grant** for it (gated on the tenant's per-install `unattended_consent`). If your remote runs **outside** an eve channel, verify that grant directly:
131
165
 
132
166
  ```ts
133
- import { verifyGrant } from "@keemakr/agent-sdk";
167
+ import { verifyGrant } from '@keemakr/agent-sdk';
134
168
 
135
169
  const claims = await verifyGrant(grantToken, { audience: process.env.KEE_AGENT_AUDIENCE });
136
- if (!claims) throw new Error("invalid or expired grant");
170
+ if (!claims) throw new Error('invalid or expired grant');
137
171
  // claims.tenantId, claims.scopes, claims.aud, claims.exp
138
172
  ```
139
173
 
@@ -143,7 +177,7 @@ Inside an eve channel, `grantAuth()` already accepts machine grants (same token
143
177
 
144
178
  - **Tenant/service credentials live in keemakr-core**, reached only via the proxy — the credential never crosses the wire. Tenant is always the verified grant, resolved server-side; never pass a tenant id from tool input.
145
179
  - **The token path is opt-in and scope-gated** (`conn:<provider>:token`), declared per dependency in `entry.json` (`"access": "token"`).
146
- - **You MAY hold your own model key.** There is no platform model gateway today, so an agent routing its own LLM calls (its own Anthropic/AI-Gateway key) is expected and fine — that is *not* a credential leak. A leak is a *tenant/service* credential read in agent code.
180
+ - **You MAY hold your own model key.** There is no platform model gateway today, so an agent routing its own LLM calls (its own Anthropic/AI-Gateway key) is expected and fine — that is _not_ a credential leak. A leak is a _tenant/service_ credential read in agent code.
147
181
  - Every capability call re-verifies the grant and enforces scope on the server.
148
182
 
149
183
  Full contract: keemakr-core `docs/CONNECTOR-CONTRACT.md`.
package/dist/client.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  export interface KeeError extends Error {
2
2
  status?: number;
3
+ /** Parsed error-response body, when core sent one (e.g. the conflicting entry on 409). */
4
+ body?: unknown;
5
+ }
6
+ /**
7
+ * A conditional memory write (`ifVersion`) lost the race — another agent wrote
8
+ * the key first. `current` is the entry as it now stands (null when the key was
9
+ * deleted concurrently). Re-read, re-derive, retry.
10
+ */
11
+ export declare class MemoryConflictError extends Error {
12
+ status: 409;
13
+ current: MemoryEntry | null;
14
+ constructor(current: MemoryEntry | null);
3
15
  }
4
16
  export interface KeeContext {
5
17
  session?: {
@@ -26,6 +38,8 @@ export interface MemoryEntry {
26
38
  key: string;
27
39
  value: unknown;
28
40
  written_by_agent: string | null;
41
+ /** Monotonic write counter — pass as `ifVersion` for compare-and-swap writes. */
42
+ version: number;
29
43
  created_at: string;
30
44
  updated_at: string;
31
45
  }
@@ -43,10 +57,25 @@ export interface MemorySearchHit extends MemoryEntry {
43
57
  export interface KeeMemory {
44
58
  /** Read a key's value, or null if absent. */
45
59
  get(namespace: string, key: string): Promise<unknown | null>;
46
- /** Read the full entry (value + provenance + timestamps), or null. */
60
+ /** Read the full entry (value + provenance + version + timestamps), or null. */
47
61
  getEntry(namespace: string, key: string): Promise<MemoryEntry | null>;
48
- /** Write a key. Returns the stored entry. */
49
- set(namespace: string, key: string, value: unknown): Promise<MemoryEntry>;
62
+ /**
63
+ * Write a key. Returns the stored entry. Pass `ifVersion` (from a prior
64
+ * getEntry) to make it a compare-and-swap: throws MemoryConflictError when
65
+ * another agent wrote the key in between.
66
+ */
67
+ set(namespace: string, key: string, value: unknown, opts?: {
68
+ ifVersion?: number;
69
+ }): Promise<MemoryEntry>;
70
+ /**
71
+ * Shallow-merge `delta` into an existing object value without reading it
72
+ * first — safe under concurrency for the "add one field" case. Throws
73
+ * MemoryConflictError on an `ifVersion` mismatch; a KeeError with status 404
74
+ * when the key is absent, 422 when the stored value isn't an object.
75
+ */
76
+ patch(namespace: string, key: string, delta: Record<string, unknown>, opts?: {
77
+ ifVersion?: number;
78
+ }): Promise<MemoryEntry>;
50
79
  /** Delete a key. Returns whether it existed. */
51
80
  delete(namespace: string, key: string): Promise<boolean>;
52
81
  /** List every entry in a namespace (tenant-wide). */
@@ -57,6 +86,26 @@ export interface KeeMemory {
57
86
  limit?: number;
58
87
  }): Promise<MemorySearchHit[]>;
59
88
  }
89
+ /** One KB retrieval hit. `text` is the chunk (or its parent context for
90
+ * parent-child-chunked documents); `score` is the reranker's relevance when
91
+ * reranking ran, else the RRF fusion score. */
92
+ export interface KBHit {
93
+ text: string;
94
+ score: number;
95
+ provenance: Record<string, unknown>;
96
+ }
97
+ /**
98
+ * Tenant knowledge-base retrieval. The agent sees the collections bound to it
99
+ * plus the tenant's default corpus plus the shared platform KB — scoping is
100
+ * enforced server-side from the grant. Requires the `kb:retrieve` scope
101
+ * (granted to every install).
102
+ */
103
+ export interface KeeKb {
104
+ /** Semantic + lexical + reranked search over the agent-visible knowledge. */
105
+ search(query: string, opts?: {
106
+ k?: number;
107
+ }): Promise<KBHit[]>;
108
+ }
60
109
  /** Platform registry tools (Shape B) — defined in core, run server-side. */
61
110
  export interface KeeTools {
62
111
  /** List the registry tools this grant is entitled to. */
@@ -76,6 +125,7 @@ export interface Kee {
76
125
  get(provider: string): KeeConnection;
77
126
  };
78
127
  memory: KeeMemory;
128
+ kb: KeeKb;
79
129
  tools: KeeTools;
80
130
  }
81
131
  /**
package/dist/client.js CHANGED
@@ -4,12 +4,27 @@
4
4
  // /api/capability/* endpoints, forwarding the grant. Core re-verifies the grant
5
5
  // and enforces scope on every call; the SDK never sees a raw credential on the
6
6
  // proxy path.
7
- function keeError(message, status) {
7
+ function keeError(message, status, body) {
8
8
  const e = new Error(message);
9
9
  e.name = 'KeeError';
10
10
  e.status = status;
11
+ e.body = body;
11
12
  return e;
12
13
  }
14
+ /**
15
+ * A conditional memory write (`ifVersion`) lost the race — another agent wrote
16
+ * the key first. `current` is the entry as it now stands (null when the key was
17
+ * deleted concurrently). Re-read, re-derive, retry.
18
+ */
19
+ export class MemoryConflictError extends Error {
20
+ status = 409;
21
+ current;
22
+ constructor(current) {
23
+ super('memory version conflict — the key changed since it was read');
24
+ this.name = 'MemoryConflictError';
25
+ this.current = current;
26
+ }
27
+ }
13
28
  function readGrant(ctx) {
14
29
  const attrs = ctx?.session?.auth?.current?.attributes ?? {};
15
30
  const token = typeof attrs.grant_token === 'string' ? attrs.grant_token : undefined;
@@ -46,7 +61,7 @@ async function capabilityFetch(grant, path, body, method = 'POST') {
46
61
  });
47
62
  const json = (await res.json().catch(() => ({})));
48
63
  if (!res.ok) {
49
- throw keeError(json.error ?? `capability request failed (${res.status})`, res.status);
64
+ throw keeError(json.error ?? `capability request failed (${res.status})`, res.status, json);
50
65
  }
51
66
  return json;
52
67
  }
@@ -92,9 +107,31 @@ export function useKee(ctx) {
92
107
  const entry = await this.getEntry(namespace, key);
93
108
  return entry ? entry.value : null;
94
109
  },
95
- async set(namespace, key, value) {
96
- const json = (await capabilityFetch(grant, `memory/${enc(namespace)}/${enc(key)}`, { value }, 'PUT'));
97
- return json.entry;
110
+ async set(namespace, key, value, opts) {
111
+ try {
112
+ const json = (await capabilityFetch(grant, `memory/${enc(namespace)}/${enc(key)}`, { value, expected_version: opts?.ifVersion }, 'PUT'));
113
+ return json.entry;
114
+ }
115
+ catch (e) {
116
+ if (e.status === 409) {
117
+ const body = e.body;
118
+ throw new MemoryConflictError(body?.entry ?? null);
119
+ }
120
+ throw e;
121
+ }
122
+ },
123
+ async patch(namespace, key, delta, opts) {
124
+ try {
125
+ const json = (await capabilityFetch(grant, `memory/${enc(namespace)}/${enc(key)}`, { delta, expected_version: opts?.ifVersion }, 'PATCH'));
126
+ return json.entry;
127
+ }
128
+ catch (e) {
129
+ if (e.status === 409) {
130
+ const body = e.body;
131
+ throw new MemoryConflictError(body?.entry ?? null);
132
+ }
133
+ throw e;
134
+ }
98
135
  },
99
136
  async delete(namespace, key) {
100
137
  const json = (await capabilityFetch(grant, `memory/${enc(namespace)}/${enc(key)}`, undefined, 'DELETE'));
@@ -113,6 +150,15 @@ export function useKee(ctx) {
113
150
  return json.hits ?? [];
114
151
  },
115
152
  };
153
+ const kb = {
154
+ async search(query, opts) {
155
+ const json = (await capabilityFetch(grant, 'kb/retrieve', {
156
+ query,
157
+ k: opts?.k,
158
+ }));
159
+ return json.hits ?? [];
160
+ },
161
+ };
116
162
  const tools = {
117
163
  async list() {
118
164
  const json = (await capabilityFetch(grant, 'tools', undefined, 'GET'));
@@ -125,5 +171,5 @@ export function useKee(ctx) {
125
171
  return json.result;
126
172
  },
127
173
  };
128
- return { tenantId: grant.tenantId, scopes: grant.scopes, connections, memory, tools };
174
+ return { tenantId: grant.tenantId, scopes: grant.scopes, connections, memory, kb, tools };
129
175
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { grantAuth } from './grant-auth.js';
2
2
  export { verifyGrant, type VerifiedGrant } from './verify-grant.js';
3
- export { useKee, type Kee, type KeeConnection, type KeeContext, type KeeError, type KeeMemory, type KeeTools, type MemoryEntry, type MemorySearchHit, } from './client.js';
3
+ export { useKee, MemoryConflictError, type Kee, type KeeConnection, type KeeContext, type KeeError, type KeeMemory, type KeeKb, type KBHit, type KeeTools, type MemoryEntry, type MemorySearchHit, } from './client.js';
4
4
  export { keemakrToolDirectory } from './tool-directory.js';
package/dist/index.js CHANGED
@@ -10,5 +10,5 @@
10
10
  // const r = await kee.connections.hunter.call('email-finder', { domain, first_name, last_name });
11
11
  export { grantAuth } from './grant-auth.js';
12
12
  export { verifyGrant } from './verify-grant.js';
13
- export { useKee, } from './client.js';
13
+ export { useKee, MemoryConflictError, } from './client.js';
14
14
  export { keemakrToolDirectory } from './tool-directory.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keemakr/agent-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The floor for keemakr marketplace agents: verify the capability grant and reach tenant connections, memory, and shared platform tools through keemakr-core — without holding raw secrets or resolving the tenant yourself.",
5
5
  "license": "MIT",
6
6
  "type": "module",