Skip to content
Merged

Dev #356

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
63a2886
Prompt Profiler
ahmad-ajmal Jun 15, 2026
359009b
fix(profiler): capture cache tokens for all LLM providers
ahmad-ajmal Jun 16, 2026
7308d10
Select action in task prompt optimization
ahmad-ajmal Jun 16, 2026
e4dfff9
Improve chance for agent to read the AGENT.md
Jun 17, 2026
fae8565
Update tasks list UI
Jun 19, 2026
a4f4aa7
fix relevance calculation and added bm25
Jun 20, 2026
5d6c76a
improved memory system
Jun 20, 2026
7161156
refactor code and remove recency logic
Jun 20, 2026
bdf706f
fix #340: guard flush when worker stdout is None
false200 Jun 22, 2026
c4a721c
Merge pull request #341 from false200/fix/run-python-stdio-flush-340
makiroll1125 Jun 23, 2026
f3d48de
Prompt update to remove creation actions + system prompt workflow cle…
ahmad-ajmal Jun 23, 2026
f7536a0
revert write_file and added set_requirement action
Jun 24, 2026
52cde75
clarify state
ahmad-ajmal Jun 24, 2026
a77483a
memory injection and retrieval update
Jun 24, 2026
72805ae
Merge branch 'improvement/prompt-optimization' into feature/set-requi…
ahmad-ajmal Jun 24, 2026
14151ba
Merge pull request #339 from CraftOS-dev/improvement/memory-update
CraftOS-dev Jun 26, 2026
1cf4e43
Fix stream_read reference in action output externalization instructions
AlanAAG Jun 26, 2026
4162d26
add event type to event
Jun 26, 2026
f3742b2
basic of sub agent
Jun 21, 2026
bf42102
refactoring base implemetation
Jun 21, 2026
7cf04e1
Update agent workflow to encourage usage of subagent
Jun 22, 2026
bc5e091
improve sub-agent instruction more
Jun 22, 2026
fa75e2b
Click reply button also put cursor in the input box
AlanAAG Jun 26, 2026
065068a
pdf conversion actions
ahmad-ajmal Jun 26, 2026
58a4b31
protect set requirements from summary
ahmad-ajmal Jun 26, 2026
fbc653e
Merge pull request #345 from CraftOS-dev/feature/sub-agent
CraftOS-dev Jun 26, 2026
8cd7403
revert write file and add convert to pdf action
Jun 27, 2026
80e1ee9
add warning to convert pdf action for custom format
Jun 27, 2026
42e98e6
Merge pull request #343 from CraftOS-dev/feature/set-requirement
CraftOS-dev Jun 27, 2026
85ff80b
shorten whatsapp bridge teardown to speed up startup time
Jun 27, 2026
7c54855
bug:fix deepseek crash agent runtime due to missing VLM issue
Jun 27, 2026
7de8537
VLM unavailable injected message update
Jun 27, 2026
f5864b0
Merge branch 'V1.4.0' into improvement/prompt-optimization
CraftOS-dev Jun 29, 2026
c725705
Merge pull request #342 from CraftOS-dev/improvement/prompt-optimization
CraftOS-dev Jun 29, 2026
18661b3
event stream threshold reductions + datetime
ahmad-ajmal Jun 29, 2026
fcf7429
sub agent logging + notion action
ahmad-ajmal Jun 29, 2026
5b43867
folder logs per run
ahmad-ajmal Jun 29, 2026
c73e861
feat(providers): add Z.ai (GLM-5.2) and Sakana (Fugu) providers
korivi-CraftOS Jun 29, 2026
3378726
major UI update: side panel, copy chat and delete task button
Jun 30, 2026
8107790
mascot screen visibility setting
Jun 30, 2026
9f9fe8f
Add: Living UI palette theme button
makiroll1125 Jun 30, 2026
4fbd7ae
Out-of-credits now shows the actual billing error, the failure counte…
ahmad-ajmal Jun 30, 2026
e23e6f5
Spawn Subagent failing with error + Grok not caching
ahmad-ajmal Jul 1, 2026
09d2cdd
GET HTTP Request when downloading files externalises the result and h…
ahmad-ajmal Jul 1, 2026
a84aa41
Reset agent should show user a checklist
ahmad-ajmal Jul 1, 2026
00e160c
added ChatGPT and Grok subscription OAuth flows
Jul 1, 2026
eba7a15
added ChatGPT and Grok subscription OAuth flows
Jul 1, 2026
1dbacbd
update activated model after model selection
Jul 1, 2026
116ddf1
Merge pull request #349 from CraftOS-dev/improvement/living-ui-theme
CraftOS-dev Jul 1, 2026
1996902
Add FUNDING file
Jul 1, 2026
e426aa0
UI update: moving panel and mobile compatability
Jul 1, 2026
fb4dfe7
minor UI update: setting page bottom margin
Jul 1, 2026
b0f5e38
Feature/message catalogue (#350)
makiroll1125 Jul 2, 2026
ace3196
internal action interface, correct order of args in use_llm action
AlanAAG Jul 2, 2026
9a44fb6
Feature/prompt ai enhancer (#346)
AlanAAG Jul 2, 2026
453acdb
fix free plan on openai connection failed
Jul 2, 2026
2a2b8e6
Merge pull request #351 from CraftOS-dev/feat/oauth-subscription-auth
CraftOS-dev Jul 2, 2026
967698e
Merge pull request #352 from CraftOS-dev/korivi-Updates-V1.4.0
CraftOS-dev Jul 2, 2026
4dd47d7
Update limits for externalize and read file
ahmad-ajmal Jul 2, 2026
2d3a268
externalize sub-agent response
ahmad-ajmal Jul 2, 2026
bfc2e9e
- create a read_pdf variant that only returns the content
ahmad-ajmal Jul 2, 2026
e6c6b48
update hard-onboarding provider info and sakana ai error handling
Jul 2, 2026
408ac36
Merge branch 'V1.4.0' of https://github.com/craftos-dev/craftbot into…
Jul 2, 2026
2391f9c
disable validation agent and increase run shell time out
Jul 3, 2026
82309e7
added environment info to sub agent
Jul 3, 2026
40b8384
Update version in setting
Jul 3, 2026
81aaef4
Update AGENT.md
Jul 3, 2026
d4f850b
Merge pull request #353 from CraftOS-dev/V1.4.0
CraftOS-dev Jul 3, 2026
595ec2e
Defer OpenAI/Anthropic SDK imports in model factory (#348)
korivi-CraftOS Jun 29, 2026
bd2962d
Merge remote-tracking branch 'origin/staging' into dev
Jul 3, 2026
8d39678
fix ruff check
Jul 3, 2026
5558698
ruff check
Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [craftos-dev]
9 changes: 9 additions & 0 deletions agent_core/core/action_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
load_actions_from_directories,
DEFAULT_ACTION_PATHS,
)
from agent_core.core.action_framework.formatting import (
candidate_dict_from_action,
format_action_candidates,
format_actions_by_name,
)

__all__ = [
# Registry classes
Expand All @@ -36,4 +41,8 @@
"PLATFORM_LINUX",
"PLATFORM_WINDOWS",
"PLATFORM_DARWIN",
# Formatting
"candidate_dict_from_action",
"format_action_candidates",
"format_actions_by_name",
]
126 changes: 126 additions & 0 deletions agent_core/core/action_framework/formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""
Shared formatters for action lists embedded in LLM prompts.

This module owns the canonical compact representation used to describe
available actions to the LLM. It is intentionally tight on tokens:

- One JSON object per action with ``name``, ``description``, ``params``.
- Each parameter collapses to a single string ``"<type>, required|optional - <desc>"``.
- No ``example`` fields, no nested type-definitions.

Both ``ActionRouter`` (main agent) and ``SubAgentContextEngine`` (sub-agents)
should call ``format_action_candidates`` so the format stays in sync.
"""

from __future__ import annotations

import json
from typing import Any, Dict, List, Optional


def candidate_dict_from_action(action) -> Dict[str, Any]:
"""Project an ``Action`` (or registry equivalent) into the candidate dict
shape consumed by ``format_action_candidates``.

Tolerates the duck-typed shape used throughout agent_core: anything with
``name``, ``description``, ``action_type``, ``input_schema``,
``output_schema`` attributes (or matching dict keys).
"""
if isinstance(action, dict):
return {
"name": action.get("name"),
"description": action.get("description", ""),
"type": action.get("action_type") or action.get("type"),
"input_schema": action.get("input_schema") or {},
"output_schema": action.get("output_schema") or {},
}
return {
"name": getattr(action, "name", None),
"description": getattr(action, "description", "") or "",
"type": getattr(action, "action_type", None),
"input_schema": getattr(action, "input_schema", {}) or {},
"output_schema": getattr(action, "output_schema", {}) or {},
}


def format_action_candidates(candidates: List[Dict[str, Any]]) -> str:
"""Render a candidate list as the compact JSON block sent to the LLM.

Args:
candidates: List of dicts each with ``name``, ``description``,
``input_schema`` keys. Use :func:`candidate_dict_from_action`
to build entries from ``Action`` objects.

Returns:
A JSON-formatted string (pretty-printed) describing the candidates.
Returns ``"[]"`` when the list is empty.
"""
if not candidates:
return "[]"

compact: List[Dict[str, Any]] = []
for c in candidates:
input_schema = c.get("input_schema") or {}
params: Dict[str, str] = {}
for param_name, param_def in input_schema.items():
if isinstance(param_def, dict):
ptype = param_def.get("type", "any")
desc = param_def.get("description", "") or ""
# Match the heuristic used by ActionRouter._format_candidates:
# treat parameters whose description mentions "default" or
# "optional" as optional, everything else required.
low = desc.lower()
is_optional = "default" in low or "optional" in low
req = "optional" if is_optional else "required"
params[param_name] = f"{ptype}, {req} - {desc}"
else:
params[param_name] = str(param_def)

compact.append(
{
"name": c.get("name"),
"description": c.get("description", "") or "",
"params": params,
}
)

return json.dumps(compact, indent=2, ensure_ascii=False)


def format_actions_by_name(
action_names: List[str],
action_library,
*,
on_missing: Optional[str] = None,
) -> str:
"""Convenience: look up actions by name and render them.

Args:
action_names: Ordered list of action names to include.
action_library: Anything with ``retrieve_action(name) -> Action | None``.
on_missing: Optional log-message prefix for actions that aren't found;
when None, missing actions are silently skipped.

Returns:
Compact JSON block (or ``"[]"`` if nothing resolved).
"""
candidates: List[Dict[str, Any]] = []
for name in action_names:
act = action_library.retrieve_action(name)
if act is None:
if on_missing is not None:
# Use late import to avoid pulling logging deps at module load.
from agent_core.utils.logger import logger

logger.warning(f"{on_missing}: action {name!r} not found in library")
continue
candidates.append(candidate_dict_from_action(act))
return format_action_candidates(candidates)


__all__ = [
"candidate_dict_from_action",
"format_action_candidates",
"format_actions_by_name",
]
154 changes: 147 additions & 7 deletions agent_core/core/event_stream/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,135 @@

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Optional


SEVERITIES = ("DEBUG", "INFO", "WARN", "ERROR")


class EventType(str, Enum):
"""Closed set of event categories.

The UI transformer routes solely on this field. New event categories
are added here, not invented at call sites as ad-hoc `kind` strings.

INVARIANT: nothing in the consumer path (UI transformer, etc.) is
allowed to look at `kind` or `message` substrings to decide how to
handle an event. Producers MUST set `event_type` explicitly when
calling `log()`. The only place `kind` is consulted for typing is
`_legacy_event_type_from_kind` below — used exclusively to upgrade
events restored from persistence written before this field existed.
"""

USER_MESSAGE = "user_message"
AGENT_MESSAGE = "agent_message"
SYSTEM = "system"
ERROR = "error"
REASONING = "reasoning"
ACTION_START = "action_start"
ACTION_END = "action_end"
TASK_START = "task_start"
TASK_END = "task_end"
WAITING_FOR_USER = "waiting_for_user"
RELEVANT_MEMORIES = "relevant_memories"
TODOS = "todos"
INTERNAL = "internal"


# Legacy `kind` → `event_type` mapping. NEW code MUST NOT call this.
# It exists solely so that EVENT.md / sessions.db entries written before
# the `event_type` field was introduced still render correctly after
# upgrade. Once all such persistence has rolled over, this map can be
# deleted along with `_legacy_event_type_from_kind`.
_LEGACY_KIND_TO_EVENT_TYPE: Dict[str, "EventType"] = {
"action_start": EventType.ACTION_START,
"action_end": EventType.ACTION_END,
"action_error": EventType.ACTION_END,
"gui action start": EventType.ACTION_START,
"gui action end": EventType.ACTION_END,
"task_start": EventType.TASK_START,
"task_started": EventType.TASK_START,
"task_end": EventType.TASK_END,
"task_ended": EventType.TASK_END,
"agent reasoning": EventType.REASONING,
"reasoning": EventType.REASONING,
"waiting_for_user": EventType.WAITING_FOR_USER,
"relevant_memories": EventType.RELEVANT_MEMORIES,
"system": EventType.SYSTEM,
"error": EventType.ERROR,
"warning": EventType.SYSTEM,
"loop_detection_warning": EventType.SYSTEM,
"internal": EventType.INTERNAL,
"todos": EventType.TODOS,
}


def _legacy_event_type_from_kind(kind: Optional[str]) -> Optional["EventType"]:
"""Map a legacy free-text `kind` string to an `EventType`.

DO NOT call this for routing decisions in new code. It is only used
by `Event.from_dict()` to upgrade persisted events that lack an
explicit `event_type` field.
"""
if not kind:
return None
k = kind.lower().strip()
if k in _LEGACY_KIND_TO_EVENT_TYPE:
return _LEGACY_KIND_TO_EVENT_TYPE[k]
if k.startswith("agent message"):
return EventType.AGENT_MESSAGE
if k.startswith("user message"):
return EventType.USER_MESSAGE
return None


@dataclass
class Event:
"""
Public event object with prompt context and display variants.

Attributes:
message: The full event message for prompts and debugging
kind: Category describing the event family (e.g., "action_start")
severity: Importance level (DEBUG, INFO, WARN, ERROR)
display_message: Optional alternative message for UI display
ts: Timestamp when event was created (UTC)
message: The full event message for prompts and debugging.
kind: Human-readable label for the prompt-facing snapshot
(e.g., ``"agent message to platform: Telegram"``). NOT used
by the UI transformer for routing — see `event_type`.
severity: Importance level (DEBUG, INFO, WARN, ERROR).
display_message: Optional alternative message for UI display.
ts: Timestamp when event was created (UTC).
event_type: Closed-set category used by consumers for routing,
hiding, and rendering. Producers set this explicitly.
action_name: Canonical action identifier when this event belongs
to an action lifecycle (start/end). None otherwise.
action_display_name: User-facing name (typically the snake_case
action name reformatted to "Title case"). Optional; consumers
fall back to `action_name` when absent.
action_id: Stable identifier paired across an action's start and
end events so consumers can correlate them without parsing.
Set by ``ActionManager`` (which generates it as ``run_id``
internally) so multiple parallel calls of the same action
can still be matched start↔end.
action_input: Structured input payload at action_start.
action_output: Structured output payload at action_end.
task_status: ``"completed"`` | ``"error"`` | ``"cancelled"`` for
TASK_END events.
platform: Originating/destination platform for chat messages
(e.g., ``"Telegram"``, ``"CraftBot Interface"``).
"""

message: str
kind: str
severity: str
display_message: Optional[str] = None
ts: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
event_type: Optional[EventType] = None
action_name: Optional[str] = None
action_display_name: Optional[str] = None
action_id: Optional[str] = None
action_input: Optional[Dict[str, Any]] = None
action_output: Optional[Dict[str, Any]] = None
task_status: Optional[str] = None
platform: Optional[str] = None

def display_text(self) -> Optional[str]:
"""
Expand All @@ -72,22 +177,53 @@ def to_dict(self) -> Dict[str, Any]:
"severity": self.severity,
"display_message": self.display_message,
"ts": self.ts.isoformat(),
"event_type": self.event_type.value if self.event_type else None,
"action_name": self.action_name,
"action_display_name": self.action_display_name,
"action_id": self.action_id,
"action_input": self.action_input,
"action_output": self.action_output,
"task_status": self.task_status,
"platform": self.platform,
}

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Event":
"""Deserialize an event from a dictionary."""
"""Deserialize an event from a dictionary.

Events written before `event_type` existed have no value for that
field; we upgrade them here by mapping `kind` once at load time.
New events MUST set `event_type` at log() time, so this
upgrade path stays cold for fresh writes.
"""
ts = (
datetime.fromisoformat(data["ts"])
if isinstance(data.get("ts"), str)
else datetime.now(timezone.utc)
)
raw_event_type = data.get("event_type")
event_type: Optional[EventType]
if raw_event_type:
try:
event_type = EventType(raw_event_type)
except ValueError:
event_type = None
else:
event_type = _legacy_event_type_from_kind(data.get("kind"))
return cls(
message=data["message"],
kind=data["kind"],
severity=data["severity"],
display_message=data.get("display_message"),
ts=ts,
event_type=event_type,
action_name=data.get("action_name"),
action_display_name=data.get("action_display_name"),
action_id=data.get("action_id"),
action_input=data.get("action_input"),
action_output=data.get("action_output"),
task_status=data.get("task_status"),
platform=data.get("platform"),
)

@property
Expand Down Expand Up @@ -146,11 +282,15 @@ def compact_line(self) -> str:
Generate a compact single-line representation of this event.

Format: "HH:MM:SS [kind]: message" with optional " xN" suffix for repeats.
The time is rendered in LOCAL time: storage stays UTC, but everything the
model sees must agree with the local wall clock the context engine's
current-datetime block reports (and with the log files a human reads
alongside). A naive ts (legacy persisted data) is assumed local.

Returns:
Compact string representation
"""
t = self.ts.strftime("%H:%M:%S")
t = self.ts.astimezone().strftime("%H:%M:%S")
k = self.event.kind
msg = self.event.message
suffix = f" x{self.repeat_count}" if self.repeat_count > 1 else ""
Expand Down
6 changes: 4 additions & 2 deletions agent_core/core/impl/action/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,10 @@ def _suppress_worker_stdio():

Returns (saved_stdout_fd, saved_stderr_fd) for later restoration.
"""
sys.stdout.flush()
sys.stderr.flush()
if sys.stdout is not None:
sys.stdout.flush()
if sys.stderr is not None:
sys.stderr.flush()
devnull_fd = os.open(os.devnull, os.O_WRONLY)
saved_stdout = os.dup(1)
saved_stderr = os.dup(2)
Expand Down
Loading
Loading