@inneranimalmedia/agentsam-sdk 2.0.0 → 2.2.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 +28 -15
- package/docs/CAPABILITIES.md +93 -0
- package/docs/DEPLOY_RECEIPTS.md +83 -0
- package/docs/RECON.md +165 -0
- package/docs/RELEASES.md +4 -3
- package/docs/portable-knowledge.md +4 -4
- package/docs/sdk-2.0-release.md +19 -21
- package/package.json +8 -1
- package/packages/identity/package.json +1 -1
- package/packages/identity/src/frontend/auth-portal/README.md +1 -1
- package/packages/identity/src/index.js +2 -0
- package/protocol/capabilities/capability-manifest.schema.json +36 -0
- package/protocol/capabilities/manifest.json +179 -0
- package/protocol/capabilities/repository-audit-input.schema.json +14 -0
- package/protocol/capabilities/repository-audit.schema.json +30 -0
- package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
- package/protocol/capabilities/repository-snapshot.schema.json +20 -0
- package/protocol/knowledge/chunk.schema.json +15 -73
- package/protocol/knowledge/document.schema.json +10 -48
- package/protocol/knowledge/index-config.schema.json +2 -2
- package/protocol/knowledge/repository.schema.json +11 -52
- package/protocol/knowledge/retrieval-query.schema.json +13 -63
- package/protocol/knowledge/source.schema.json +9 -43
- package/protocol/presets/catalog.json +48 -0
- package/protocol/recon/README.md +19 -0
- package/protocol/recon/finding-report.schema.json +46 -0
- package/protocol/recon/task-packet.schema.json +79 -0
- package/python/agentsam_sdk/knowledge/models.py +19 -7
- package/python/agentsam_sdk/repository/__main__.py +2 -2
- package/python/agentsam_sdk/repository/recon/__init__.py +28 -0
- package/python/agentsam_sdk/repository/recon/__main__.py +3 -0
- package/python/agentsam_sdk/repository/recon/cli.py +178 -0
- package/python/agentsam_sdk/repository/recon/packet.py +294 -0
- package/python/agentsam_sdk/repository/recon/validate.py +76 -0
- package/python/tests/test_knowledge_models.py +6 -2
- package/python/tests/test_recon.py +257 -0
- package/src/agent/capability-adapter.js +50 -0
- package/src/agent/index.js +2 -0
- package/src/agent/repository-audit.js +188 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/manifest.js +22 -0
- package/src/capabilities/repository-snapshot.js +180 -0
- package/src/cli.js +68 -8
- package/src/commands/deploy-receipt.js +129 -0
- package/src/commands/deploy.js +0 -1
- package/src/commands/knowledge.js +5 -6
- package/src/commands/product.js +119 -0
- package/src/commands/recon.js +71 -0
- package/src/index.js +17 -0
- package/src/knowledge/config.js +12 -6
- package/src/knowledge/contracts.js +1 -1
- package/src/knowledge/engine.js +1 -1
- package/src/knowledge/service/server.js +2 -2
- package/src/lib/deploy-receipt/index.js +246 -0
- package/src/lib/git-context.js +3 -1
- package/src/presets/index.js +20 -0
- package/src/repository/index.js +4 -0
- package/test/agent-capabilities.test.mjs +67 -0
- package/test/capabilities.test.mjs +84 -0
- package/test/deploy-receipt.test.mjs +91 -0
- package/test/portable-context.test.mjs +11 -1
|
@@ -4,49 +4,15 @@
|
|
|
4
4
|
"type": "object",
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"title": "Source",
|
|
7
|
-
"required": [
|
|
8
|
-
"source_id",
|
|
9
|
-
"source_type",
|
|
10
|
-
"workspace_id",
|
|
11
|
-
"uri"
|
|
12
|
-
],
|
|
7
|
+
"required": ["source_id", "source_type", "uri"],
|
|
13
8
|
"properties": {
|
|
14
|
-
"source_id": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
"workspace_id": {
|
|
23
|
-
"type": "string",
|
|
24
|
-
"minLength": 1
|
|
25
|
-
},
|
|
26
|
-
"uri": {
|
|
27
|
-
"type": "string",
|
|
28
|
-
"minLength": 1
|
|
29
|
-
},
|
|
30
|
-
"repository_full_name": {
|
|
31
|
-
"type": [
|
|
32
|
-
"string",
|
|
33
|
-
"null"
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
"ref": {
|
|
37
|
-
"type": [
|
|
38
|
-
"string",
|
|
39
|
-
"null"
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
"commit_sha": {
|
|
43
|
-
"type": [
|
|
44
|
-
"string",
|
|
45
|
-
"null"
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
"metadata": {
|
|
49
|
-
"type": "object"
|
|
50
|
-
}
|
|
9
|
+
"source_id": { "type": "string", "minLength": 1 },
|
|
10
|
+
"source_type": { "type": "string", "minLength": 1 },
|
|
11
|
+
"workspace_id": { "type": "string", "minLength": 1, "deprecated": true, "description": "Legacy compatibility metadata only." },
|
|
12
|
+
"uri": { "type": "string", "minLength": 1 },
|
|
13
|
+
"repository_full_name": { "type": ["string", "null"] },
|
|
14
|
+
"ref": { "type": ["string", "null"] },
|
|
15
|
+
"commit_sha": { "type": ["string", "null"] },
|
|
16
|
+
"metadata": { "type": "object" }
|
|
51
17
|
}
|
|
52
18
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"presets": {
|
|
4
|
+
"fullstack": {
|
|
5
|
+
"id": "fullstack",
|
|
6
|
+
"version": 1,
|
|
7
|
+
"lane": "fullstack",
|
|
8
|
+
"description": "Local-first application with AgentSam runtime, repository primitives, knowledge, and delivery hooks.",
|
|
9
|
+
"features": ["agent", "knowledge"],
|
|
10
|
+
"capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "security.scan", "deploy.receipt"],
|
|
11
|
+
"defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
|
|
12
|
+
},
|
|
13
|
+
"cms": {
|
|
14
|
+
"id": "cms",
|
|
15
|
+
"version": 1,
|
|
16
|
+
"lane": "cms",
|
|
17
|
+
"description": "Content-oriented application starting from local CMS primitives with explicit auth/deploy opt-ins.",
|
|
18
|
+
"features": ["cms", "knowledge"],
|
|
19
|
+
"capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "site.scrape", "deploy.receipt"],
|
|
20
|
+
"defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
|
|
21
|
+
},
|
|
22
|
+
"prototype": {
|
|
23
|
+
"id": "prototype",
|
|
24
|
+
"version": 1,
|
|
25
|
+
"lane": "fullstack",
|
|
26
|
+
"description": "Small local prototype with the minimum deterministic AgentSam runtime.",
|
|
27
|
+
"features": ["agent"],
|
|
28
|
+
"capabilities": ["scaffold.create", "repository.snapshot", "security.scan"],
|
|
29
|
+
"defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
|
|
30
|
+
},
|
|
31
|
+
"data": {
|
|
32
|
+
"id": "data",
|
|
33
|
+
"version": 1,
|
|
34
|
+
"lane": "data",
|
|
35
|
+
"description": "Data-oriented local application with repository knowledge and explicit production storage graduation.",
|
|
36
|
+
"features": ["knowledge"],
|
|
37
|
+
"capabilities": ["scaffold.create", "repository.snapshot", "knowledge.index", "knowledge.search", "security.scan"],
|
|
38
|
+
"defaults": { "database": "sqlite", "assets": "local", "deploy": "local" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"addons": {
|
|
42
|
+
"agent": { "id": "agent", "capabilities": ["scaffold.create"], "description": "AgentSam local runtime selection." },
|
|
43
|
+
"auth": { "id": "auth", "capabilities": ["identity.init"], "description": "Identity package selection; use `agentsam identity init` when a standalone auth app scaffold is required." },
|
|
44
|
+
"cms": { "id": "cms", "capabilities": ["site.scrape"], "description": "CMS/content feature selection." },
|
|
45
|
+
"knowledge": { "id": "knowledge", "capabilities": ["knowledge.index", "knowledge.search"], "description": "Portable repository indexing and retrieval." },
|
|
46
|
+
"deploy-cloudflare": { "id": "deploy-cloudflare", "capabilities": ["deploy.receipt"], "description": "Select Cloudflare as the intended deploy target; provisioning remains explicit at deploy time." }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# AgentSam Recon Protocol (aka "MiniCodeScout")
|
|
2
|
+
|
|
3
|
+
Provider-neutral contracts for delegating bounded read-only investigation to a cheap or
|
|
4
|
+
local model (Ollama/Qwen-class, mini-tier hosted models, etc.) without giving it branch,
|
|
5
|
+
Git, or write authority. See [docs/RECON.md](../../docs/RECON.md) for the design rationale
|
|
6
|
+
and current status — the delegation step itself is currently benched in favor of plain
|
|
7
|
+
deterministic search; these contracts are the reusable substrate for when it isn't.
|
|
8
|
+
|
|
9
|
+
`task-packet.schema.json` is the only input a recon worker receives. It is built by a
|
|
10
|
+
deterministic controller (`repository.intelligence`, `rg`, AST lookups) — never by the
|
|
11
|
+
worker itself, and never contains a workspace/tenant ID as the task handle.
|
|
12
|
+
|
|
13
|
+
`finding-report.schema.json` is the only output a recon worker may return. A worker that
|
|
14
|
+
cannot answer from its supplied slices must return `status: "needs_context"`, not a guess.
|
|
15
|
+
|
|
16
|
+
These schemas intentionally do not define a code-mutation contract. Recon workers observe;
|
|
17
|
+
they do not write, `git commit`, migrate, or merge. A future `bounded-mutation` protocol
|
|
18
|
+
(scoped to specific, proven-safe transforms) is a separate contract, not an extension of
|
|
19
|
+
this one — see ownership rule 6 in `protocol/README.md`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.inneranimalmedia.com/agentsam/recon/finding-report.schema.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"title": "ReconFindingReport",
|
|
7
|
+
"description": "The only artifact a bounded recon worker may produce. It never edits files, never touches Git, and never owns a branch.",
|
|
8
|
+
"required": ["schema_version", "task_id", "status"],
|
|
9
|
+
"oneOf": [
|
|
10
|
+
{ "required": ["status"], "properties": { "status": { "const": "needs_context" } } },
|
|
11
|
+
{ "required": ["status"], "properties": { "status": { "const": "answered" } } }
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"schema_version": { "const": 1 },
|
|
15
|
+
"task_id": { "type": "string", "minLength": 1 },
|
|
16
|
+
"status": { "enum": ["answered", "needs_context"] },
|
|
17
|
+
"summary": { "type": "string" },
|
|
18
|
+
"findings": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"required": ["severity", "file", "finding"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"severity": { "enum": ["low", "medium", "high"] },
|
|
26
|
+
"file": { "type": "string" },
|
|
27
|
+
"lines": { "type": "string" },
|
|
28
|
+
"finding": { "type": "string" },
|
|
29
|
+
"evidence": { "type": "string" },
|
|
30
|
+
"recommended_action": { "type": "string" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"affected_files": { "type": "array", "items": { "type": "string" } },
|
|
35
|
+
"unknowns": { "type": "array", "items": { "type": "string" } },
|
|
36
|
+
"missing": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string" },
|
|
39
|
+
"description": "Required when status is needs_context: what evidence was missing."
|
|
40
|
+
},
|
|
41
|
+
"reason": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Required when status is needs_context: why the supplied slices were insufficient."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.inneranimalmedia.com/agentsam/recon/task-packet.schema.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"title": "ReconTaskPacket",
|
|
7
|
+
"description": "A bounded assignment handed to a cheap/local recon worker. Produced by a deterministic controller (rg/AST/repository.intelligence), never by the worker itself.",
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"task_id",
|
|
11
|
+
"repo_root",
|
|
12
|
+
"base_sha",
|
|
13
|
+
"question",
|
|
14
|
+
"slices",
|
|
15
|
+
"ceilings"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"schema_version": { "const": 1 },
|
|
19
|
+
"task_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"description": "Caller-supplied identifier. Never a workspace/tenant ID — this is a task handle only."
|
|
23
|
+
},
|
|
24
|
+
"repo_root": { "type": "string", "minLength": 1 },
|
|
25
|
+
"base_sha": { "type": "string", "minLength": 1 },
|
|
26
|
+
"question": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1,
|
|
29
|
+
"description": "Exactly one bounded question. Not 'audit the subsystem'."
|
|
30
|
+
},
|
|
31
|
+
"slices": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"minItems": 1,
|
|
34
|
+
"maxItems": 5,
|
|
35
|
+
"description": "Exact file/line slices the worker may read. The worker must not open anything outside this list.",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["path"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"path": { "type": "string", "minLength": 1 },
|
|
42
|
+
"start_line": { "type": "integer", "minimum": 1 },
|
|
43
|
+
"end_line": { "type": "integer", "minimum": 1 },
|
|
44
|
+
"content": { "type": "string" },
|
|
45
|
+
"reason": { "type": "string" },
|
|
46
|
+
"kind": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Optional structural label from a disambiguating tool (e.g. ast-grep): member, object_key, sql_string, call, comment, unknown. Absent when the slice came from lexical search alone."
|
|
49
|
+
},
|
|
50
|
+
"hit_count": {
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"minimum": 1,
|
|
53
|
+
"description": "Optional: how many raw search hits this slice's line range covers, when built via from_matches()."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"allowed_diagnostics": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"description": "Pre-approved read-only commands the worker may run verbatim. Empty/omitted means none.",
|
|
61
|
+
"items": { "type": "string" },
|
|
62
|
+
"default": []
|
|
63
|
+
},
|
|
64
|
+
"ceilings": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["max_follow_up_reads", "max_output_tokens", "timeout_seconds"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"max_follow_up_reads": { "type": "integer", "minimum": 0, "maximum": 2 },
|
|
70
|
+
"max_output_tokens": { "type": "integer", "minimum": 1 },
|
|
71
|
+
"timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 600 }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"response_schema_ref": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"const": "./finding-report.schema.json"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,40 +1,46 @@
|
|
|
1
|
-
"""Provider-neutral contracts for indexing and retrieval.
|
|
1
|
+
"""Provider-neutral contracts for indexing and retrieval.
|
|
2
|
+
|
|
3
|
+
Portable knowledge is repository-scoped. ``workspace_id`` remains optional compatibility
|
|
4
|
+
metadata only; it is never required to identify repository knowledge.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Any, Mapping
|
|
10
|
+
from typing import Any, Mapping
|
|
7
11
|
|
|
8
12
|
JsonMap = Mapping[str, Any]
|
|
9
13
|
|
|
14
|
+
|
|
10
15
|
@dataclass(frozen=True, slots=True)
|
|
11
16
|
class Source:
|
|
12
17
|
source_id: str
|
|
13
18
|
source_type: str
|
|
14
|
-
workspace_id: str
|
|
15
19
|
uri: str
|
|
16
20
|
repository_full_name: str | None = None
|
|
17
21
|
ref: str | None = None
|
|
18
22
|
commit_sha: str | None = None
|
|
23
|
+
workspace_id: str | None = None
|
|
19
24
|
metadata: JsonMap = field(default_factory=dict)
|
|
20
25
|
|
|
26
|
+
|
|
21
27
|
@dataclass(frozen=True, slots=True)
|
|
22
28
|
class Document:
|
|
23
29
|
document_id: str
|
|
24
30
|
source_id: str
|
|
25
|
-
workspace_id: str
|
|
26
31
|
content_hash: str
|
|
27
32
|
media_type: str
|
|
28
33
|
title: str | None = None
|
|
29
34
|
path: str | None = None
|
|
30
35
|
text: str | None = None
|
|
36
|
+
workspace_id: str | None = None
|
|
31
37
|
metadata: JsonMap = field(default_factory=dict)
|
|
32
38
|
|
|
39
|
+
|
|
33
40
|
@dataclass(frozen=True, slots=True)
|
|
34
41
|
class Chunk:
|
|
35
42
|
chunk_id: str
|
|
36
43
|
document_id: str
|
|
37
|
-
workspace_id: str
|
|
38
44
|
content: str
|
|
39
45
|
ordinal: int
|
|
40
46
|
content_hash: str
|
|
@@ -45,25 +51,27 @@ class Chunk:
|
|
|
45
51
|
path: str | None = None
|
|
46
52
|
symbol: str | None = None
|
|
47
53
|
heading_path: tuple[str, ...] = ()
|
|
54
|
+
workspace_id: str | None = None
|
|
48
55
|
metadata: JsonMap = field(default_factory=dict)
|
|
49
56
|
|
|
57
|
+
|
|
50
58
|
@dataclass(frozen=True, slots=True)
|
|
51
59
|
class RepositoryDescriptor:
|
|
52
60
|
repository_id: str
|
|
53
61
|
full_name: str
|
|
54
|
-
workspace_id: str
|
|
55
62
|
default_branch: str
|
|
56
63
|
product_ids: tuple[str, ...] = ()
|
|
57
64
|
indexed_refs: tuple[str, ...] = ()
|
|
58
65
|
enabled_lanes: tuple[str, ...] = ("code", "docs")
|
|
59
66
|
include: tuple[str, ...] = ()
|
|
60
67
|
exclude: tuple[str, ...] = ()
|
|
68
|
+
workspace_id: str | None = None
|
|
61
69
|
metadata: JsonMap = field(default_factory=dict)
|
|
62
70
|
|
|
71
|
+
|
|
63
72
|
@dataclass(frozen=True, slots=True)
|
|
64
73
|
class RetrievalQuery:
|
|
65
74
|
text: str
|
|
66
|
-
workspace_id: str
|
|
67
75
|
repositories: tuple[str, ...] = ()
|
|
68
76
|
product_ids: tuple[str, ...] = ()
|
|
69
77
|
refs: tuple[str, ...] = ()
|
|
@@ -74,6 +82,8 @@ class RetrievalQuery:
|
|
|
74
82
|
require_current_ref: bool = True
|
|
75
83
|
include_archived: bool = False
|
|
76
84
|
explain: bool = True
|
|
85
|
+
workspace_id: str | None = None
|
|
86
|
+
|
|
77
87
|
|
|
78
88
|
@dataclass(frozen=True, slots=True)
|
|
79
89
|
class RetrievalHit:
|
|
@@ -92,6 +102,7 @@ class RetrievalHit:
|
|
|
92
102
|
stale: bool = False
|
|
93
103
|
metadata: JsonMap = field(default_factory=dict)
|
|
94
104
|
|
|
105
|
+
|
|
95
106
|
@dataclass(frozen=True, slots=True)
|
|
96
107
|
class ContextPack:
|
|
97
108
|
query_id: str
|
|
@@ -101,6 +112,7 @@ class ContextPack:
|
|
|
101
112
|
confidence: str = "unknown"
|
|
102
113
|
diagnostics: JsonMap = field(default_factory=dict)
|
|
103
114
|
|
|
115
|
+
|
|
104
116
|
@dataclass(frozen=True, slots=True)
|
|
105
117
|
class IngestReceipt:
|
|
106
118
|
run_id: str
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from agentsam_sdk.repository.
|
|
1
|
+
from agentsam_sdk.repository.intelligence.cli import main_cli
|
|
2
2
|
|
|
3
|
-
raise SystemExit(
|
|
3
|
+
raise SystemExit(main_cli())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Bounded recon-worker harness: task-packet construction and finding-report validation.
|
|
2
|
+
|
|
3
|
+
This module does not call any model. It is the deterministic controller half of the
|
|
4
|
+
recon protocol described in docs/RECON.md — building small, exact, already-scoped work
|
|
5
|
+
packets, and validating whatever a worker (local or hosted) sends back against hard
|
|
6
|
+
ceilings before it reaches a capable coding agent.
|
|
7
|
+
"""
|
|
8
|
+
from .packet import (
|
|
9
|
+
build_task_packet,
|
|
10
|
+
from_matches,
|
|
11
|
+
from_ast_grep,
|
|
12
|
+
from_ripgrep,
|
|
13
|
+
PacketError,
|
|
14
|
+
TOOL_NAME_PACK,
|
|
15
|
+
)
|
|
16
|
+
from .validate import validate_report, ReportError, TOOL_NAME_VALIDATE
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"build_task_packet",
|
|
20
|
+
"from_matches",
|
|
21
|
+
"from_ast_grep",
|
|
22
|
+
"from_ripgrep",
|
|
23
|
+
"PacketError",
|
|
24
|
+
"TOOL_NAME_PACK",
|
|
25
|
+
"validate_report",
|
|
26
|
+
"ReportError",
|
|
27
|
+
"TOOL_NAME_VALIDATE",
|
|
28
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""CLI and ToolInput adapter for the recon bounded-worker harness.
|
|
2
|
+
|
|
3
|
+
Two read-only operations, matching the controller/validator halves of the pipeline
|
|
4
|
+
described in docs/RECON.md:
|
|
5
|
+
|
|
6
|
+
python -m agentsam_sdk.repository.recon pack --repo-root . \\
|
|
7
|
+
--question "..." --slice backend/workflows/repository/workflows.js:1-180 \\
|
|
8
|
+
--out packet.json
|
|
9
|
+
|
|
10
|
+
python -m agentsam_sdk.repository.recon validate --packet packet.json \\
|
|
11
|
+
--report report.json
|
|
12
|
+
|
|
13
|
+
Neither operation calls a model. `pack` is the deterministic controller step;
|
|
14
|
+
`validate` is the deterministic gate a worker's report must pass before a capable
|
|
15
|
+
agent ever sees it.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from agentsam_sdk.runtime.contract import ToolInput, ToolResult, start_timer, write_receipt
|
|
24
|
+
|
|
25
|
+
from .packet import TOOL_NAME_PACK, build_task_packet
|
|
26
|
+
from .validate import TOOL_NAME_VALIDATE, ReportError, validate_report
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_slice_arg(raw: str) -> dict[str, str | int]:
|
|
30
|
+
# path[:start-end]
|
|
31
|
+
if ":" in raw:
|
|
32
|
+
path, rng = raw.rsplit(":", 1)
|
|
33
|
+
if "-" in rng:
|
|
34
|
+
start, end = rng.split("-", 1)
|
|
35
|
+
return {"path": path, "start_line": int(start), "end_line": int(end)}
|
|
36
|
+
return {"path": raw}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_pack(tool_input: ToolInput) -> ToolResult:
|
|
40
|
+
started = start_timer()
|
|
41
|
+
try:
|
|
42
|
+
tool_input.assert_read_only()
|
|
43
|
+
params = tool_input.params
|
|
44
|
+
packet = build_task_packet(
|
|
45
|
+
params.get("repo_root") or ".",
|
|
46
|
+
question=params["question"],
|
|
47
|
+
slices=params.get("slices") or [],
|
|
48
|
+
task_id=params.get("task_id"),
|
|
49
|
+
allowed_diagnostics=params.get("allowed_diagnostics") or (),
|
|
50
|
+
max_follow_up_reads=int(params.get("max_follow_up_reads", 2)),
|
|
51
|
+
max_output_tokens=int(params.get("max_output_tokens", 800)),
|
|
52
|
+
timeout_seconds=int(params.get("timeout_seconds", 90)),
|
|
53
|
+
)
|
|
54
|
+
artifacts: list[str] = []
|
|
55
|
+
output_dir = tool_input.output_path()
|
|
56
|
+
if output_dir:
|
|
57
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
out = output_dir / f"recon-packet-{packet['task_id']}.json"
|
|
59
|
+
out.write_text(json.dumps(packet, indent=2), encoding="utf-8")
|
|
60
|
+
artifacts.append(str(out))
|
|
61
|
+
result = ToolResult(
|
|
62
|
+
ok=True,
|
|
63
|
+
tool=TOOL_NAME_PACK,
|
|
64
|
+
mode=tool_input.mode or "read-only",
|
|
65
|
+
request_id=tool_input.request_id,
|
|
66
|
+
started_at=started,
|
|
67
|
+
finished_at=start_timer(),
|
|
68
|
+
summary=f"Built bounded task packet {packet['task_id']} ({len(packet['slices'])} slice(s)).",
|
|
69
|
+
data=packet,
|
|
70
|
+
artifacts=artifacts,
|
|
71
|
+
)
|
|
72
|
+
except Exception as exc: # noqa: BLE001 - normalized into ToolResult
|
|
73
|
+
result = ToolResult(
|
|
74
|
+
ok=False,
|
|
75
|
+
tool=TOOL_NAME_PACK,
|
|
76
|
+
mode=tool_input.mode or "read-only",
|
|
77
|
+
request_id=tool_input.request_id,
|
|
78
|
+
started_at=started,
|
|
79
|
+
finished_at=start_timer(),
|
|
80
|
+
summary="recon pack failed",
|
|
81
|
+
error=str(exc)[:500],
|
|
82
|
+
)
|
|
83
|
+
write_receipt(result, tool_input.output_path())
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_validate(tool_input: ToolInput) -> ToolResult:
|
|
88
|
+
started = start_timer()
|
|
89
|
+
try:
|
|
90
|
+
tool_input.assert_read_only()
|
|
91
|
+
params = tool_input.params
|
|
92
|
+
packet = params["packet"]
|
|
93
|
+
report = params["report"]
|
|
94
|
+
try:
|
|
95
|
+
validated = validate_report(report, packet)
|
|
96
|
+
ok = True
|
|
97
|
+
summary = f"Report for {report.get('task_id')} is valid ({report.get('status')})."
|
|
98
|
+
error = None
|
|
99
|
+
except ReportError as exc:
|
|
100
|
+
validated = {}
|
|
101
|
+
ok = False
|
|
102
|
+
summary = "Report rejected; do not forward to the capable agent."
|
|
103
|
+
error = str(exc)
|
|
104
|
+
result = ToolResult(
|
|
105
|
+
ok=ok,
|
|
106
|
+
tool=TOOL_NAME_VALIDATE,
|
|
107
|
+
mode=tool_input.mode or "read-only",
|
|
108
|
+
request_id=tool_input.request_id,
|
|
109
|
+
started_at=started,
|
|
110
|
+
finished_at=start_timer(),
|
|
111
|
+
summary=summary,
|
|
112
|
+
data={"validated_report": validated} if ok else {},
|
|
113
|
+
error=error,
|
|
114
|
+
)
|
|
115
|
+
except Exception as exc: # noqa: BLE001 - normalized into ToolResult
|
|
116
|
+
result = ToolResult(
|
|
117
|
+
ok=False,
|
|
118
|
+
tool=TOOL_NAME_VALIDATE,
|
|
119
|
+
mode=tool_input.mode or "read-only",
|
|
120
|
+
request_id=tool_input.request_id,
|
|
121
|
+
started_at=started,
|
|
122
|
+
finished_at=start_timer(),
|
|
123
|
+
summary="recon validate failed",
|
|
124
|
+
error=str(exc)[:500],
|
|
125
|
+
)
|
|
126
|
+
write_receipt(result, tool_input.output_path())
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main_cli(argv: list[str] | None = None) -> int:
|
|
131
|
+
parser = argparse.ArgumentParser(
|
|
132
|
+
prog="python -m agentsam_sdk.repository.recon",
|
|
133
|
+
description="Build bounded recon task packets and validate worker findings.",
|
|
134
|
+
)
|
|
135
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
136
|
+
|
|
137
|
+
pack_p = sub.add_parser("pack", help="Build a bounded ReconTaskPacket.")
|
|
138
|
+
pack_p.add_argument("--repo-root", default=".")
|
|
139
|
+
pack_p.add_argument("--question", required=True)
|
|
140
|
+
pack_p.add_argument(
|
|
141
|
+
"--slice", dest="slices", action="append", default=[],
|
|
142
|
+
help="path[:start-end], repeatable, up to 5.",
|
|
143
|
+
)
|
|
144
|
+
pack_p.add_argument("--task-id")
|
|
145
|
+
pack_p.add_argument("--out")
|
|
146
|
+
|
|
147
|
+
validate_p = sub.add_parser("validate", help="Validate a ReconFindingReport against its packet.")
|
|
148
|
+
validate_p.add_argument("--packet", required=True, help="Path to a packet JSON file.")
|
|
149
|
+
validate_p.add_argument("--report", required=True, help="Path to a report JSON file.")
|
|
150
|
+
|
|
151
|
+
args = parser.parse_args(argv)
|
|
152
|
+
|
|
153
|
+
if args.command == "pack":
|
|
154
|
+
packet = build_task_packet(
|
|
155
|
+
args.repo_root,
|
|
156
|
+
question=args.question,
|
|
157
|
+
slices=[_parse_slice_arg(s) for s in args.slices],
|
|
158
|
+
task_id=args.task_id,
|
|
159
|
+
)
|
|
160
|
+
rendered = json.dumps(packet, indent=2)
|
|
161
|
+
if args.out:
|
|
162
|
+
Path(args.out).expanduser().write_text(rendered, encoding="utf-8")
|
|
163
|
+
else:
|
|
164
|
+
print(rendered)
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
if args.command == "validate":
|
|
168
|
+
packet = json.loads(Path(args.packet).expanduser().read_text(encoding="utf-8"))
|
|
169
|
+
report = json.loads(Path(args.report).expanduser().read_text(encoding="utf-8"))
|
|
170
|
+
try:
|
|
171
|
+
validate_report(report, packet)
|
|
172
|
+
except ReportError as exc:
|
|
173
|
+
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
|
174
|
+
return 1
|
|
175
|
+
print(json.dumps({"ok": True}, indent=2))
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
return 2
|