@inflexa-ai/harness 0.5.0 → 0.6.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/dist/agents/conversation-agent.d.ts +8 -0
- package/dist/agents/conversation-agent.js +4 -1
- package/dist/state/init.js +31 -5
- package/dist/tools/approval/contract.d.ts +8 -0
- package/dist/tools/approval/gateway.js +5 -3
- package/dist/tools/approval/queries.d.ts +4 -2
- package/dist/tools/approval/queries.js +29 -14
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
|
34
34
|
import type { ChromeConfig } from "../lib/chrome.js";
|
|
35
35
|
import type { AgentDefinition } from "../loop/types.js";
|
|
36
36
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
37
|
+
import type { Tool } from "../tools/define-tool.js";
|
|
37
38
|
import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
|
|
38
39
|
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
39
40
|
import { type BioToolKeys } from "../tools/bio/keys.js";
|
|
@@ -104,6 +105,13 @@ export interface ConversationAgentDeps {
|
|
|
104
105
|
* planning guidance and scheduling keep their legacy behavior.
|
|
105
106
|
*/
|
|
106
107
|
readonly resourcePolicy?: ResourcePolicy;
|
|
108
|
+
/**
|
|
109
|
+
* Embedder-contributed conversation-tool seam — additional tools the host
|
|
110
|
+
* supplies, appended after the built-in roster. The harness stays agnostic to
|
|
111
|
+
* what they do; each receives the same `ToolContext` (including `ask`) as any
|
|
112
|
+
* built-in tool.
|
|
113
|
+
*/
|
|
114
|
+
readonly hostTools?: readonly Tool[];
|
|
107
115
|
}
|
|
108
116
|
/** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
|
|
109
117
|
export declare function createConversationAgent(deps: ConversationAgentDeps): AgentDefinition;
|
|
@@ -48,7 +48,7 @@ export const CONVERSATION_AGENT_ID = "conversation-agent";
|
|
|
48
48
|
const CONVERSATION_MAX_ITERATIONS = 50;
|
|
49
49
|
/** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
|
|
50
50
|
export function createConversationAgent(deps) {
|
|
51
|
-
const { provider, pool, embedding, workspaceFs, model, executeAnalysisWorkflow, ephemeralWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, } = deps;
|
|
51
|
+
const { provider, pool, embedding, workspaceFs, model, executeAnalysisWorkflow, ephemeralWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, } = deps;
|
|
52
52
|
const workingMemory = createWorkingMemory(pool);
|
|
53
53
|
const ncbi = createNcbiTools(bioKeys);
|
|
54
54
|
const chemDb = createChemDbTools(bioKeys);
|
|
@@ -126,6 +126,9 @@ export function createConversationAgent(deps) {
|
|
|
126
126
|
createGrepTool(workspaceFs),
|
|
127
127
|
// Working memory.
|
|
128
128
|
createUpdateWorkingMemoryTool(workingMemory, pool),
|
|
129
|
+
// Host-contributed tools ride the same context and dispatch path as the
|
|
130
|
+
// built-ins; appended last so the built-in roster order stays fixed.
|
|
131
|
+
...(hostTools ?? []),
|
|
129
132
|
];
|
|
130
133
|
return {
|
|
131
134
|
id: CONVERSATION_AGENT_ID,
|
package/dist/state/init.js
CHANGED
|
@@ -260,6 +260,11 @@ CREATE TABLE IF NOT EXISTS cortex_asks (
|
|
|
260
260
|
title TEXT NOT NULL,
|
|
261
261
|
command TEXT NOT NULL,
|
|
262
262
|
detail TEXT,
|
|
263
|
+
-- The key the standing grant behind an 'always' is recorded under: the displayed
|
|
264
|
+
-- command unless the tool supplied a broader grant key. Written on every insert,
|
|
265
|
+
-- defaulting to the command when absent. Nullable only so the additive migration
|
|
266
|
+
-- can add it to an already-created table without a per-row backfill.
|
|
267
|
+
grant_key TEXT,
|
|
263
268
|
status TEXT NOT NULL,
|
|
264
269
|
reply JSONB,
|
|
265
270
|
created_at TEXT NOT NULL,
|
|
@@ -275,14 +280,15 @@ CREATE INDEX IF NOT EXISTS idx_cortex_asks_pending
|
|
|
275
280
|
ON cortex_asks(status) WHERE status = 'pending';
|
|
276
281
|
|
|
277
282
|
-- Standing approval grants behind an 'always' reply — keyed by the analysis and
|
|
278
|
-
-- the
|
|
279
|
-
-- auto-approves without pausing. Lives for the
|
|
280
|
-
-- process restarts, and never applies to another
|
|
283
|
+
-- the ask's grant key (the displayed command when the tool supplied no broader
|
|
284
|
+
-- key), so a later matching ask auto-approves without pausing. Lives for the
|
|
285
|
+
-- analysis lifecycle, surviving process restarts, and never applies to another
|
|
286
|
+
-- analysis.
|
|
281
287
|
CREATE TABLE IF NOT EXISTS cortex_ask_grants (
|
|
282
288
|
analysis_id TEXT NOT NULL,
|
|
283
|
-
|
|
289
|
+
grant_key TEXT NOT NULL,
|
|
284
290
|
created_at TEXT NOT NULL,
|
|
285
|
-
PRIMARY KEY (analysis_id,
|
|
291
|
+
PRIMARY KEY (analysis_id, grant_key)
|
|
286
292
|
);
|
|
287
293
|
`;
|
|
288
294
|
/**
|
|
@@ -406,6 +412,26 @@ export async function initCortexState(pool, injected) {
|
|
|
406
412
|
// dropping an absent NOT NULL no-ops, so this is a no-op on fresh DBs.
|
|
407
413
|
"ALTER TABLE cortex_analysis_state ALTER COLUMN data_profile_status DROP NOT NULL",
|
|
408
414
|
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSONB",
|
|
415
|
+
// Grant key an 'always' records the standing grant under. Nullable
|
|
416
|
+
// and unbackfilled: every new row writes it (command when absent), and
|
|
417
|
+
// orphaned legacy pending rows are swept to 'expired' before any answer
|
|
418
|
+
// reads it, so no existing row is left needing a value.
|
|
419
|
+
"ALTER TABLE cortex_asks ADD COLUMN IF NOT EXISTS grant_key TEXT",
|
|
420
|
+
// The grants table's key column is named for what it stores. A table
|
|
421
|
+
// created when the column was `command` is renamed in place — its
|
|
422
|
+
// values carry over unchanged, because a grant recorded without a
|
|
423
|
+
// grant key is keyed on its exact command. Guarded via
|
|
424
|
+
// information_schema (Postgres has no RENAME COLUMN IF EXISTS) and
|
|
425
|
+
// scoped to current_schema() so parallel test schemas in the same
|
|
426
|
+
// database never trigger each other's rename.
|
|
427
|
+
`DO $$ BEGIN
|
|
428
|
+
IF EXISTS (
|
|
429
|
+
SELECT 1 FROM information_schema.columns
|
|
430
|
+
WHERE table_schema = current_schema() AND table_name = 'cortex_ask_grants' AND column_name = 'command'
|
|
431
|
+
) THEN
|
|
432
|
+
ALTER TABLE cortex_ask_grants RENAME COLUMN command TO grant_key;
|
|
433
|
+
END IF;
|
|
434
|
+
END $$`,
|
|
409
435
|
];
|
|
410
436
|
for (const sql of addMigrations) {
|
|
411
437
|
await client.query(sql);
|
|
@@ -21,6 +21,14 @@ export interface AskRequest {
|
|
|
21
21
|
readonly title: string;
|
|
22
22
|
readonly command: string;
|
|
23
23
|
readonly detail?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Keys the standing grant an `always` records when the class being blessed is
|
|
26
|
+
* broader than the displayed `command` — a tool that grants a whole family of
|
|
27
|
+
* operations passes the family's key here while still showing one concrete
|
|
28
|
+
* command. It is a generic, opaque key string, not a tool- or domain-specific
|
|
29
|
+
* field, and the surface never renders it. Absent, the grant keys on `command`.
|
|
30
|
+
*/
|
|
31
|
+
readonly grantKey?: string;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* A user's decision on an approval request — three distinct outcomes, never a
|
|
@@ -27,11 +27,13 @@ export function createAskGateway(deps) {
|
|
|
27
27
|
title: request.title,
|
|
28
28
|
command: request.command,
|
|
29
29
|
detail: request.detail ?? null,
|
|
30
|
+
grantKey: request.grantKey ?? null,
|
|
30
31
|
createdAt: now,
|
|
31
32
|
};
|
|
32
|
-
// A standing grant for this
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// A standing grant for this ask's grant key — the command unless the tool
|
|
34
|
+
// supplied a broader key — short-circuits the prompt: record the invocation as
|
|
35
|
+
// `resolved` for the audit trail and return without pausing.
|
|
36
|
+
if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, request.grantKey ?? request.command))) {
|
|
35
37
|
unwrapOrThrow(await insertGrantedAsk(pool, row, now));
|
|
36
38
|
return { kind: "always" };
|
|
37
39
|
}
|
|
@@ -40,6 +40,8 @@ export interface AskRow {
|
|
|
40
40
|
readonly title: string;
|
|
41
41
|
readonly command: string;
|
|
42
42
|
readonly detail: string | null;
|
|
43
|
+
/** Grant key an `always` keys its standing grant under; `null` falls back to `command`. */
|
|
44
|
+
readonly grantKey: string | null;
|
|
43
45
|
readonly createdAt: string;
|
|
44
46
|
}
|
|
45
47
|
/** Insert a fresh `pending` ask before its decision is awaited. */
|
|
@@ -50,8 +52,8 @@ export declare function insertPendingAsk(querier: Querier, row: AskRow): ResultA
|
|
|
50
52
|
* every approval-gated action. The recorded reply is `{ kind: "always" }`.
|
|
51
53
|
*/
|
|
52
54
|
export declare function insertGrantedAsk(querier: Querier, row: AskRow, resolvedAt: string): ResultAsync<void, DbError>;
|
|
53
|
-
/** Does a standing grant cover `(analysisId,
|
|
54
|
-
export declare function selectGrant(querier: Querier, analysisId: string,
|
|
55
|
+
/** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
|
|
56
|
+
export declare function selectGrant(querier: Querier, analysisId: string, grantKey: string): ResultAsync<boolean, DbError>;
|
|
55
57
|
/** Read a row's current status + recorded reply. `null` when no such row exists. */
|
|
56
58
|
export declare function selectAskResolution(querier: Querier, id: string): ResultAsync<AskResolution | null, DbError>;
|
|
57
59
|
/**
|
|
@@ -15,9 +15,9 @@ export function insertPendingAsk(querier, row) {
|
|
|
15
15
|
return tryMutation("asks.insertPending", async () => {
|
|
16
16
|
await querier.query({
|
|
17
17
|
text: `INSERT INTO cortex_asks
|
|
18
|
-
(id, analysis_id, thread_id, title, command, detail, status, created_at)
|
|
19
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $
|
|
20
|
-
values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.createdAt],
|
|
18
|
+
(id, analysis_id, thread_id, title, command, detail, grant_key, status, created_at)
|
|
19
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)`,
|
|
20
|
+
values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.grantKey ?? row.command, row.createdAt],
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -30,18 +30,29 @@ export function insertGrantedAsk(querier, row, resolvedAt) {
|
|
|
30
30
|
return tryMutation("asks.insertGranted", async () => {
|
|
31
31
|
await querier.query({
|
|
32
32
|
text: `INSERT INTO cortex_asks
|
|
33
|
-
(id, analysis_id, thread_id, title, command, detail, status, reply, created_at, resolved_at)
|
|
34
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'resolved', $
|
|
35
|
-
values: [
|
|
33
|
+
(id, analysis_id, thread_id, title, command, detail, grant_key, status, reply, created_at, resolved_at)
|
|
34
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'resolved', $8::jsonb, $9, $10)`,
|
|
35
|
+
values: [
|
|
36
|
+
row.id,
|
|
37
|
+
row.analysisId,
|
|
38
|
+
row.threadId,
|
|
39
|
+
row.title,
|
|
40
|
+
row.command,
|
|
41
|
+
row.detail,
|
|
42
|
+
row.grantKey ?? row.command,
|
|
43
|
+
JSON.stringify({ kind: "always" }),
|
|
44
|
+
row.createdAt,
|
|
45
|
+
resolvedAt,
|
|
46
|
+
],
|
|
36
47
|
});
|
|
37
48
|
});
|
|
38
49
|
}
|
|
39
|
-
/** Does a standing grant cover `(analysisId,
|
|
40
|
-
export function selectGrant(querier, analysisId,
|
|
50
|
+
/** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
|
|
51
|
+
export function selectGrant(querier, analysisId, grantKey) {
|
|
41
52
|
return tryQuery("asks.selectGrant", async () => {
|
|
42
53
|
const result = await querier.query({
|
|
43
|
-
text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND
|
|
44
|
-
values: [analysisId,
|
|
54
|
+
text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND grant_key = $2`,
|
|
55
|
+
values: [analysisId, grantKey],
|
|
45
56
|
});
|
|
46
57
|
return (result.rowCount ?? 0) > 0;
|
|
47
58
|
});
|
|
@@ -114,19 +125,23 @@ export function answerAsk(pool, id, reply, now) {
|
|
|
114
125
|
return withTransaction(pool, "asks.answer", (client) => tryMutation("asks.answer:update", () => client.query({
|
|
115
126
|
text: `UPDATE cortex_asks SET status = $1, reply = $2::jsonb, resolved_at = $3
|
|
116
127
|
WHERE id = $4 AND status = 'pending'
|
|
117
|
-
RETURNING analysis_id, command`,
|
|
128
|
+
RETURNING analysis_id, COALESCE(grant_key, command) AS grant_key`,
|
|
118
129
|
values: [status, JSON.stringify(reply), now, id],
|
|
119
130
|
})).andThen((updated) => {
|
|
120
131
|
if ((updated.rowCount ?? 0) === 0)
|
|
121
132
|
return discriminateMiss(client, id);
|
|
122
133
|
if (reply.kind !== "always")
|
|
123
134
|
return okAsync("applied");
|
|
135
|
+
// The cast's non-null `grant_key` is sound without any boot-order
|
|
136
|
+
// caveat: every insert writes grant_key (command when the tool supplied
|
|
137
|
+
// none), and the COALESCE above covers any pending row that predates
|
|
138
|
+
// the column — `command` is NOT NULL, so the key can never be null.
|
|
124
139
|
const granted = updated.rows[0];
|
|
125
140
|
return tryMutation("asks.answer:grant", async () => {
|
|
126
141
|
await client.query({
|
|
127
|
-
text: `INSERT INTO cortex_ask_grants (analysis_id,
|
|
128
|
-
VALUES ($1, $2, $3) ON CONFLICT (analysis_id,
|
|
129
|
-
values: [granted.analysis_id, granted.
|
|
142
|
+
text: `INSERT INTO cortex_ask_grants (analysis_id, grant_key, created_at)
|
|
143
|
+
VALUES ($1, $2, $3) ON CONFLICT (analysis_id, grant_key) DO NOTHING`,
|
|
144
|
+
values: [granted.analysis_id, granted.grant_key, now],
|
|
130
145
|
});
|
|
131
146
|
}).map(() => "applied");
|
|
132
147
|
}));
|
package/package.json
CHANGED