agentic-runner 2.6.1__tar.gz → 2.8.0__tar.gz
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.
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/PKG-INFO +4 -1
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/pyproject.toml +11 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/activities.py +106 -3
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/registration.py +4 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/service.py +35 -4
- agentic_runner-2.8.0/src/agentic_runner/testing/__init__.py +18 -0
- agentic_runner-2.8.0/src/agentic_runner/testing/__main__.py +32 -0
- agentic_runner-2.8.0/src/agentic_runner/testing/control_plane.py +441 -0
- agentic_runner-2.8.0/src/agentic_runner/testing/plugin.py +178 -0
- agentic_runner-2.8.0/src/agentic_runner/testing/test_conformance.py +163 -0
- agentic_runner-2.8.0/src/agentic_runner/testing/workflow.py +37 -0
- agentic_runner-2.8.0/src/agentic_runner/workers/skills.py +164 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/.gitignore +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/LICENSE +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/README.md +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/cli.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/device_login_activities.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-2.6.1 → agentic_runner-2.8.0}/src/agentic_runner/workstation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentic-runner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,6 +12,9 @@ Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
|
12
12
|
Requires-Dist: pydantic<3.0,>=2.0
|
|
13
13
|
Requires-Dist: pyjwt[crypto]>=2.13.0
|
|
14
14
|
Requires-Dist: temporalio<2.0,>=1.10
|
|
15
|
+
Provides-Extra: testing
|
|
16
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.24; extra == 'testing'
|
|
17
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'testing'
|
|
15
18
|
Description-Content-Type: text/markdown
|
|
16
19
|
|
|
17
20
|
# agentic-runner
|
|
@@ -19,9 +19,20 @@ dependencies = [
|
|
|
19
19
|
"temporalio>=1.10,<2.0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
# The conformance kit (`agentic_runner.testing`, docs/conformance.md): the fake control
|
|
24
|
+
# plane needs nothing beyond the dependencies above; the scenario suite needs these.
|
|
25
|
+
testing = [
|
|
26
|
+
"pytest>=8.0,<9.0",
|
|
27
|
+
"pytest-asyncio>=0.24,<1.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
22
30
|
[project.scripts]
|
|
23
31
|
agentic-runner = "agentic_runner.cli:main"
|
|
24
32
|
|
|
33
|
+
[project.entry-points.pytest11]
|
|
34
|
+
agentic_runner = "agentic_runner.testing.plugin"
|
|
35
|
+
|
|
25
36
|
[build-system]
|
|
26
37
|
requires = ["hatchling>=1.27,<2.0"]
|
|
27
38
|
build-backend = "hatchling.build"
|
|
@@ -129,6 +129,14 @@ from agentic_runner.workers.fastapi_client import (
|
|
|
129
129
|
)
|
|
130
130
|
from agentic_runner.workers.harness_usage import extract_claude_result, extract_codex_turn_usage
|
|
131
131
|
from agentic_runner.workers.mcp_config import McpServerEntry
|
|
132
|
+
from agentic_runner.workers.skills import (
|
|
133
|
+
SkillDelivery,
|
|
134
|
+
delivery_for,
|
|
135
|
+
first_digest_mismatch,
|
|
136
|
+
remove_skills,
|
|
137
|
+
skills_preamble,
|
|
138
|
+
write_skills,
|
|
139
|
+
)
|
|
132
140
|
from agentic_runner_contracts.activity_io import (
|
|
133
141
|
LEARNER_CHILDREN_BUDGET_BYTES,
|
|
134
142
|
LEARNING_FAILED,
|
|
@@ -238,6 +246,7 @@ from agentic_runner_contracts.routing import (
|
|
|
238
246
|
from agentic_runner_contracts.runtime_context import (
|
|
239
247
|
EGRESS_ALLOW_LIST_KEY,
|
|
240
248
|
McpServerSpec,
|
|
249
|
+
SkillVersionSpec,
|
|
241
250
|
WorkerRuntimeContext,
|
|
242
251
|
WorkerRuntimeContextResolver,
|
|
243
252
|
work_branch_name,
|
|
@@ -491,6 +500,8 @@ class _RuntimeContextState:
|
|
|
491
500
|
experience: str = ""
|
|
492
501
|
experience_lesson_ids: tuple[str, ...] = ()
|
|
493
502
|
experience_truncated: bool = False
|
|
503
|
+
# Console-v2 issue 28: the Skill versions attached to the Agent.
|
|
504
|
+
skills: tuple[SkillVersionSpec, ...] = ()
|
|
494
505
|
workspace_path: Path | None = None
|
|
495
506
|
branch_head_sha: str | None = None
|
|
496
507
|
# The Autonomy Policy's tier source (PRD issue 14) -- see WorkerRuntimeContext for
|
|
@@ -533,6 +544,9 @@ class _DirectiveAttempt:
|
|
|
533
544
|
# PRD issue 58: what config assembly wrote for the CLI, and where it may reach.
|
|
534
545
|
mcp_servers: tuple[McpServerEntry, ...] | None = None
|
|
535
546
|
egress_allow_list: tuple[str, ...] = ()
|
|
547
|
+
# Console-v2 issue 28: the attached Skills, for a runtime with no skills directory to
|
|
548
|
+
# write them into. Prefixed to the Directive prompt; empty when there is none.
|
|
549
|
+
skills_preamble: str = ""
|
|
536
550
|
|
|
537
551
|
async def run_environment_hook(self) -> None:
|
|
538
552
|
self.extra_env.update(await self.hooks.environment())
|
|
@@ -1325,6 +1339,7 @@ _ROUTING_EVIDENCE_SOURCE = "runner.routing"
|
|
|
1325
1339
|
PRIOR_ATTEMPT_ALIVE_EVIDENCE_SOURCE = "runner.prior_attempt_alive"
|
|
1326
1340
|
DIRECTIVE_TOKEN_EVIDENCE_SOURCE = "runner.directive_token"
|
|
1327
1341
|
AGENT_RUNTIME_EVIDENCE_SOURCE = "runner.agent_runtime"
|
|
1342
|
+
SKILLS_EVIDENCE_SOURCE = "runner.skills"
|
|
1328
1343
|
|
|
1329
1344
|
|
|
1330
1345
|
class _EvidenceWriter(Protocol):
|
|
@@ -1793,7 +1808,8 @@ class RunnerRalphActivities:
|
|
|
1793
1808
|
extra_env=attempt.env,
|
|
1794
1809
|
mcp_servers=attempt.mcp_servers,
|
|
1795
1810
|
egress_allow_list=attempt.egress_allow_list,
|
|
1796
|
-
prompt=
|
|
1811
|
+
prompt=attempt.skills_preamble
|
|
1812
|
+
+ _directive_prompt(
|
|
1797
1813
|
completion_criteria=runtime_state.completion_criteria,
|
|
1798
1814
|
pr_body=request.pr_body,
|
|
1799
1815
|
persona_preamble=_persona_prompt_preamble(
|
|
@@ -2258,6 +2274,7 @@ class RunnerRalphActivities:
|
|
|
2258
2274
|
work_record_id, source=source, payload=dict(payload)
|
|
2259
2275
|
)
|
|
2260
2276
|
|
|
2277
|
+
await self._refuse_skill_digest_mismatch(work_record_id, state)
|
|
2261
2278
|
credentials = await self._resolve_credentials(work_record_id, state)
|
|
2262
2279
|
if credentials is not None:
|
|
2263
2280
|
# Names only, at the moment they resolved (22 A1's Evidence): a support read
|
|
@@ -2321,6 +2338,13 @@ class RunnerRalphActivities:
|
|
|
2321
2338
|
credentials=credentials,
|
|
2322
2339
|
env=callback_env,
|
|
2323
2340
|
)
|
|
2341
|
+
delivered_preamble = await self._deliver_skills(
|
|
2342
|
+
stack,
|
|
2343
|
+
work_record_id=work_record_id,
|
|
2344
|
+
state=state,
|
|
2345
|
+
sandbox=sandbox,
|
|
2346
|
+
directive_number=directive_number,
|
|
2347
|
+
)
|
|
2324
2348
|
session = await stack.enter_async_context(
|
|
2325
2349
|
DirectiveHookSession(
|
|
2326
2350
|
hooks=self._hooks,
|
|
@@ -2342,8 +2366,85 @@ class RunnerRalphActivities:
|
|
|
2342
2366
|
credentials=credentials,
|
|
2343
2367
|
mcp_servers=mcp_servers,
|
|
2344
2368
|
egress_allow_list=egress_allow_list,
|
|
2369
|
+
skills_preamble=delivered_preamble,
|
|
2345
2370
|
)
|
|
2346
2371
|
|
|
2372
|
+
async def _refuse_skill_digest_mismatch(
|
|
2373
|
+
self, work_record_id: str, state: _RuntimeContextState
|
|
2374
|
+
) -> None:
|
|
2375
|
+
"""A Skill whose body is not the version that was published fails the Directive
|
|
2376
|
+
non-retryably (console-v2 issue 28): a retry would fetch the same body, and
|
|
2377
|
+
running on it would hand the Agent prompt material no person reviewed."""
|
|
2378
|
+
|
|
2379
|
+
mismatch = first_digest_mismatch(state.skills)
|
|
2380
|
+
if mismatch is None:
|
|
2381
|
+
return
|
|
2382
|
+
await _raise_recorded(
|
|
2383
|
+
ApplicationError(
|
|
2384
|
+
f"Skill {mismatch.slug} version {mismatch.version} does not match its sha256",
|
|
2385
|
+
non_retryable=True,
|
|
2386
|
+
),
|
|
2387
|
+
self._fastapi_client,
|
|
2388
|
+
work_record_id,
|
|
2389
|
+
source=SKILLS_EVIDENCE_SOURCE,
|
|
2390
|
+
actor=self._actor(),
|
|
2391
|
+
payload={
|
|
2392
|
+
"event": "skills.digest_mismatch",
|
|
2393
|
+
"agent_id": state.agent_id,
|
|
2394
|
+
"slug": mismatch.slug,
|
|
2395
|
+
"version": mismatch.version,
|
|
2396
|
+
"sha256": mismatch.sha256,
|
|
2397
|
+
},
|
|
2398
|
+
)
|
|
2399
|
+
|
|
2400
|
+
async def _deliver_skills(
|
|
2401
|
+
self,
|
|
2402
|
+
stack: contextlib.AsyncExitStack,
|
|
2403
|
+
*,
|
|
2404
|
+
work_record_id: str,
|
|
2405
|
+
state: _RuntimeContextState,
|
|
2406
|
+
sandbox: DirectiveSandbox | None,
|
|
2407
|
+
directive_number: int,
|
|
2408
|
+
) -> str:
|
|
2409
|
+
"""Put the attached Skills where this runtime reads them; return any preamble.
|
|
2410
|
+
|
|
2411
|
+
Written files are removed on the attempt's exit, success or failure. A Runner
|
|
2412
|
+
with no Contract sandbox has no harness root it owns, so it falls back to the
|
|
2413
|
+
preamble rather than writing into a shared ``CODEX_HOME``.
|
|
2414
|
+
"""
|
|
2415
|
+
|
|
2416
|
+
if not state.skills:
|
|
2417
|
+
return ""
|
|
2418
|
+
delivery = delivery_for(state.cli_kind)
|
|
2419
|
+
if delivery is SkillDelivery.DIRECTORY and sandbox is None:
|
|
2420
|
+
delivery = SkillDelivery.PROMPT_PREAMBLE
|
|
2421
|
+
preamble = ""
|
|
2422
|
+
if delivery is SkillDelivery.DIRECTORY:
|
|
2423
|
+
assert sandbox is not None
|
|
2424
|
+
# Registered before the write, so a write that fails half-way is cleaned too.
|
|
2425
|
+
stack.push_async_callback(
|
|
2426
|
+
remove_skills, sandbox.harness_config_dir, state.skills, uid=sandbox.uid
|
|
2427
|
+
)
|
|
2428
|
+
await write_skills(sandbox.harness_config_dir, state.skills, uid=sandbox.uid)
|
|
2429
|
+
else:
|
|
2430
|
+
preamble = skills_preamble(state.skills)
|
|
2431
|
+
await self._fastapi_client.append_evidence(
|
|
2432
|
+
work_record_id,
|
|
2433
|
+
source=SKILLS_EVIDENCE_SOURCE,
|
|
2434
|
+
payload={
|
|
2435
|
+
"event": "skills.delivered",
|
|
2436
|
+
"directive_number": directive_number,
|
|
2437
|
+
"agent_id": state.agent_id,
|
|
2438
|
+
"cli_kind": state.cli_kind,
|
|
2439
|
+
"delivery": delivery.value,
|
|
2440
|
+
"skills": [
|
|
2441
|
+
{"slug": skill.slug, "version": skill.version, "sha256": skill.sha256}
|
|
2442
|
+
for skill in state.skills
|
|
2443
|
+
],
|
|
2444
|
+
},
|
|
2445
|
+
)
|
|
2446
|
+
return preamble
|
|
2447
|
+
|
|
2347
2448
|
async def _plan_mcp(
|
|
2348
2449
|
self,
|
|
2349
2450
|
work_record_id: str,
|
|
@@ -2995,6 +3096,7 @@ class RunnerRalphActivities:
|
|
|
2995
3096
|
experience=runtime_context.experience,
|
|
2996
3097
|
experience_lesson_ids=tuple(runtime_context.experience_lesson_ids),
|
|
2997
3098
|
experience_truncated=runtime_context.experience_truncated,
|
|
3099
|
+
skills=tuple(runtime_context.skills),
|
|
2998
3100
|
workspace_path=resolved_workspace,
|
|
2999
3101
|
branch_head_sha=branch_head_sha,
|
|
3000
3102
|
product_id=runtime_context.product_id,
|
|
@@ -4253,7 +4355,8 @@ class RunnerRalphActivities:
|
|
|
4253
4355
|
extra_env=attempt.env,
|
|
4254
4356
|
mcp_servers=attempt.mcp_servers,
|
|
4255
4357
|
egress_allow_list=attempt.egress_allow_list,
|
|
4256
|
-
prompt=
|
|
4358
|
+
prompt=attempt.skills_preamble
|
|
4359
|
+
+ _learning_prompt(
|
|
4257
4360
|
request,
|
|
4258
4361
|
base_branch=state.base_branch or request.base_ref,
|
|
4259
4362
|
transcript=self._learner_transcript(state, request.work_record_id),
|
|
@@ -4488,7 +4591,7 @@ class RunnerRalphActivities:
|
|
|
4488
4591
|
extra_env=attempt.env,
|
|
4489
4592
|
mcp_servers=attempt.mcp_servers,
|
|
4490
4593
|
egress_allow_list=attempt.egress_allow_list,
|
|
4491
|
-
prompt=await kind.prompt(state, prompt_notes),
|
|
4594
|
+
prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
|
|
4492
4595
|
base_branch=request.base_ref,
|
|
4493
4596
|
work_branch=state.work_branch,
|
|
4494
4597
|
)
|
|
@@ -64,6 +64,10 @@ DIRECTIVE_TOKEN_PATH = "/api/runner/v1/runners/directive-token"
|
|
|
64
64
|
INTAKE_WORK_RECORDS_PATH = "/api/runner/v1/runners/intake/work-records"
|
|
65
65
|
INTAKE_IGNORED_PATH = "/api/runner/v1/runners/intake/ignored"
|
|
66
66
|
|
|
67
|
+
# The refusal a revoked Runner's heartbeat earns (06 §2: revocation *is* the refused
|
|
68
|
+
# heartbeat). The one reason this process stops on rather than retries.
|
|
69
|
+
RUNNER_REVOKED_REASON = "runner_revoked"
|
|
70
|
+
|
|
67
71
|
# What the state directory holds after a successful bootstrap. One file, mode 0600: the
|
|
68
72
|
# private key is in it, and a Runner that loses it re-bootstraps rather than recovering.
|
|
69
73
|
STATE_FILENAME = "runner-identity.json"
|
|
@@ -64,6 +64,7 @@ from agentic_runner.llm_proxy import CeilingStore, LlmProxy, SlotStore, UsageOut
|
|
|
64
64
|
from agentic_runner.mcp import ToolServerHealthLog
|
|
65
65
|
from agentic_runner.message_store import MESSAGES_DIR, MessageStore, TemporalWorkflowSignaller
|
|
66
66
|
from agentic_runner.registration import (
|
|
67
|
+
RUNNER_REVOKED_REASON,
|
|
67
68
|
RunnerRegistrationClient,
|
|
68
69
|
RunnerRegistrationError,
|
|
69
70
|
RunnerState,
|
|
@@ -890,6 +891,8 @@ async def _serve_registered(
|
|
|
890
891
|
await link.exchange()
|
|
891
892
|
break
|
|
892
893
|
except Exception as error: # noqa: BLE001 - keep trying until the control plane answers
|
|
894
|
+
if _revoked(error):
|
|
895
|
+
return 1
|
|
893
896
|
_logger.warning("heartbeat failed before start: %s", error)
|
|
894
897
|
if stop is not None and stop.is_set():
|
|
895
898
|
return 1
|
|
@@ -931,7 +934,11 @@ async def _serve_registered(
|
|
|
931
934
|
graceful_shutdown_timeout=graceful_shutdown_timeout(settings),
|
|
932
935
|
max_concurrent_activities=state.max_concurrent_directives,
|
|
933
936
|
)
|
|
934
|
-
|
|
937
|
+
stop = stop or asyncio.Event()
|
|
938
|
+
revoked = asyncio.Event()
|
|
939
|
+
beat = asyncio.create_task(
|
|
940
|
+
_heartbeat_forever(link, stream, client, interval, stop=stop, revoked=revoked)
|
|
941
|
+
)
|
|
935
942
|
poll = asyncio.create_task(_poll_sources_forever(stream.sources, _source_poll_seconds()))
|
|
936
943
|
readiness.ready = True
|
|
937
944
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_STARTUP)
|
|
@@ -945,7 +952,20 @@ async def _serve_registered(
|
|
|
945
952
|
await task
|
|
946
953
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_SHUTDOWN)
|
|
947
954
|
await _report_stop(link, lifecycle)
|
|
948
|
-
return 0
|
|
955
|
+
return 1 if revoked.is_set() else 0
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _revoked(error: Exception) -> bool:
|
|
959
|
+
"""Whether a failed heartbeat was the control plane revoking this Runner.
|
|
960
|
+
|
|
961
|
+
Every other failure is a lost beat and the next tick retries it; a revocation is
|
|
962
|
+
final, so retrying would only dial a control plane that has already said no.
|
|
963
|
+
"""
|
|
964
|
+
|
|
965
|
+
if isinstance(error, RunnerRegistrationError) and error.reason == RUNNER_REVOKED_REASON:
|
|
966
|
+
print(f"revoked by the control plane: {error}", flush=True)
|
|
967
|
+
return True
|
|
968
|
+
return False
|
|
949
969
|
|
|
950
970
|
|
|
951
971
|
async def _report_stop(link: HeartbeatLink, lifecycle: LifecycleOutbox) -> None:
|
|
@@ -959,13 +979,20 @@ async def _report_stop(link: HeartbeatLink, lifecycle: LifecycleOutbox) -> None:
|
|
|
959
979
|
|
|
960
980
|
|
|
961
981
|
async def _heartbeat_forever(
|
|
962
|
-
link: HeartbeatLink,
|
|
982
|
+
link: HeartbeatLink,
|
|
983
|
+
stream: ControlPlaneStream,
|
|
984
|
+
client: Client,
|
|
985
|
+
interval: float,
|
|
986
|
+
*,
|
|
987
|
+
stop: asyncio.Event,
|
|
988
|
+
revoked: asyncio.Event,
|
|
963
989
|
) -> None:
|
|
964
990
|
"""Every 30 s, for as long as the process lives; a missed beat is logged, not fatal.
|
|
965
991
|
|
|
966
992
|
Staleness is the link's own property (issue 44): after three minutes of failures the
|
|
967
993
|
verb seams refuse on their own, so there is nothing for this loop to do about a
|
|
968
|
-
failure except try again on the next tick.
|
|
994
|
+
failure except try again on the next tick. A revocation is the exception: it drains
|
|
995
|
+
the Worker exactly as SIGTERM would, and the process exits non-zero.
|
|
969
996
|
"""
|
|
970
997
|
|
|
971
998
|
while True:
|
|
@@ -974,6 +1001,10 @@ async def _heartbeat_forever(
|
|
|
974
1001
|
try:
|
|
975
1002
|
await link.exchange()
|
|
976
1003
|
except Exception as error: # noqa: BLE001 - any failed exchange is a lost beat
|
|
1004
|
+
if _revoked(error):
|
|
1005
|
+
revoked.set()
|
|
1006
|
+
stop.set()
|
|
1007
|
+
return
|
|
977
1008
|
_logger.warning("heartbeat failed: %s", error)
|
|
978
1009
|
continue
|
|
979
1010
|
if stream.runner_token is not None and stream.runner_token != token_before:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The Runner conformance kit (runner-repo issue 07, ``docs/conformance.md``).
|
|
2
|
+
|
|
3
|
+
A fake control plane (:class:`FakeControlPlane`) and a scenario suite that proves a Runner
|
|
4
|
+
build works without the platform: ``pip install 'agentic-runner[testing]'`` and then
|
|
5
|
+
``pytest --pyargs agentic_runner.testing``. The fake itself needs only the Runner's own
|
|
6
|
+
dependencies, so ``python -m agentic_runner.testing`` serves it from any Runner install,
|
|
7
|
+
the image included; pytest and the scenario fixtures come with the extra.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from agentic_runner.testing.control_plane import (
|
|
11
|
+
DEFAULT_NAMESPACE,
|
|
12
|
+
RUNNER_PREFIX,
|
|
13
|
+
Evidence,
|
|
14
|
+
FakeControlPlane,
|
|
15
|
+
Refusal,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["DEFAULT_NAMESPACE", "RUNNER_PREFIX", "Evidence", "FakeControlPlane", "Refusal"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""``python -m agentic_runner.testing [port]``: serve the fake control plane until killed.
|
|
2
|
+
|
|
3
|
+
What the chart, Docker and workstation tests register a Runner against; ``GET /stats``
|
|
4
|
+
reports what it saw.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from agentic_runner.testing.control_plane import DEFAULT_NAMESPACE, FakeControlPlane
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="python -m agentic_runner.testing")
|
|
16
|
+
parser.add_argument("port", type=int, nargs="?", default=8000)
|
|
17
|
+
parser.add_argument("--host", default="0.0.0.0")
|
|
18
|
+
parser.add_argument("--namespace", default=DEFAULT_NAMESPACE)
|
|
19
|
+
parser.add_argument("--host-party", default="organisation")
|
|
20
|
+
arguments = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
plane = FakeControlPlane(namespace=arguments.namespace, host_party=arguments.host_party)
|
|
23
|
+
server = plane.server(arguments.host, arguments.port)
|
|
24
|
+
print(
|
|
25
|
+
f"fake control plane on :{server.server_address[1]}, namespace {arguments.namespace}",
|
|
26
|
+
flush=True,
|
|
27
|
+
)
|
|
28
|
+
server.serve_forever()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|