@openhands/extensions 0.4.2 → 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/.github/workflows/pypi-publish.yml +104 -0
- package/.github/workflows/tests.yml +6 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +1 -1
- package/README.md +31 -3
- package/automations/catalog/slack-channel-monitor.json +2 -2
- package/integrations/README.md +17 -7
- package/integrations/catalog/airtable.json +40 -10
- package/integrations/catalog/apify.json +0 -2
- package/integrations/catalog/asana.json +38 -0
- package/integrations/catalog/atlassian.json +5 -8
- package/integrations/catalog/bitbucket.json +39 -0
- package/integrations/catalog/box.json +39 -0
- package/integrations/catalog/brave-search.json +1 -2
- package/integrations/catalog/browser-mcp.json +0 -3
- package/integrations/catalog/canva.json +35 -0
- package/integrations/catalog/clickhouse.json +1 -2
- package/integrations/catalog/clickup.json +36 -0
- package/integrations/catalog/cloudflare-bindings.json +5 -8
- package/integrations/catalog/cloudflare-browser-rendering.json +5 -8
- package/integrations/catalog/cloudflare-builds.json +6 -9
- package/integrations/catalog/cloudflare-docs.json +4 -7
- package/integrations/catalog/cloudflare-observability.json +5 -8
- package/integrations/catalog/confluence.json +38 -0
- package/integrations/catalog/datadog.json +34 -0
- package/integrations/catalog/deepwiki.json +3 -7
- package/integrations/catalog/discord.json +39 -0
- package/integrations/catalog/dropbox.json +38 -0
- package/integrations/catalog/elevenlabs.json +27 -28
- package/integrations/catalog/everything.json +0 -2
- package/integrations/catalog/exa.json +0 -2
- package/integrations/catalog/fetch.json +0 -2
- package/integrations/catalog/figma.json +41 -9
- package/integrations/catalog/filesystem.json +0 -3
- package/integrations/catalog/firecrawl.json +0 -2
- package/integrations/catalog/freshdesk.json +38 -0
- package/integrations/catalog/git.json +0 -3
- package/integrations/catalog/github.json +51 -34
- package/integrations/catalog/gitlab.json +38 -0
- package/integrations/catalog/gmail.json +38 -0
- package/integrations/catalog/google-calendar.json +38 -0
- package/integrations/catalog/google-docs.json +38 -0
- package/integrations/catalog/google-drive.json +38 -0
- package/integrations/catalog/google-sheets.json +38 -0
- package/integrations/catalog/hubspot.json +34 -0
- package/integrations/catalog/huggingface.json +1 -2
- package/integrations/catalog/intercom.json +39 -0
- package/integrations/catalog/jira.json +38 -0
- package/integrations/catalog/kagi.json +2 -3
- package/integrations/catalog/linear.json +47 -16
- package/integrations/catalog/mailchimp.json +38 -0
- package/integrations/catalog/memory.json +0 -2
- package/integrations/catalog/microsoft-outlook.json +38 -0
- package/integrations/catalog/microsoft-teams.json +38 -0
- package/integrations/catalog/miro.json +38 -0
- package/integrations/catalog/monday.json +38 -0
- package/integrations/catalog/mongodb.json +1 -2
- package/integrations/catalog/neon.json +0 -2
- package/integrations/catalog/netlify.json +38 -0
- package/integrations/catalog/notion.json +37 -10
- package/integrations/catalog/obsidian.json +1 -3
- package/integrations/catalog/okta.json +38 -0
- package/integrations/catalog/onedrive.json +38 -0
- package/integrations/catalog/ordinal.json +33 -0
- package/integrations/catalog/paypal.json +5 -8
- package/integrations/catalog/pipedrive.json +39 -0
- package/integrations/catalog/plaid.json +31 -0
- package/integrations/catalog/playwright.json +0 -2
- package/integrations/catalog/posthog.json +38 -0
- package/integrations/catalog/quickbooks.json +38 -0
- package/integrations/catalog/redis.json +1 -3
- package/integrations/catalog/resend.json +1 -2
- package/integrations/catalog/salesforce.json +38 -0
- package/integrations/catalog/sentry.json +52 -14
- package/integrations/catalog/sequential-thinking.json +0 -2
- package/integrations/catalog/servicenow.json +39 -0
- package/integrations/catalog/sharepoint.json +38 -0
- package/integrations/catalog/shopify.json +38 -0
- package/integrations/catalog/slack.json +49 -10
- package/integrations/catalog/stripe.json +44 -14
- package/integrations/catalog/supabase.json +36 -9
- package/integrations/catalog/tavily.json +0 -2
- package/integrations/catalog/time.json +0 -2
- package/integrations/catalog/trello.json +31 -0
- package/integrations/catalog/vercel.json +38 -0
- package/integrations/catalog/webflow.json +34 -0
- package/integrations/catalog/xero.json +38 -0
- package/integrations/catalog/zendesk.json +38 -0
- package/integrations/catalog/zoom.json +38 -0
- package/integrations/catalog-index.js +171 -0
- package/integrations/index.d.ts +26 -60
- package/integrations/index.js +29 -207
- package/marketplaces/openhands-extensions.json +14 -1
- package/package.json +4 -7
- package/plugins/cobol-modernization/README.md +3 -3
- package/plugins/cobol-modernization/skills/{cobol-modernization-overview → cobol-modernization}/SKILL.md +1 -1
- package/plugins/magic-test/skills/magic-word/SKILL.md +1 -0
- package/plugins/migration-scoring/README.md +1 -1
- package/pyproject.toml +17 -3
- package/python/openhands_extensions/__init__.py +24 -0
- package/python/openhands_extensions/_version.py +24 -0
- package/python/openhands_extensions/integrations.py +92 -0
- package/release-please-config.json +2 -1
- package/scripts/build-integration-catalog.mjs +45 -0
- package/skills/agent-canvas-environment/.plugin/plugin.json +19 -0
- package/skills/agent-canvas-environment/README.md +5 -0
- package/skills/agent-canvas-environment/SKILL.md +141 -0
- package/skills/github-pr-reviewer/references/state-schema.md +18 -4
- package/skills/github-pr-reviewer/scripts/main.py +66 -12
- package/skills/github-repo-monitor/references/state-schema.md +11 -4
- package/skills/github-repo-monitor/scripts/main.py +67 -19
- package/skills/github-repo-monitor/tests/test_main.py +67 -8
- package/skills/index.js +19 -5
- package/skills/openhands-api/README.md +5 -1
- package/skills/openhands-api/SKILL.md +74 -5
- package/skills/openhands-automation/SKILL.md +3 -1
- package/skills/openhands-automation/references/custom-automation.md +166 -0
- package/skills/openhands-sdk/SKILL.md +8 -2
- package/skills/slack-channel-monitor/README.md +9 -5
- package/skills/slack-channel-monitor/SKILL.md +16 -6
- package/skills/slack-channel-monitor/references/slack-api.md +5 -3
- package/skills/slack-channel-monitor/references/state-schema.md +70 -36
- package/skills/slack-channel-monitor/scripts/main.py +335 -63
- package/tests/test_catalogs.py +21 -3
- package/tests/test_integration_catalog_in_sync.py +204 -0
- package/tests/test_slack_channel_monitor.py +142 -0
- package/tests/test_version_alignment.py +64 -0
- package/integrations/logos.d.ts +0 -5
- package/integrations/logos.js +0 -94
- package/integrations/oauth-provider-catalog.js +0 -553
- package/integrations/oauth-provider-registration-defaults.js +0 -610
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/SKILL.md +0 -0
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/references/cics-transformation-examples.md +0 -0
- /package/plugins/migration-scoring/skills/{migration-scoring-overview → migration-scoring}/SKILL.md +0 -0
package/pyproject.toml
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
name = "extensions"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "OpenHands extensions, plugins, and skills"
|
|
2
|
+
name = "openhands-extensions"
|
|
3
|
+
version = "0.6.0"
|
|
4
|
+
description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
|
|
7
|
+
[build-system]
|
|
8
|
+
requires = ["hatchling>=1.21"]
|
|
9
|
+
build-backend = "hatchling.build"
|
|
10
|
+
|
|
11
|
+
[tool.hatch.build.targets.wheel]
|
|
12
|
+
packages = ["python/openhands_extensions"]
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
15
|
+
"integrations/catalog" = "openhands_extensions/catalog"
|
|
16
|
+
|
|
7
17
|
[dependency-groups]
|
|
8
18
|
test = [
|
|
9
19
|
"pytest>=8.0",
|
|
10
20
|
"requests>=2.31",
|
|
11
21
|
"openhands-sdk>=0.3",
|
|
12
22
|
]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
pythonpath = ["python"]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Python bindings for the OpenHands extensions catalogs.
|
|
2
|
+
|
|
3
|
+
Mirrors the JavaScript package (``@openhands/extensions``): both read the same
|
|
4
|
+
hand-authored ``integrations/catalog/<id>.json`` files. The JavaScript package
|
|
5
|
+
uses a generated static import index; the Python package loads the packaged
|
|
6
|
+
individual JSON files directly. See ``integrations.py`` for the catalog API,
|
|
7
|
+
including filtering by connector type (mcp / oauth).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
from .integrations import (
|
|
14
|
+
INTEGRATION_CATALOG_SNAPSHOT,
|
|
15
|
+
get_integration_catalog_entry,
|
|
16
|
+
list_integration_catalog,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"INTEGRATION_CATALOG_SNAPSHOT",
|
|
21
|
+
"__version__",
|
|
22
|
+
"get_integration_catalog_entry",
|
|
23
|
+
"list_integration_catalog",
|
|
24
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Package version, derived from installed package metadata.
|
|
2
|
+
|
|
3
|
+
The single source of truth is ``pyproject.toml``'s ``[project].version``
|
|
4
|
+
(which ``release-please`` bumps together with ``package.json`` via
|
|
5
|
+
``release-please-config.json`` -> ``extra-files``). At runtime we read the
|
|
6
|
+
installed distribution metadata via ``importlib.metadata`` so there is no
|
|
7
|
+
second hand-maintained version string. ``_FALLBACK_VERSION`` is only used
|
|
8
|
+
when the package is imported without being installed (e.g. straight off a
|
|
9
|
+
source checkout with no build), and ``tests/test_version_alignment.py``
|
|
10
|
+
asserts it stays in lock-step with ``pyproject.toml`` / ``package.json``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
#: Fallback used only when the package is not installed (no dist metadata).
|
|
18
|
+
#: Kept in lock-step with pyproject.toml/package.json by the version test.
|
|
19
|
+
_FALLBACK_VERSION = "0.6.0"
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__: str = version("openhands-extensions")
|
|
23
|
+
except PackageNotFoundError: # not installed (e.g. raw source checkout)
|
|
24
|
+
__version__ = _FALLBACK_VERSION
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Python bindings for the OpenHands extensions integration catalog.
|
|
2
|
+
|
|
3
|
+
The source of truth is the hand-authored ``integrations/catalog/<id>.json``
|
|
4
|
+
directory. Wheels include those individual JSON files directly; no aggregate
|
|
5
|
+
catalog JSON is authored or packaged.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from importlib import resources
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Iterable
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"INTEGRATION_CATALOG_SNAPSHOT",
|
|
19
|
+
"get_integration_catalog_entry",
|
|
20
|
+
"list_integration_catalog",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _repo_catalog_dir() -> Path:
|
|
25
|
+
return Path(__file__).resolve().parents[2] / "integrations" / "catalog"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _catalog_files() -> Iterable[Any]:
|
|
29
|
+
packaged = resources.files(__package__).joinpath("catalog")
|
|
30
|
+
if packaged.is_dir():
|
|
31
|
+
return sorted(
|
|
32
|
+
(path for path in packaged.iterdir() if path.name.endswith(".json")),
|
|
33
|
+
key=lambda path: path.name,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
repo_catalog = _repo_catalog_dir()
|
|
37
|
+
return sorted(repo_catalog.glob("*.json"), key=lambda path: path.name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_json(path: Any) -> dict[str, Any]:
|
|
41
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@lru_cache(maxsize=1)
|
|
45
|
+
def _integrations() -> tuple[dict[str, Any], ...]:
|
|
46
|
+
entries = [_read_json(path) for path in _catalog_files()]
|
|
47
|
+
entries.sort(
|
|
48
|
+
key=lambda entry: (-(entry.get("popularityRank") if entry.get("popularityRank") is not None else -1), entry["id"]),
|
|
49
|
+
)
|
|
50
|
+
return tuple(entries)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@lru_cache(maxsize=1)
|
|
54
|
+
def _integration_by_id() -> dict[str, dict[str, Any]]:
|
|
55
|
+
return {entry["id"]: entry for entry in _integrations()}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _entry_supports_mcp(entry: dict[str, Any]) -> bool:
|
|
59
|
+
return any(option.get("provider") == "mcp" for option in entry.get("connectionOptions", []))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _entry_supports_oauth(entry: dict[str, Any]) -> bool:
|
|
63
|
+
return any(
|
|
64
|
+
option.get("auth", {}).get("strategy") == "oauth2"
|
|
65
|
+
for option in entry.get("connectionOptions", [])
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def list_integration_catalog(
|
|
70
|
+
mcp: bool | None = None,
|
|
71
|
+
oauth: bool | None = None,
|
|
72
|
+
) -> list[dict[str, Any]]:
|
|
73
|
+
"""Return the integration catalog, optionally filtered by connector type."""
|
|
74
|
+
result = []
|
|
75
|
+
for entry in _integrations():
|
|
76
|
+
if mcp is not None and _entry_supports_mcp(entry) != mcp:
|
|
77
|
+
continue
|
|
78
|
+
if oauth is not None and _entry_supports_oauth(entry) != oauth:
|
|
79
|
+
continue
|
|
80
|
+
result.append(copy.deepcopy(entry))
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_integration_catalog_entry(id: str) -> dict[str, Any] | None:
|
|
85
|
+
"""Return one integration catalog entry by id, or ``None``."""
|
|
86
|
+
entry = _integration_by_id().get(id)
|
|
87
|
+
return copy.deepcopy(entry) if entry is not None else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
INTEGRATION_CATALOG_SNAPSHOT: dict[str, Any] = {
|
|
91
|
+
"integrations": copy.deepcopy(list(_integrations()))
|
|
92
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"include-component-in-tag": false,
|
|
10
10
|
"skip-changelog": true,
|
|
11
11
|
"extra-files": [
|
|
12
|
-
{ "type": "toml", "path": "pyproject.toml", "jsonpath": "$.project.version" }
|
|
12
|
+
{ "type": "toml", "path": "pyproject.toml", "jsonpath": "$.project.version" },
|
|
13
|
+
"python/openhands_extensions/_version.py"
|
|
13
14
|
]
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const root = process.cwd();
|
|
5
|
+
const catalogDir = path.join(root, "integrations", "catalog");
|
|
6
|
+
const outputPath = path.join(root, "integrations", "catalog-index.js");
|
|
7
|
+
|
|
8
|
+
const files = (await readdir(catalogDir))
|
|
9
|
+
.filter((file) => file.endsWith(".json"))
|
|
10
|
+
.sort((left, right) => left.localeCompare(right));
|
|
11
|
+
|
|
12
|
+
const integrations = await Promise.all(
|
|
13
|
+
files.map(async (file) => ({
|
|
14
|
+
file,
|
|
15
|
+
entry: JSON.parse(await readFile(path.join(catalogDir, file), "utf8")),
|
|
16
|
+
})),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
integrations.sort((left, right) => {
|
|
20
|
+
const rank = (right.entry.popularityRank ?? -1) - (left.entry.popularityRank ?? -1);
|
|
21
|
+
return rank || left.entry.id.localeCompare(right.entry.id);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const indexedIntegrations = integrations.map((integration, index) => ({
|
|
25
|
+
...integration,
|
|
26
|
+
importName: `entry${index}`,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const imports = indexedIntegrations
|
|
30
|
+
.map(({ file, importName }) => `import ${importName} from "./catalog/${file}" with { type: "json" };`)
|
|
31
|
+
.join("\n");
|
|
32
|
+
const entries = indexedIntegrations.map(({ importName }) => ` ${importName},`).join("\n");
|
|
33
|
+
|
|
34
|
+
const header = `// This file is auto-generated by scripts/build-integration-catalog.mjs.
|
|
35
|
+
// Do not edit it manually. To update it after changing integrations/catalog/*.json,
|
|
36
|
+
// run: npm run build:integrations
|
|
37
|
+
|
|
38
|
+
`;
|
|
39
|
+
const body = `${header}${imports}
|
|
40
|
+
|
|
41
|
+
export const INTEGRATION_CATALOG_ENTRIES = [
|
|
42
|
+
${entries}
|
|
43
|
+
];
|
|
44
|
+
`;
|
|
45
|
+
await writeFile(outputPath, body);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-canvas-environment",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, frontend/backend port discovery, safe workspace hygiene, and local conversation delegation.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "OpenHands",
|
|
7
|
+
"email": "contact@all-hands.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenHands/extensions",
|
|
10
|
+
"repository": "https://github.com/OpenHands/extensions",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent-canvas",
|
|
14
|
+
"openhands",
|
|
15
|
+
"local",
|
|
16
|
+
"conversation",
|
|
17
|
+
"delegation"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-canvas-environment
|
|
3
|
+
description: Work effectively inside a local Agent Canvas environment, including local agent-server auth, frontend/backend port discovery, safe workspace hygiene, and delegating work to a new local conversation through POST /api/conversations.
|
|
4
|
+
triggers:
|
|
5
|
+
- agent canvas
|
|
6
|
+
- agent-canvas
|
|
7
|
+
- local conversation
|
|
8
|
+
- delegate local conversation
|
|
9
|
+
- session api key
|
|
10
|
+
- X-Session-API-Key
|
|
11
|
+
- localhost:8001
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Agent Canvas Environment
|
|
15
|
+
|
|
16
|
+
Use this skill when running inside or alongside a local Agent Canvas stack, especially when the user asks to inspect the local backend, create or monitor local conversations, or delegate work to another local conversation.
|
|
17
|
+
|
|
18
|
+
## Core rules
|
|
19
|
+
|
|
20
|
+
- Treat the local Agent Canvas backend as an agent-server API, usually `http://localhost:8001`.
|
|
21
|
+
- Treat the local UI as a separate frontend, usually `http://localhost:8000`.
|
|
22
|
+
- Do not print session API keys. Pass them directly in `X-Session-API-Key`.
|
|
23
|
+
- Trust any runtime-services block or explicit user-provided host over default ports.
|
|
24
|
+
- Before mutating a repository, check `git status -sb`. If a worktree has unrelated changes, use a separate worktree or clone.
|
|
25
|
+
- When delegating, write a self-contained prompt. The new conversation does not inherit the current chat context.
|
|
26
|
+
|
|
27
|
+
## Find the session key
|
|
28
|
+
|
|
29
|
+
Use the first available value, without echoing it:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
KEY="${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}"
|
|
33
|
+
if [ -z "$KEY" ] && [ -f "$HOME/.openhands/agent-canvas/api-key.txt" ]; then
|
|
34
|
+
KEY="$(tr -d '\n' < "$HOME/.openhands/agent-canvas/api-key.txt")"
|
|
35
|
+
fi
|
|
36
|
+
test -n "$KEY" || { echo "No Agent Canvas session API key found" >&2; exit 1; }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Validate backend access:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -sS -o /tmp/agent-canvas-conversations.json -w '%{http_code}\n' \
|
|
43
|
+
-H "X-Session-API-Key: $KEY" \
|
|
44
|
+
http://localhost:8001/api/conversations/search
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
HTTP `200` means the backend and key work.
|
|
48
|
+
|
|
49
|
+
## Delegate to a local conversation
|
|
50
|
+
|
|
51
|
+
Use `POST /api/conversations` with:
|
|
52
|
+
|
|
53
|
+
- current `/api/settings` as the base agent profile
|
|
54
|
+
- a fresh absolute workspace directory
|
|
55
|
+
- `initial_message.run: true`
|
|
56
|
+
- `worktree: false` when the workspace is already isolated
|
|
57
|
+
|
|
58
|
+
Template:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
set -euo pipefail
|
|
62
|
+
|
|
63
|
+
BASE="${AGENT_CANVAS_BACKEND:-http://localhost:8001}"
|
|
64
|
+
KEY="${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}"
|
|
65
|
+
if [ -z "$KEY" ] && [ -f "$HOME/.openhands/agent-canvas/api-key.txt" ]; then
|
|
66
|
+
KEY="$(tr -d '\n' < "$HOME/.openhands/agent-canvas/api-key.txt")"
|
|
67
|
+
fi
|
|
68
|
+
test -n "$KEY" || { echo "No Agent Canvas session API key found" >&2; exit 1; }
|
|
69
|
+
|
|
70
|
+
WORKDIR="${WORKDIR:-$HOME/workspace/delegated/$(date +%Y%m%d-%H%M%S)}"
|
|
71
|
+
mkdir -p "$WORKDIR"
|
|
72
|
+
|
|
73
|
+
SETTINGS_JSON="$(curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/settings")"
|
|
74
|
+
PROMPT='Write a complete, task-specific prompt here. Include repo, branch, constraints, validation, and expected report.'
|
|
75
|
+
|
|
76
|
+
PAYLOAD="$(jq -n --argjson settings "$SETTINGS_JSON" --arg prompt "$PROMPT" --arg workdir "$WORKDIR" '
|
|
77
|
+
def agent_settings:
|
|
78
|
+
($settings.agent_settings // {})
|
|
79
|
+
| del(.schema_version)
|
|
80
|
+
| . + {
|
|
81
|
+
agent_context: ((.agent_context // {}) + {
|
|
82
|
+
load_public_skills: true,
|
|
83
|
+
load_user_skills: true,
|
|
84
|
+
load_project_skills: true
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
($settings.conversation_settings // {}) as $conv |
|
|
88
|
+
{
|
|
89
|
+
agent_settings: agent_settings,
|
|
90
|
+
workspace: {kind: "LocalWorkspace", working_dir: $workdir},
|
|
91
|
+
confirmation_policy: {kind: "NeverConfirm"},
|
|
92
|
+
max_iterations: (($conv.max_iterations // 80) | if . == null then 80 else . end),
|
|
93
|
+
stuck_detection: true,
|
|
94
|
+
autotitle: true,
|
|
95
|
+
worktree: false,
|
|
96
|
+
initial_message: {
|
|
97
|
+
role: "user",
|
|
98
|
+
content: [{type: "text", text: $prompt}],
|
|
99
|
+
run: true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
')"
|
|
103
|
+
|
|
104
|
+
curl -sS -X POST "$BASE/api/conversations" \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-H "X-Session-API-Key: $KEY" \
|
|
107
|
+
--data-binary "$PAYLOAD" | jq '{id, title, execution_status, workspace}'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Report both links:
|
|
111
|
+
|
|
112
|
+
- UI: `http://localhost:8000/conversations/<conversation_id>`
|
|
113
|
+
- API: `http://localhost:8001/api/conversations/<conversation_id>`
|
|
114
|
+
|
|
115
|
+
## Monitor a delegated conversation
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
CID="<conversation_id>"
|
|
119
|
+
curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/conversations/$CID" \
|
|
120
|
+
| jq '{id, title, execution_status, updated_at, workspace, agent_kind: .agent.kind, current_model_id, current_model_name}'
|
|
121
|
+
|
|
122
|
+
curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/conversations/$CID/events/search?limit=20" \
|
|
123
|
+
| jq '.events // .items // .'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Terminal statuses commonly include `idle`, `running`, `finished`, `error`, `stuck`, and `stopped`.
|
|
127
|
+
|
|
128
|
+
## Prompt checklist for delegation
|
|
129
|
+
|
|
130
|
+
Include:
|
|
131
|
+
|
|
132
|
+
- repository owner/name and local path if relevant
|
|
133
|
+
- branch, PR, issue, or Linear ticket identifiers
|
|
134
|
+
- current status and known blockers
|
|
135
|
+
- exact files or subsystems in scope
|
|
136
|
+
- dirty-worktree warnings and paths not to touch
|
|
137
|
+
- whether to push, open a PR, or only report
|
|
138
|
+
- checks/tests to run
|
|
139
|
+
- expected final report format
|
|
140
|
+
|
|
141
|
+
Do not rely on the new conversation knowing anything from the current thread.
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
# State
|
|
1
|
+
# State Schema
|
|
2
2
|
|
|
3
|
-
The automation maintains a JSON state
|
|
3
|
+
The automation maintains a JSON state document that persists across polling runs.
|
|
4
4
|
It is the source of truth for which trigger-label events have queued reviews
|
|
5
5
|
and which conversations are still active.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Storage
|
|
10
|
+
|
|
11
|
+
**Primary (cloud):** The state is stored in the automation service's built-in KV
|
|
12
|
+
store under the key `"state"`. The KV store is available when `AUTOMATION_KV_TOKEN`
|
|
13
|
+
is injected into the run environment. Each automation has its own isolated namespace.
|
|
14
|
+
|
|
15
|
+
**Fallback (local/dev):** When the KV store is not available, the state is written
|
|
16
|
+
to a local JSON file at:
|
|
10
17
|
|
|
11
18
|
```
|
|
12
19
|
{WORKSPACE_BASE_ROOT}/automation-state/github_pr_reviewer_label_event_{automation_id}.json
|
|
@@ -116,8 +123,15 @@ Trigger label applied on GitHub
|
|
|
116
123
|
## Resetting State
|
|
117
124
|
|
|
118
125
|
To force the automation to reconsider previous label events, delete the state
|
|
119
|
-
file
|
|
126
|
+
from the KV store (cloud) or the fallback file (local).
|
|
127
|
+
|
|
128
|
+
**Cloud (KV store):**
|
|
129
|
+
```bash
|
|
130
|
+
curl -X DELETE "${OPENHANDS_HOST}/api/automation/v1/kv/state" \
|
|
131
|
+
-H "Authorization: Bearer ${AUTOMATION_KV_TOKEN}"
|
|
132
|
+
```
|
|
120
133
|
|
|
134
|
+
**Local (file fallback):**
|
|
121
135
|
```bash
|
|
122
136
|
rm ~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_<id>.json
|
|
123
137
|
```
|
|
@@ -67,6 +67,45 @@ def fire_callback(
|
|
|
67
67
|
print(f"Callback error (non-fatal): {exc}")
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
# ── State persistence (KV store with local-file fallback) ─────────────────────
|
|
71
|
+
|
|
72
|
+
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
|
|
73
|
+
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
|
|
74
|
+
_STATE_KEY = "state"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _kv_available() -> bool:
|
|
78
|
+
return bool(_KV_TOKEN and _KV_BASE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _kv_get(key: str) -> dict | None:
|
|
82
|
+
req = urllib.request.Request(
|
|
83
|
+
f"{_KV_BASE}/v1/kv/{key}",
|
|
84
|
+
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
with urllib.request.urlopen(req) as r:
|
|
88
|
+
return json.loads(r.read())["value"]
|
|
89
|
+
except urllib.error.HTTPError as exc:
|
|
90
|
+
if exc.code == 404:
|
|
91
|
+
return None
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _kv_set(key: str, value: dict) -> None:
|
|
96
|
+
req = urllib.request.Request(
|
|
97
|
+
f"{_KV_BASE}/v1/kv/{key}",
|
|
98
|
+
data=json.dumps(value).encode(),
|
|
99
|
+
headers={
|
|
100
|
+
"Authorization": f"Bearer {_KV_TOKEN}",
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
},
|
|
103
|
+
method="PUT",
|
|
104
|
+
)
|
|
105
|
+
with urllib.request.urlopen(req) as r:
|
|
106
|
+
r.read()
|
|
107
|
+
|
|
108
|
+
|
|
70
109
|
def _state_file_path() -> str:
|
|
71
110
|
workspace_base = os.environ.get("WORKSPACE_BASE", "")
|
|
72
111
|
event_payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))
|
|
@@ -82,13 +121,7 @@ def _state_file_path() -> str:
|
|
|
82
121
|
return str(state_dir / f"github_pr_reviewer_label_event_{automation_id}.json")
|
|
83
122
|
|
|
84
123
|
|
|
85
|
-
def
|
|
86
|
-
if os.path.exists(path):
|
|
87
|
-
try:
|
|
88
|
-
with open(path) as f:
|
|
89
|
-
return json.load(f)
|
|
90
|
-
except (json.JSONDecodeError, OSError) as exc:
|
|
91
|
-
print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
|
|
124
|
+
def _default_state() -> dict:
|
|
92
125
|
return {
|
|
93
126
|
"version": 2,
|
|
94
127
|
"repo": REPO,
|
|
@@ -98,11 +131,34 @@ def load_state(path: str) -> dict:
|
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
|
|
101
|
-
def
|
|
134
|
+
def load_state() -> dict:
|
|
135
|
+
if _kv_available():
|
|
136
|
+
data = _kv_get(_STATE_KEY)
|
|
137
|
+
if data is not None:
|
|
138
|
+
print("State loaded from KV store")
|
|
139
|
+
return data
|
|
140
|
+
return _default_state()
|
|
141
|
+
path = _state_file_path()
|
|
142
|
+
if os.path.exists(path):
|
|
143
|
+
try:
|
|
144
|
+
with open(path) as f:
|
|
145
|
+
return json.load(f)
|
|
146
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
147
|
+
print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
|
|
148
|
+
return _default_state()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def save_state(state: dict) -> None:
|
|
152
|
+
if _kv_available():
|
|
153
|
+
_kv_set(_STATE_KEY, state)
|
|
154
|
+
print("State saved to KV store")
|
|
155
|
+
return
|
|
156
|
+
path = _state_file_path()
|
|
102
157
|
tmp_path = f"{path}.tmp"
|
|
103
158
|
with open(tmp_path, "w") as f:
|
|
104
159
|
json.dump(state, f, indent=2, sort_keys=True)
|
|
105
160
|
os.replace(tmp_path, path)
|
|
161
|
+
print(f"State saved to {path}")
|
|
106
162
|
|
|
107
163
|
|
|
108
164
|
def _github_request(
|
|
@@ -522,8 +578,7 @@ def _check_conversation_completion(
|
|
|
522
578
|
|
|
523
579
|
|
|
524
580
|
def main() -> str | None:
|
|
525
|
-
|
|
526
|
-
state = load_state(state_path)
|
|
581
|
+
state = load_state()
|
|
527
582
|
agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
528
583
|
api_key = _get_env_key()
|
|
529
584
|
|
|
@@ -591,8 +646,7 @@ def main() -> str | None:
|
|
|
591
646
|
state["repo"] = REPO
|
|
592
647
|
state["trigger_label"] = TRIGGER_LABEL
|
|
593
648
|
state["updated_at"] = time.time()
|
|
594
|
-
save_state(
|
|
595
|
-
print(f"State saved → {state_path}")
|
|
649
|
+
save_state(state)
|
|
596
650
|
return last_conversation_id
|
|
597
651
|
|
|
598
652
|
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
# State
|
|
1
|
+
# State Schema
|
|
2
2
|
|
|
3
|
-
The automation maintains a JSON state
|
|
4
|
-
This
|
|
3
|
+
The automation maintains a JSON state document that persists across polling runs.
|
|
4
|
+
This document is the source of truth for which conversations are active, which
|
|
5
5
|
comment IDs have already been processed, and the timestamp of the last poll.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Storage
|
|
10
|
+
|
|
11
|
+
**Primary (cloud):** The state is stored in the automation service's built-in KV
|
|
12
|
+
store under the key `"state"`. The KV store is available when `AUTOMATION_KV_TOKEN`
|
|
13
|
+
is injected into the run environment. Each automation has its own isolated namespace.
|
|
14
|
+
|
|
15
|
+
**Fallback (local/dev):** When the KV store is not available, the state is written
|
|
16
|
+
to a local JSON file at:
|
|
10
17
|
|
|
11
18
|
```
|
|
12
19
|
{WORKSPACE_BASE_ROOT}/automation-state/github_poller_{automation_id}.json
|